? ? ? ? 此篇博客着重于:在多线程并发执行读、写操作的场景下,Vector集合、CopyOnWriteArrayList集合是否能保证线程安全?它们是通过什么方式保证线程安全的?
? ? ? ? (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 code
int 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 work
return oldValue;
}
????????此时有一个get线程和一个remove线程同时来操作Vector对象,操作动态数组的最后一个元素,由于没有加锁,任何执行顺序都是有可能的。假设有如下图所示的执行顺序:
? ? ? ? 我们预期的结果是get线程会报数组越界异常,但结果却是返回了一个null值,与我们想要的结果不符。写线程修改了Vector集合的结构后,我们期望读线程能感知到表结构的改变,所以线程安全问题实质上是数据一致性问题。
? ? ? ? 我们分析了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数组
//新数组长度比旧数组长度多1
Object[] 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的特点我们不难发现,这种集合对象只适用于读多写少的场景,如果写线程远多于读线程,写线程串行执行的同时还要执行耗时的拷贝操作,性能较低。