1. 多线程下的ArrayList
public class ConcurrentArrayList {
public static void main(String[] args) throws InterruptedException {
List<Integer> list = new ArrayList<>();
Runnable runnable = () -> {
for (int i = 0; i < 10000; i++) {
list.add(i);
}
};
for (int i = 0; i < 2; i++) {
new Thread(runnable).start();
}
Thread.sleep(500);
System.out.println(list.size());
}
}
上述运行可能会出现 代码中循环创建了两个线程,这两个线程都执行10000次数组的添加操作,理论上最后输出的结果应该为20000,但经过多次尝试,最后只出现了两种结果:
1.数组索引越界异常 2.list.size()不为20000
看一下add()方法的代码:
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
在上述过程中,会出问题的部分在于: 1. 增加元素 2. 扩充数组长度;
增加元素过程中较为容易出现问题的部分在于elementData[size++] = e; .赋值的过程可以分为两个步骤elementData[size] = e;size++;
我们分别使用两个线程来模拟插入过程.例如有两个线程,分别加入数字1与2.
运行的过程如下所示:
线程1 赋值 element[1] = 1; 随后因为时间片用完而中断; 线程2 赋值 element[1] = 2; 随后因为时间片用完中断; 此处导致了之前所说的一个问题(有的线程没有输出); 因为后续的线程将前面的线程的值覆盖了. 线程1 自增 size++; (size=2) 线程2 自增 size++; (size=3) 此处导致了某些值为null的问题.因为原size=1, 但是因为线程1与线程2都将值赋值给了element[1],导致了element[2]内没有值,被跳过了.指针index指向了3.所以,导致了某些情况下值为null的情况.
产生了null值 以及数据丢失的问题
数组越界情况. 我们将上方的线程运行图更新下进行演示:
前提条件: 当前size=2 数组长度为2.
线程1 判断数组是否越界.因为size=2 长度为2,没有越界.将进行赋值操作.但是因为时间片问题导致了中断. 线程2 判断数组是否越界.因为size=2 长度为2,没有越界.将进行赋值操作.但是因为时间片问题导致了中断. 线程1 重新获取到主动权.上文判断了长度刚刚好够用.进行赋值操作element[size]=1,并且size++ 线程2 因为上文判断了数组没有越界.所以进行赋值操作.但是此时的size=3了.再执行element[3]=2. 导致了数组越界了. 由此处可以看出因为数组的当前指向size并未进行加锁的操作,导致了数组越界的情况出现.
核心问题就在于 elementData[size++] = e;不是原子操作
2.线程安全的list解决方案:
有三种方式:
-
vector 在add方法上加上synchronized锁 -
Collections的静态方法synchronizedList(List< T> list) -
copyonwritearraylist写时复制的思想
1.使用Vector容器
Vector类实现了可扩展的对象数组,并且它是线程安全的。Vector的解决方案很简单,它采用了同步关键词synchronized修饰方法。
public void add(int index, E element) {
insertElementAt(element, index);
}
...
public synchronized void insertElementAt(E obj, int index) {
modCount++;
if (index > elementCount) {
throw new ArrayIndexOutOfBoundsException(index
+ " > " + elementCount);
}
ensureCapacityHelper(elementCount + 1);
System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
elementData[index] = obj;
elementCount++;
}
可以看出,Vector在通用方法的实现上ArrayList并没有什么区别(这里不比较扩容方式等细节)
2. Collections.synchronizedList(List< T> list)
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
因为ArrayList实现了RandomAccess接口,因此该方法返回一个SynchronizedRandomAccessList实例。 该类的add实现:
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
其中,mutex是final修饰的一个对象:
final Object mutex;
我们可以看到,这种线程安全容器是通过同步代码块来实现的,基础的add方法任然是由ArrayList实现。
我们再来看看它的读方法:
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
和写方法没什么区别,同样是使用了同步代码块。线程同步的实现原理非常简单!
通过上面的分析可以看出,无论是读操作还是写操作,它都会进行加锁,当线程的并发级别非常高时就会浪费掉大量的资源,因此某些情况下它并不是一个好的选择。针对这个问题,我们引出第三种线程安全容器的实现。
3. CopyOnWriteArrayList
CopyOnwriteArrayList 实现了 读 读操作和 读写操作不互斥
实现
public boolean add(E e) {
final ReentrantLock lock = this.lock
lock.lock();
try{
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
可以看到是通过JUC包下的lock来实现线程间的同步的,
怎么实现读写不互斥的呢?
在面临写操作的时候,CopyOnwriteArrayList会先复制原来的数组并且在新数组上进行修改,最后再将原数组覆盖。如果写操作过程中发生了线程切换。并且切换到读线程,因为此时数组并未发生覆盖,读操作读取的还是原数组。
另外,数组定义private transient volatile Object[] array,其中采用volatile修饰,保证内存可见性,读取线程可以马上知道这个修改。
private transient volatile Object[] array;
也就是说当读写并发时 读操作是在旧数组中 读到的旧值。
三种方式的性能比较
1. 首先我们来看看三种方式在写操作的情况:
public class ConcurrentList {
public static void main(String[] args) {
testVector();
testSynchronizedList();
testCopyOnWriteArrayList();
}
private static void testVector() {
Vector vector = new Vector();
long time1 = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
vector.add(i);
}
long time2 = System.currentTimeMillis();
System.out.println("vector: "+(time2-time1));
}
public static void testSynchronizedList(){
List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
long time1 = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
list.add(i);
}
long time2 = System.currentTimeMillis();
System.out.println("synchronizedList: "+(time2-time1));
}
public static void testCopyOnWriteArrayList(){
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
long time1 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
long time2 = System.currentTimeMillis();
System.out.println("copyOnWriteArrayList: "+(time2-time1));
}
}
在代码中我让Vector和SynchronizedList两种实现方式进行写操作10000000次,而CopyOnWriteArrayList仅仅只有100000次,与前两种方式少了100倍!但是耗时缺差不多
第三种方式使用的时间远大于前两种,写操作越多,时间差就越明显。
看似出乎意料,实则意料之中,copyOnWriteArrayList每进行一次写操作都会复制一次数组,这是非常耗时的操作,因此在面临巨大的写操作量时才会差异这么大。
而对于 vector和SynchronizedList的性能上的差别 主要是由于synchronized加载实例方法和加载代码块上的差别引起的,同步方法直接在方法上加synchronized实现加锁,同步代码块则在方法内部加锁,很明显,同步方法锁的范围比较大,而同步代码块范围要小点,一般同步的范围越大,性能就越差,一般需要加锁进行同步的时候,肯定是范围越小越好,这样性能更好。
如果开两个线程的情况下
public class ConcurrentList {
public static void main(String[] args) throws InterruptedException {
testVector();
testSynchronizedList();
testCopyOnWriteArrayList();
}
private static void testVector() throws InterruptedException {
Vector vector = new Vector();
Semaphore semaphore = new Semaphore(0);
long time1 = System.currentTimeMillis();
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000000; i++) {
vector.add(i);
}
semaphore.release();
}
};
for (int i = 0; i < 2; i++) {
new Thread(runnable).start();
}
semaphore.acquire(2);
long time2 = System.currentTimeMillis();
System.out.println("vector: "+(time2-time1));
}
public static void testSynchronizedList() throws InterruptedException {
List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
Semaphore semaphore = new Semaphore(0);
long time1 = System.currentTimeMillis();
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000000; i++) {
list.add(i);
}
semaphore.release();
}
};
for (int i = 0; i < 2; i++) {
new Thread(runnable).start();
}
semaphore.acquire(2);
long time2 = System.currentTimeMillis();
System.out.println("synchronizedList: "+(time2-time1));
}
public static void testCopyOnWriteArrayList() throws InterruptedException {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
Semaphore semaphore = new Semaphore(0);
long time1 = System.currentTimeMillis();
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
list.add(i);
}
semaphore.release();
}
};
for (int i = 0; i < 2; i++) {
new Thread(runnable).start();
}
semaphore.acquire(2);
long time2 = System.currentTimeMillis();
System.out.println("copyOnWriteArrayList: "+(time2-time1));
}
}
vector和synchorinzedList性能差不多
2. 我们再来看看三种方式在读操作的情况:
public class ConcurrentList2 {
public static void main(String[] args) throws InterruptedException {
testVector();
testSynchronizedList();
testCopyOnWriteArrayList();
}
public static void testVector() throws InterruptedException {
Vector<Integer> vector = new Vector<>();
vector.add(0);
Semaphore semaphore = new Semaphore(0);
long time1 = System.currentTimeMillis();
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000000; i++) {
vector.get(0);
}
semaphore.release(1);
}
};
for (int i = 0; i < 2; i++) {
new Thread(runnable).start();
}
semaphore.acquire(2);
long time2 = System.currentTimeMillis();
System.out.println("vector: "+(time2-time1));
}
public static void testSynchronizedList() throws InterruptedException {
List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
list.add(0);
Semaphore semaphore = new Semaphore(0);
long time1 = System.currentTimeMillis();
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000000; i++) {
list.get(0);
}
semaphore.release(1);
}
};
for (int i = 0; i < 2; i++) {
new Thread(runnable).start();
}
semaphore.acquire(2);
long time2 = System.currentTimeMillis();
System.out.println("synchronizedList: "+(time2-time1));
}
public static void testCopyOnWriteArrayList() throws InterruptedException {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
list.add(0);
Semaphore semaphore = new Semaphore(0);
long time1 = System.currentTimeMillis();
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000000; i++) {
list.get(0);
}
semaphore.release(1);
}
};
for (int i = 0; i < 2; i++) {
new Thread(runnable).start();
}
semaphore.acquire(2);
long time2 = System.currentTimeMillis();
System.out.println("copyOnWriteArrayList: "+(time2-time1));
}
}
测试方式不严谨哈 读写并发就不测了
可以看出 CopyOnWriteArrayList由于读不加锁 在读的性能上 非常好
总结
- 获取线程安全的List我们可以通过Vector、Collections.synchronizedList()方法和CopyOnWriteArrayList三种方式
- 得益于读写不冲突,以及读不加锁,在读多写少的情况下,推荐使用CopyOnWriteArrayList方式
|