在Java 集合框架中,我们介绍了List集合中最常用的子类ArrayList和LinkedList,但是它们都不保证多线程安全。如果多个线程同时读取和修改数据,就会产生冲突。
要实现多线程安全,可以使用同步集合Vector以及使用Collections类中synchronizedXXX系列方法。它们都是使用synchronized同步锁,保证同一时间只有一个线程能读取或修改集合。
其实还有一种更加高效的方法,CopyOnWrite(只有在修改时才复制)。
CopyOnWrite方式它的核心思想就是当我们对集合进行读取时,不做任何锁的控制,可以多线程并发读取,但是我们任何对集合进行修改的操作,都要加锁控制,保证同一时间只有一个线程,进行下面三个操作:(注意这里说的)
- 先用老集合copy出一份新集合.
- 然后在新集合上做修改.
- 最后用新集合直接替换老集合.
注意:这里说的集合,就是集合中的数组array对象。
因为我们是在新集合中进行修改,所以不会影响正在读取数据的老集合,然后将新集合替换老集合,就能读取到最新修改的数据。
CopyOnWriteArrayList集合所以这里也就暴露了CopyOnWrite这种方式的缺点,就是我们修改了集合数据,可能不会立即读取到这个新数据,得到的还是老数据。
例如我们一个线程调用了set(int index, E element)替换了集合index位置的数据,然后另一个线程调用get(index),可能得不到那个线程对集合的修改。
重要属性
/** 通过ReentrantLock来实现独占锁的 */ final transient ReentrantLock lock = new ReentrantLock(); /** 用来储存集合中数据,使用volatile关键字修饰,保证可见性和有序性 */ private transient volatile Object[] array;
CopyOnWriteArrayList集合是使用lock来进行加锁操作的。
读取方法
// 不需要加同步锁 public int size() { return getArray().length; } // 不需要加同步锁 @SuppressWarnings("unchecked") private E get(Object[] a, int index) { return (E) a[index]; } // 不需要加同步锁 public E get(int index) { return get(getArray(), index); }
这里选取了几个读取的方法,它们都不没有加同步锁。所以正在修改或者等待修改的东西,是读取不到的,以此会有延时性。
修改方法
不然就会出现这样一种情况:
- 多个线程一起调用getArray方法,得到老数组array。
- 然后其中一个线程操作完成之后,首先调用setArray方法,修改了共享变量array,
- 之后又用另一个线程操作完成调用setArray方法,修改了共享变量array,
- 这个时候你就会发现,另一个线程对集合的修改不是在前一个修改之上的,它会把上个线程的修改覆盖,产生多线程冲突。
public E set(int index, E element) { final ReentrantLock lock = this.lock; // 使用lock锁,保证修改共享变量array的线程安全。 lock.lock(); try { // 获取老数组elements Object[] elements = getArray(); // 得到原来的值 E oldValue = get(elements, index); // 有改变才修改 if (oldValue != element) { int len = elements.length; // 用老的数组创建一个新数组 Object[] newElements = Arrays.copyOf(elements, len); // 更新index位置的值 newElements[index] = element; // 设置新数组 setArray(newElements); } else { // Not quite a no-op; ensures volatile write semantics // 这个并不是无用语句,确保volatile变量的重新写入 setArray(elements); } return oldValue; } finally { lock.unlock(); } }