前言
之前的文章已经说过了java开发中,保存集合数据是用ArrayList还是LinkedList,了解下:Java编程中我该用ArrayList还是LinkedList?
平常面试过程中问的最多的就是 HashMap,当问到可以保证线程安全的并发容器时,最熟悉的就是ConcurrentHashMap,也了解下:我已经学了HashMap,怎么还要会用ConcurrentHashMap
但是如果问 “有没有类似ArrayList,又保证线程安全的容器类”,首先我们都知道ArrayList是线程不安全的,所以很多人估计会说 Vector,没错,这个就好像HashTable,每个方法都用了Synchronized去声明,保证线程安全。也可以通过 Collections 方法来保证线程安全。
Collections.synchronizedList(new ArrayList())
我们都知道Vector是一个很老的技术了,性能很差,扩容也有问题。
但是面试官更多的是想听到 CopyOnWriteArrayList,这个类如ConcurrentHashMap样,都在java的JUC包中,属于并发容器类。
CopyOnWriteArrayList
啥叫CopyOnWrite?
CopyOnWrite简称COW,看意思就晓得是写时复制,这是一种计算机程序设计领域的优化策略。简单来说就是多个线程同时调用相同的资源,某个线程试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该线程,而其他线程获取的资源仍然保持不变。也是一种延时懒惰策略。
这种系统机制在很多的场景中有用到,可以去了解下,linux下copy-on-write机制,mysql B-tree索引数据结构就是采用了copy-on-write,还有Kafka。。。。等等。
这里呢我们再从程序的角度再来解释下CopyOnWrite机制。
我们往集合中添加元素的时候,先copy一份原有数据数组,在copy数组中进行写操作,完成写操作后利用这个copy数组复制给成员变量的数组(array)。这么做的目的就是,多线程竞争来读的时候不需要加锁,因为多个线程写时不时直接操作成员变量的数组(array),这里呢,相信大家知道这是一种读写分离的思想。如图:
看下CopyOnWriteArrayList的源码就更明白了:
//成员变量数组private transient volatile Object[] array;//添加元素代码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; //将新的数组指向原来的引用 (array) setArray(newElements); return true; } finally { lock.unlock(); } }//获取元素代码public E get(int index) { return get(getArray(), index);}
是不是一眼明了。CopyOnWriteArrayList的增删改都需要获得锁,而读操作不需要获得锁,提高并发。如果读写锁互斥的话,会导致写锁阻塞大量读操作,影响并发性能。
看完这个代码,有的人会问,ReentrantLock也是一种悲观锁啊,那岂不是和synchronized声明的方法一样了?!!!他们两个都是独占锁!!下面说一下为啥用 ReentrantLock。
相比synchronized,ReentrantLock是一套互斥锁,底层使用了CAS,有着其他的高级特性(我前面的文章也提到过java中各种锁):
1、synchronized是一种非公平锁,而ReentrantLock可以声明为公平锁,也就是先等待的线程先获得锁,不会造成线程饥饿现象。
2、等待可中断,持有锁的线程长期不释放的时候,等待的线程可放弃,从一定程度上防止死锁。
3、ReenTrantLock控制线程非常的灵活,通过Condition来实现分组唤醒需要唤醒的线程们。
从锁粒度分析的话,ReenTrantLock优于Synchronized。
问题又来了,成员变量数组array为啥用了volatile和transient去修饰。
如果一个线程修改完数据并赋值给成员变量数组array了,读线程要怎么晓得这个变化。这个volatile就保证了 成员变量数组array在多线程中的可见性。但是不能绝对的保证一致性。
至于transient的作用,我就不清楚了,只晓得对象序列化的时候,标记为transient的变量不会被序列化。
总的来说,CopyOnWriteArrayList适合读多写少,不要求数据强一致性的场景。