ArrayList线程不安全&写时复制

967 阅读2分钟

这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战

ArrayList是常用的一个集合类,底层基于数组实现,但是它在并发环境下线程不安全,以下是简单的一些分析。

ArrayList线程不安全

Demo

public class ArrayListNotSafeDemo {
   static List<String> list =new ArrayList<>();
    public static void main(String[] args) {
        for (int i = 0; i <3 ; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(list);
            }).start();
        }
    }
}

运行结果:

[null, 342a081b]

[null, 342a081b]

[null, 342a081b]

将i修改为10,加大并发的数量可以看到报异常:

Exception in thread "Thread-8" java.util.ConcurrentModificationException

这是并发修改异常

源码分析

//Appends the specified element to the end of this list.
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

可以看到add方法并没有加锁,所以在多个线程并发操作的时候会出现线程安全问题

解决方法

  • 1.Vector
  • 2.Collections.synchronizedList()
  • 3.CopyOnWrite(写时复制)

Vector

class VectorDemo{
    static Vector<String> vector=new Vector<>();
    public static void main(String[] args) {
        for (int i = 0; i <10 ; i++) {
            new Thread(()->{
                vector.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(vector);
            }).start();
        }
    }
}

运行后发现并没有出现线程不安全的问题(没有出现并发修改异常),我们查看Vector类的源码发现它的方法都用synchronized修饰,所以Vector是线程安全的,但是性能比较低。

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

Collections.synchronizedList()

Collections.synchronizedList(list);

相当于Collections工具类提供了一个方法给原来没有加锁的集合加锁,当然类似的还有

Collections.synchronizedMap(), Collections.synchronizedSet()等等

写时复制(重点)

class CopyOnWriteDemo {
    static List<String> list = new CopyOnWriteArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }).start();
        }
    }
}

来查看下CopyOnWriteArrayList中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();
    }
}
  • CopyOnWriteArrayList就是写时复制的容器,每次添加Objcet对象时不是直接向原Object[]数组中添加,而是复制一个长度为原数组长度+1的新数组,把要添加的数据写到新数组上。
  • 写完之后在把原来的引用指向新数组setArray(newElements),最后再释放锁,让其他线程进行写操作。

image.png

  • 这样做的好处是在进行读操作的时候不用加锁,保证了并发性,同时这也是读写分离思想的体现。