初识CopyOnWriteArrayListjava
第一次见到CopyOnWriteArrayList,是在研究JDBC的时候,每个数据库的Driver都是维护在一个CopyOnWriteArrayList中的,为了证实这一点,贴两段代码,第一段在com.mysql.jdbc.Driver下,也就是咱们写Class.forName("...")中的内容:mysql
public class Driver extends NonRegisteringDriver implements java.sql.Driver { public Driver() throws SQLException { } static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } }
看到com.mysql.jdbc.Driver调用了DriverManager的registerDriver方法,这个类在java.sql.DriverManager下:sql
public class DriverManager { private static final CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList(); private static volatile int loginTimeout = 0; private static volatile PrintWriter logWriter = null; private static volatile PrintStream logStream = null; private static final Object logSync = new Object(); static final SQLPermission SET_LOG_PERMISSION = new SQLPermission("setLog"); ... }
看到全部的DriverInfo都在CopyOnWriteArrayList中。既然看到了CopyOnWriteArrayList,我天然免不了要研究一番为何JDK使用的是这个List。数据库
首先提两点:数组
一、CopyOnWriteArrayList位于java.util.concurrent包下,可想而知,这个类是为并发而设计的缓存
二、CopyOnWriteArrayList,顾名思义,Write的时候老是要Copy,也就是说对于CopyOnWriteArrayList,任何可变的操做(add、set、remove等等)都是伴随复制这个动做的,后面会解读CopyOnWriteArrayList的底层实现机制安全
四个关注点在CopyOnWriteArrayList上的答案并发
关 注 点 | 结 论 |
CopyOnWriteArrayList是否容许空 | 容许 |
CopyOnWriteArrayList是否容许重复数据 | 容许 |
CopyOnWriteArrayList是否有序 | 有序 |
CopyOnWriteArrayList是否线程安全 | 线程安全 |
如何向CopyOnWriteArrayList中添加元素dom
对于CopyOnWriteArrayList来讲,增长、删除、修改、插入的原理都是同样的,因此用增长元素来分析一下CopyOnWriteArrayList的底层实现机制就能够了。先看一段代码:分布式
public static void main(String[] args){ List<Integer> list = new CopyOnWriteArrayList<Integer>(); list.add(1); list.add(2); }
看一下这段代码作了什么,先是第3行的实例化一个新的CopyOnWriteArrayList:
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8673264195747942595L; /** The lock protecting all mutators */ transient final ReentrantLock lock = new ReentrantLock(); /** The array, accessed only via getArray/setArray. */ private volatile transient Object[] array; ... }
public CopyOnWriteArrayList() { setArray(new Object[0]); }
final void setArray(Object[] a) { array = a; }
看到,对于CopyOnWriteArrayList来讲,底层就是一个Object[] array,而后实例化一个CopyOnWriteArrayList,用图来表示很是简单:
就是这样,Object array指向一个数组大小为0的数组。接着看一下,第4行的add一个整数1作了什么,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(); } }
画一张图表示一下:
每一步都清楚地表示在图上了,一次add大体经历了几个步骤:
一、加锁
二、拿到原数组,获得新数组的大小(原数组大小+1),实例化出一个新的数组来
三、把原数组的元素复制到新数组中去
四、新数组最后一个位置设置为待添加的元素(由于新数组的大小是按照原数组大小+1来的)
五、把Object array引用指向新数组
六、解锁
整个过程看起来比较像ArrayList的扩容。有了这个基础,咱们再来看一下第5行的add了一个整数2作了什么,这应该很是简单了,仍是画一张图来表示:
和前面差很少,就不解释了。
另外,插入、删除、修改操做也都是同样,每一次的操做都是以对Object[] array进行一次复制为基础的,若是上面的流程看懂了,那么研究插入、删除、修改的源代码应该不难。
普通List的缺陷
经常使用的List有ArrayList、LinkedList、Vector,其中前两个是线程非安全的,最后一个是线程安全的。我有一种场景,两个线程操做了同一个List,分别对同一个List进行迭代和删除,就如同下面的代码:
public static class T1 extends Thread { private List<Integer> list; public T1(List<Integer> list) { this.list = list; } public void run() { for (Integer i : list) { } } } public static class T2 extends Thread { private List<Integer> list; public T2(List<Integer> list) { this.list = list; } public void run() { for (int i = 0; i < list.size(); i++) { list.remove(i); } } }
首先我在这两个线程中放入ArrayList并启动这两个线程:
public static void main(String[] args) { List<Integer> list = new ArrayList<Integer>(); for (int i = 0; i < 10000; i++) { list.add(i); } T1 t1 = new T1(list); T2 t2 = new T2(list); t1.start(); t2.start(); }
运行结果为:
Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372) at java.util.AbstractList$Itr.next(AbstractList.java:343) at com.xrq.test60.TestMain$T1.run(TestMain.java:19)
把ArrayList换成LinkedList,main函数的代码就不贴了,运行结果为:
Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.LinkedList$ListItr.checkForComodification(LinkedList.java:761) at java.util.LinkedList$ListItr.next(LinkedList.java:696) at com.xrq.test60.TestMain$T1.run(TestMain.java:19)
可能有人以为,这两个线程都是线程非安全的类,因此不行。其实这个问题和线程安不安全没有关系,换成Vector看一下运行结果:
Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372) at java.util.AbstractList$Itr.next(AbstractList.java:343) at com.xrq.test60.TestMain$T1.run(TestMain.java:19)
Vector虽然是线程安全的,可是只是一种相对的线程安全而不是绝对的线程安全,它只可以保证增、删、改、查的单个操做必定是原子的,不会被打断,可是若是组合起来用,并不能保证线程安全性。好比就像上面的线程1在遍历一个Vector中的元素、线程2在删除一个Vector中的元素同样,势必产生并发修改异常,也就是fail-fast。
CopyOnWriteArrayList的做用
把上面的代码修改一下,用CopyOnWriteArrayList:
public static void main(String[] args) { List<Integer> list = new CopyOnWriteArrayList<Integer>(); for (int i = 0; i < 10; i++) { list.add(i); } T1 t1 = new T1(list); T2 t2 = new T2(list); t1.start(); t2.start(); }
能够运行一下这段代码,是没有任何问题的。
看到我把元素数量改小了一点,由于咱们从上面的分析中应该能够看出,CopyOnWriteArrayList的缺点,就是修改代价十分昂贵,每次修改都伴随着一次的数组复制;但同时优势也十分明显,就是在并发下不会产生任何的线程安全问题,也就是绝对的线程安全,这也是为何咱们要使用CopyOnWriteArrayList的缘由。
另外,有两点必须讲一下。我认为CopyOnWriteArrayList这个并发组件,其实反映的是两个十分重要的分布式理念:
(1)读写分离
咱们读取CopyOnWriteArrayList的时候读取的是CopyOnWriteArrayList中的Object[] array,可是修改的时候,操做的是一个新的Object[] array,读和写操做的不是同一个对象,这就是读写分离。这种技术数据库用的很是多,在高并发下为了缓解数据库的压力,即便作了缓存也要对数据库作读写分离,读的时候使用读库,写的时候使用写库,而后读库、写库之间进行必定的同步,这样就避免同一个库上读、写的IO操做太多
(2)最终一致
对CopyOnWriteArrayList来讲,线程1读取集合里面的数据,未必是最新的数据。由于线程二、线程三、线程4四个线程都修改了CopyOnWriteArrayList里面的数据,可是线程1拿到的仍是最老的那个Object[] array,新添加进去的数据并无,因此线程1读取的内容未必准确。不过这些数据虽然对于线程1是不一致的,可是对于以后的线程必定是一致的,它们拿到的Object[] array必定是三个线程都操做完毕以后的Object array[],这就是最终一致。最终一致对于分布式系统也很是重要,它经过容忍必定时间的数据不一致,提高整个分布式系统的可用性与分区容错性。固然,最终一致并非任何场景都适用的,像火车站售票这种系统用户对于数据的实时性要求很是很是高,就必须作成强一致性的。
最后总结一点,随着CopyOnWriteArrayList中元素的增长,CopyOnWriteArrayList的修改代价将愈来愈昂贵,所以,CopyOnWriteArrayList适用于读操做远多于修改操做的并发场景中。