赞
踩
如果我们想把一系列执行结果放到 List 集合中,可能会这样实现:
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
// 处理业务
list.add(i);
}
为了提升执行结果,我们可能会用到多线程:
public static void main(String[] args) throws InterruptedException { ThreadPoolExecutor executor = new ThreadPoolExecutor( 10, 10, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(10000), r -> new Thread(r, "DemoThread-" + r.hashCode())); List<Integer> list = new ArrayList<>(); CountDownLatch countDownLatch = new CountDownLatch(1000); for (int i = 0; i < 1000; i++) { int num = i; executor.execute(() -> { try { // 处理业务 Thread.sleep(100L); list.add(num); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { countDownLatch.countDown(); } }); } countDownLatch.await(); list.sort(Integer::compareTo); list.forEach(System.out::println); executor.shutdown(); }
执行结果:
可以看到莫名其妙抛出了 ArrayIndexOutOfBoundsException
异常,而且结果中也出现了null,虽然我们知道 ArrayList<>
是线程不安全的,但是具体是为什么呢?
首先看看 ArrayList 类所拥有的部分属性字段:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
/**
* 列表元素集合数组
* 如果新建ArrayList对象时没有指定大小,那么会将EMPTY_ELEMENTDATA赋值给elementData,
* 并在第一次添加元素时,将列表容量设置为DEFAULT_CAPACITY
*/
transient Object[] elementData;
/**
* 列表大小,elementData中存储的元素个数
*/
private int size;
}
通过这两个字段我们可以看出,ArrayList 的实现主要就是用了一个 Object[]
,用来保存所有的元素,以及一个 size 变量用来保存当前数组中已经添加了多少元素。
接着我们看下最重要的 add 操作时的源码:
public boolean add(E e) {
/**
* 添加一个元素时,做了如下两步操作
* 1.判断列表的capacity容量是否足够,是否需要扩容
* 2.真正将元素放在列表的元素数组里面
*/
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
ensureCapacityInternal()
这个方法的详细代码我们可以暂时不看,它的作用就是判断如果将当前的新元素添加到列表后面,列表的 elementData 数组的大小是否满足,如果 size+1 的这个需求长度大于 elementData 数组的长度,那么就要对这个数组进行扩容。由此看到 add 元素时,实际做了两个大的步骤:
针对这两个步骤,就出现了两个导致线程不安全的隐患。
在第1步 ensureCapacityInternal(size + 1)
中,如果多个线程进行调用可能会导致 elementData 数组越界,具体逻辑如下:
第二步 elementData[size++]=e
设置值的操作同样会导致线程不安全。从这里可以看出,这部操作也不是一个原子操作,它由如下两步操作构成:
在单线程执行这两行代码时,没有任何问题,但是当多线程环境下执行时,可能就会发生一个线程的值覆盖另一个线程添加的值,具体逻辑如下:
这样,线程 A、B 执行完毕后,
因为线程 A、B 执行完毕后,size=2,所以下一个线程添加元素时,会从下标为 2 的位置上开始:elementData[2]=C。
那么如何解决 ArrayList 的线程安全问题呢?这时候就需要我们的主角 CopyOnWriteArrayList
登场了。
CopyOnWriteArrayList
:是 JDK 在并发包(java.util.concurrent)下的一个类,它是 ArrayList 的一个线程安全的变体。CopyOnWrite 即 读写分离,读时共享,写时复制,通俗的理解是:当我们往一个 List 添加元素的时候,不直接往当前 List 添加,而是先将当前 List 进行 Copy,复制出一个新的 List,然后新的容器里添加元素,添加完元素之后,再将原 List 的引用指向新的 List。 这种设计使得读操作可以在原数组上进行,而不需要加锁,从而大大提高了读操作的并发性。
UnsupportedOperationException
异常。这是因为迭代器在迭代过程中持有的是原始数组的引用,而修改操作会导致底层数组的复制和替换,从而导致迭代器的引用失效。因此,如果需要在迭代过程中修改列表,应该使用其他的数据结构或者采取其他的并发控制措施。我们再用 CopyOnWriteArrayList
重新实现最开始我们提到的问题:
public static void main(String[] args) throws InterruptedException { ThreadPoolExecutor executor = new ThreadPoolExecutor( 10, 10, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(10000), r -> new Thread(r, "DemoThread-" + r.hashCode())); CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(); CountDownLatch countDownLatch = new CountDownLatch(1000); for (int i = 0; i < 1000; i++) { int num = i; executor.execute(() -> { try { // 处理业务 Thread.sleep(100L); list.add(num); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { countDownLatch.countDown(); } }); } countDownLatch.await(); list.forEach(System.out::println); executor.shutdown(); }
执行结果:
可以看到,没有报错,没有 null 值,完美~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。