前言:
此篇博客着重于:在多线程并发执行读、写操作的场景下,Vector集合、CopyOnWriteArrayList集合是否能保证线程安全?它们是通过什么方式保证线程安全的?
Vector:
(1)add(E e)方法实现:
public synchronized boolean add(E e) {//modCount:修改表结构的次数(增、删、改等操作都算修改了表结构)modCount++;//确保数组容量没有达到上限,达到上限则扩容ensureCapacityHelper(elementCount + 1);//将元素添加到Object数组elementData[elementCount++] = e;//返回指向结果return true;}private void ensureCapacityHelper(int minCapacity) {// overflow-conscious code// 如果动态数组容量达到了上限if (minCapacity - elementData.length > 0)//扩容grow(minCapacity);}private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);if (newCapacity - minCapacity < 0)newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);elementData = Arrays.copyOf(elementData, newCapacity);}
(2)get(int index)方法实现:
public synchronized E get(int index) {//如果索引越界,则直接抛出异常if (index >= elementCount)throw new ArrayIndexOutOfBoundsException(index);//否则通过索引返回对应元素return elementData(index);}E elementData(int index) {//通过索引获取Object数组中的元素//将Object对象强转成泛型E返回return (E) elementData[index];}
总结:
无论是add(E e)方法,还是get(int index)方法,方法声明上都有synchronized关键字,这意味着每次读、写操作都会对当前Vector对象上锁,保证同一时间并发的多个读、写线程是串行执行的,以此来确保多线程并发读、写时的线程安全。
图解:
为什么并发读、写场景下,不上锁会有线程安全问题呢?
以get(int index)(读操作)、remove(int index)(写操作)这两个方法为切入点分析。
get(int index)方法可以分为两步:1、判断索引是否越界;2、返回索引对应的元素。
remove(int index)方法也可以分为两步:1、从数组中移除指定数据;2、更新数组元素数量。
public synchronized E remove(int index) {modCount++;if (index >= elementCount)throw new ArrayIndexOutOfBoundsException(index);E oldValue = elementData(index);int numMoved = elementCount - index - 1;if (numMoved > 0)System.arraycopy(elementData, index+1, elementData, index,numMoved);elementData[--elementCount] = null; // Let gc do its workreturn oldValue;}
此时有一个get线程和一个remove线程同时来操作Vector对象,操作动态数组的最后一个元素,由于没有加锁,任何执行顺序都是有可能的。假设有如下图所示的执行顺序:
我们预期的结果是get线程会报数组越界异常,但结果却是返回了一个null值,与我们想要的结果不符。写线程修改了Vector集合的结构后,我们期望读线程能感知到表结构的改变,所以线程安全问题实质上是数据一致性问题。
CopyOnWriteArrayList:
我们分析了Vector集合的add、get方法,知道了Vector集合多线程并发场景下,保证线程安全的原理:读、写操作都会对当前Vector集合对象上synchronized锁。但其实多线程并发执行读操作的场景是不会有线程安全问题的,这时候我们就希望有一个集合类,它能将读、写操作分离,只让写线程串行执行,而读线程可以并行执行,这个集合类就是:CopyOnWriteArrayList。
如何实现读、写分离?
让我们来看看add(E e)方法实现:
public boolean add(E e) {final ReentrantLock lock = this.lock;//获取锁对象lock.lock();try {//获取存储元素的Object数组Object[] elements = getArray();//获取Object数组长度int len = elements.length;//拷贝旧Object数组得到一个新的Object数组//新数组长度比旧数组长度多1Object[] newElements = Arrays.copyOf(elements, len + 1);//将元素插入新的Object数组newElements[len] = e;//使用新数组覆盖旧数组setArray(newElements);//返回return true;} finally {//finally块释放锁,避免死锁lock.unlock();}}
get(int index)方法实现:
public E get(int index) {return get(getArray(), index);}private E get(Object[] a, int index) {return (E) a[index];}//获取当前Object数组
final Object[] getArray() {//array:成员变量,是集合类底层存储元素的Object数组return array;}
总结:
1、多个写线程并发访问CopyOnWriteArrayList集合对象时,只有一个写线程能获取到ReentrantLock锁,所有写线程串行执行。
2、add插入逻辑:获取旧数组及旧数组长度;基于旧数组拷贝出一个新数组,新数组长度为旧数组长度+1;将元素插入到新数组中,并用新数组覆盖掉旧数组。
3、get方法逻辑:无需上锁,使用getArray()方法获取当前的Object数组,并通过索引查找对应元素即可。
基于CopyOnWriteArrayList的特点我们不难发现,这种集合对象只适用于读多写少的场景,如果写线程远多于读线程,写线程串行执行的同时还要执行耗时的拷贝操作,性能较低。