文章目录
- 1. 引入
- 2. 源码剖析
- 3. 总结
1. 引入
我们都知道java.util
包下的ArrayList是线程不安全的,如果想要在多线程且存在竞争的场景下使用ArrayList,就需要通过一定的逻辑来保证线程安全。常用于解决ArrayList线程安全问题的方案有:
- Vector:类似于HashMap和Hashtable的关系,通过在可能发生线程安全问题的方法上直接使用synchronized关键字进行保证
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
- synchronizedList:类似于synchronizedMap,方法在执行前需要竞争通过synchronized竞争互斥锁,锁粒度同样较大
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
更好的选择是使用JUC下的CopyOnWriteArrayList,见名知意:复制写的ArrayList。盲猜它通过类似COW机制的思想来保证线程安全,即:
- 多线程读时不会有线程安全问题,不需要加锁,可并发读
- 多线程写时,实际上操作的是ArrayList的副本,因此写请求并不会阻塞读请求
到底是不是这样呢?
2. 源码剖析
对于CopyOnWriteArrayList来说,写操作需要加锁,方法并发写入导致数据丢失。它并不会直接操作原数组,而是先复制一个数组再进行操作,写操作执行结束后在将复制后的数组赋值给原数组。CopyOnWriteArrayList在写操作的同时允许读操作,大大提高了读操作的性能。
下面通过剖析一下集合常用方法的源码实现,来看一下它是如何做到线程安全的。它首先在属性字段定义了ReentrantLock锁对象和被volatile修饰的对象数组:
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
对于get()
来说,由于不存在线程安全问题,所以可以直接返回索引对应的元素:
private E get(Object[] a, int index) {
return (E) a[index];
}
get()
的实现如下:
public boolean add(E e) {
final ReentrantLock lock = this.lock; // 获取锁
lock.lock(); // 加锁
try {
Object[] elements = getArray(); // 获取属性字段定义的array
int len = elements.length; // 获取array的长度
Object[] newElements = Arrays.copyOf(elements, len + 1); // 获取数组的副本
newElements[len] = e; // 在副本上执行添加操作
setArray(newElements); // 将副本复制给原数组
return true;
} finally {
lock.unlock(); // 释放锁
}
}
其中getArray()
和setArray()
的方法实现如下:
final Object[] getArray() {
return array;
}
/**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a;
}
从源码的实现可以看出,add()
的逻辑如下:
- 首先获取ReentrantLock对象,进行加锁保证线程安全
- 获取array的副本,并将新元素添加到数组尾部
- 将副本复制给原数组
- 释放锁
对于在指定的索引处添加新元素的add()
方法逻辑类似,源码实现如下:
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (index > len || index < 0) // 非法索引抛异常
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index;
if (numMoved == 0) // 如果索引位置就是数组尾部,直接获取副本
newElements = Arrays.copyOf(elements, len + 1);
else { // 否则需要进行数组元素的移动
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
newElements[index] = element; // 插入新元素
setArray(newElements);
} finally {
lock.unlock();
}
}
其他涉及到线程安全问题的方法都需要使用ReentrantLock和数组复制来保证线程安全,详细实现可见源码。
3. 总结
总体来说,CopyOnWriteArrayList需要使用ReentrantLock来加锁和释放锁保证线程安全。因此,适合于读多写少的应用场景,不适合内存敏感和对数据实时性要求高的场景。不足之处在于:
- 内存占用:复制的新数组需要占用内存空间
- 数据不一致:读操作不能读取实时的数据,即读操作不能读取到写操作还没有同步到源数组中的数据