知足常乐

日常学习的记录与分享

JAVA并发编程-ArrayList线程不安全的原因和解决方案

ArrayList

1.故障现象

public class ContainerDemo {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        Random random = new Random();
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                list.add(random.nextInt(10));
                System.out.println(list);
            }).start();
        }
    }
}

发现报 java.util.ConcurrentModificationException

2.导致原因

并发修改导致的异常

3.解决方案

  • new Vector();
  • Collections.synchronizedList(new ArrayList<>());
  • new CopyOnWriteArrayList<>();

3.1 CopyOnWriteArrayList 原理

opyOnWriteArrayList的内部也是一个数组,但这个数组是以原子方式被整体更新的。每次修改操作,都会新建一个数组,复制原数组的内容到新数组,在新数组上进行需要的修改,然后以原子方式设置内部的数组引用,这就是写时拷贝。

所有的读操作,都是先拿到当前引用的数组,然后直接访问该数组,在读的过程中,可能内部的数组引用已经被修改了,但不会影响读操作,它依旧访问原数组内容。

换句话说,数组内容是只读的,写操作都是通过新建数组,然后原子性的修改数组引用来实现的

内部数组声明为:

private volatile transient Object[] array;

注意,它声明为了volatile,这是必需的,保证内存可见性,写操作更改了之后,读操作能看到。有两个方法用来访问/设置该数组:

final Object[] getArray() {
    return array;
}
 
final void setArray(Object[] a) {
    array = a;
}

在CopyOnWriteArrayList中,读不需要锁,可以并行,读和写也可以并行,但多个线程不能同时写,每个写操作都需要先获取锁,CopyOnWriteArrayList内部使用ReentrantLock,成员声明为:

transient final ReentrantLock lock = new ReentrantLock();

add方法的代码为:


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();
    }

代码也容易理解,add方法是修改操作,整个过程需要被锁保护,先拿到当前数组elements,然后复制了个长度加1的新数组newElements,在新数组中添加元素,最后调用setArray原子性的修改内部数组引用。

4.优化建议

  • 在读多写少的时候推荐使用 CopeOnWriteArrayList 这个类
点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注