前言
今天,我们接着上一篇文章继续聊ArrayList。我们今天的研究重点将放在ArrayList的线程安全实现类上,具体包括:CopyOnWriteArrayList、Vector和Collections.synchronizedList。
关于三者的大致关系,在上一篇文章中,我们已经做了大致梗概的描述,现在我们来重新梳理一下。首先Vector类为与ArrayList类属于同一个包,并且同时继承于java.util.AbstractList,Vector类在实现上也与ArrayList类基本一致,只是在需要同步的操作上进行了线程安全的同步。另一方面,为了进行读的优化,Java在java.util.concurrent包中提供了CopyOnWriteArrayList类,该类的最大特点为写时复制。最后,在Java的设计之初,就提供了Collections.synchronzedList的实现,来对ArrayList提供同步实现。
通过上面的分析,我们大致明白了这三个类的大体关系,我们在接下来的文章中,将详细分析各个类的重要特性和具体代码的实现逻辑。
CopyOnWriteArrayList类的特性分析
CopyOnWriteArrayList类的最大特性就是基于写时复制的无阻塞式读操作。简单来说,CopyOnWriteArrayList类在提供读操作时是非阻塞的,在写操作上,需要进行加锁,因此在读多写少的场景中,该类可以具有非常好的性能。接下来,我们将详细分析CopyOnWriteArrayList的写时复制特性。
首先来描述一下,什么是写时复制,具体的操作即是,在进行写操作、重置操作和删除操作时,首先使用锁进行同步限制,在同步语句块中,进行原数组的拷贝,然后在拷贝上进行具体的相关操作(写、删除和修改),完成后将原数组的引用指向该拷贝数组,完成操作并退出同步语句。因此,在读操作时,可以透明的使用同一个数组引用,而不用关心当前是否有写操作正在进行。通过分析,我们大致可以了解得到,这样的写时复制机制,可以保证任何时候的读操作都是非阻塞的,但是缺点也非常明显,会出现脏读的问题。但是CopyOnWriteArrayList可以保证数据的最终一致性。
我们通过下图来对写时复制做一个更形象的理解:
![](https://i-blog.csdnimg.cn/blog_migrate/7bfb5a4e95092bb1421482765bd40634.png)
![](https://i-blog.csdnimg.cn/blog_migrate/bb728c7d2480b9687da8aa2ebd3d527f.png)
通过上面的分析可知,基于写时复制的CopyOnWriteArrayList类可以获得很好的读性能,但是由于写时复制过程中需要对数组进行复制操作,因此,如果数组非常大或者写操作非常频繁,该类的性能会是非常差的,因此需要根据具体的使用场景,决定是否使用该类。
CopyOnWriteArrayList类核心函数分析
首先我们来看两个该类的核心成员变量:
//全局写操作的互斥锁
final transient ReentrantLock lock = new ReentrantLock();
//用于元素存储的数组
private transient volatile Object[] array;
可以看到,使用final和transient修饰的可重入锁lock,该锁在全局范围内,控制对数组array的写操作。fianl的修饰保证了该变量的访问是线程安全的,transient的修饰,保证了该变量的安全性,也表明该变量是不可序列化的。同时数组array是通过volatile进行修饰的,保证了每次读取该变量时,都是从主存中读取的。
1)set函数
public E set(int index, E element) {
//首先获取全局可重入锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获得当前数组的引用
Object[] elements = getArray();
//获得老数组中对应索引位置的值
E oldValue = get(elements, index);
//若新值和旧值不相等,则进行基于写时复制的写操作
if (oldValue != element) {
int len = elements.length;
//通过数组工具类Arrays.copyOf进行数组的复制
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
//保证volatile的写语意,重置elements函数的引用
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
如果仔细研读了上文中对写时复制的描述,那么该set函数的逻辑就非常简单了。其中需要注意的是,set操作并非直接替换索引位置的值,而是做了一次值比较,值不同时进行替换操作,值相等时,为确保volatile语意,还是调用了setArray函数,进行引用的替换(其实引用并没有发生变化)。
此处说一下我的理解,可能存在偏差,若有不正之处望各位指出。此处在值相等时,为了确保的set函数的语义完整执行,才依旧调用了setArray函数对elements进行赋值。因为全局锁只会保护该函数的互斥执行,但是针对setArray函数,并不会互斥执行,也就是说别处的其他函数依旧会调用该函数来修改array的引用,造成set函数在值相等的情况下,数组引用依旧发生了变化,因此为保证set函数能够在值不变的时候,数组的引用也不发生变化,所以才最后调用了setArray函数,确保array引用没有发生变化。
本函数中,还有一个不确定的点就是对于共享变量array的引用了,为什么不直接去使用array引用,而是定义了一个getArray的函数,具体代码如下:
final Object[] getArray() {
return array;
}
望CSDN大神能指出此处设计之原由,小弟不胜感激~
2)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();
}
}
若仔细研读了set函数,该add函数则更为简单,同样也是写时复制的代码逻辑。具体的代码逻辑,在如上注释所示,就不做过多分析了。
CopyOnWriteArrayList类总结
关于CopyOnWriteArrayList类的分析就到此为止,我们在源码分析上,也主要强调了其写时复制的特性,而未完全的从代码框架逻辑上对其分析。具体代码框架逻辑,请参考本人的ArrayList源码分析部分的分析。两者之间,在数组元素的插入、删除、修改和查找上近乎是一样的,因此在本文中不做过多的赘述了。
关于此类,最令人激动的设计也就是写时复制了。通过写时复制的机制,可以实现数组的无阻塞式读操作,这种特性也常常应用在数据库系统的设计中,例如MySQL的InnoDB存储存储引擎提供的MVCC机制,该机制也是通过多版本并发控制,来保证非阻塞的读操作。关于写时复制机制,在上文中,已经分析的很清楚了,望各位看官能仔细研读透彻,并结合代码,了解该机制是如何实现的。
一句话总结一下:CopyOnWriteArrayList应用于读多写少的使用场景,在读上具有非常好的性能,在写上会花费一定的代价。当数据量较大或者写操作频繁时,都不建议使用该类。
Vector类的特性分析
关于Vector类的介绍,我们也将着重放在其如何实现线程安全上,至于该类对于数组操作的支持,与ArrayList基本一致,因为两者实现了同样的抽象类和相关接口,因此两者实现的函数都基本是一致的,只是Vector做了同步操作而已。
该类已经存在非常久远了,在JDK1.0中,就已经使用该类来作为ArrayList线程安全的支持版本,因此关于其特性分析也是与ArrayList的使用是一致的,所以我们就直接来看其如何实现线程安全的吧。
Vector类核心函数分析
//元素添加
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
//元素删除
public synchronized boolean removeElement(Object obj) {
modCount++;
int i = indexOf(obj);
if (i >= 0) {
removeElementAt(i);
return true;
}
return false;
}
//元素修改
public synchronized void setElementAt(E obj, int index) {
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " +
elementCount);
}
elementData[index] = obj;
}
//元素大小
public synchronized int size() {
return elementCount;
}
如上所示,Vector实现线程安全的方法就已经非常明显了,即将各个需要同步的函数使用synchronized进行修饰。其中Vector中还有部分没有使用synchronized修饰的函数,是因为这些函数是同步函数的辅助函数,且调用也只会存在同一个函数中,不存在多处调用,则不需要进行重复的同步了。
Vector类总结
综上所述,Vector类的线程安全就是通过synchronized修饰同步函数来实现的。所以可以了解到,该类的性能其实是非常差的,一旦涉及到需要同步的地方,都是同步的执行。最终造成该类相比于ArrayList来说,性能差了很多。因此,很多地方已经不再支持使用该同步类了。
Collections.SynchronizedList分析
该实现为Collections工具类中的静态内部类,相比于Vector对函数直接使用synchronized做同步,该工具类提供的同步方案是使用同一个变量mutex做锁,由synchronized基于该锁实现同步代码块。
除此之外就没有其他特别的地方了,如果各位读者有兴趣可以详细的去了解一下该类的实现,我们此处只简单贴出一段该内部类的代码,给各位做了解。
static class SynchronizedList<E>
extends SynchronizedCollection<E>
implements List<E> {
private static final long serialVersionUID = -7754090372962971524L;
final List<E> list;
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
public boolean equals(Object o) {
if (this == o)
return true;
synchronized (mutex) {return list.equals(o);}
}
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);}
}
....
}
后言
关于ArrayList类的线程安全支持,在上文中我们已经做了详细的分析,除了使用CopyOnWriteArrayList基于写时复制的方式能够提高读性能以外,其他两种线程安全方案都是基于简单的同步来实现的,即通过synchronized修饰函数和synchronized修饰程序代码块。
关于这一部分的分析,我们最大的收获,就是写时复制机制了。所谓写时复制,本质也是采用读写分离的方案,来解决读写冲突,这种方案在数据库中也使用的非常多,最著名的就是数据库的读写分离,来减轻数据库的访问压力,该方案所存在的问题也较为明显,存在一定时延范围内的脏读,但是可以确保数据的最终一致性。这又提出了关于CAP问题的权衡,这些更加深入的设计与架构问题,其实早早的在Java类的设计中就已初见端倪。Java类的设计也不断的借鉴和使用其他更加高级的架构和设计方案,同时自身也提出了非常多的令人振奋的设计手法,因此在Java的源码分析中,可以不断的收获惊喜,提高架构和设计的能力,为今后从事更高层次的工作提供基础。
因此,希望更多的朋友能够加入到Java源码分析的行列中,支持本系列博客,同时有任何错误的地方,请望指出,谢谢各位的支持~