1. 并发容器的概述
对于ArrayList来说是线程不安全的,为了解决这个问题,JDK为我们提供了线程安全的并发容器
① Vector
过时的同步容器
Vector
是早期为了解决ArrayList
的线程不安全而实现的,它的实现方法很简单,在ArrayList
的方法上都加上了Synchronized
既然放在方法上锁住的是整个实例对象,可想而知这样的效率很低
② Collections.synchronizedList()
List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
Collections.synchronizedList()
为ArrayList加上了一层包装使其变成了线程安全的,我们来看看他的源码
* @param <T> the class of the objects in the list
* @param list the list to be "wrapped" in a synchronized list.
* @return a synchronized view of the specified list.
*/
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
可以看到它是根据传入的参数是否实现了RandomAccess
接口而决定返回SynchronizedRandomAccessList
还是SynchronizedList
;我们的ArrayList是实现了这个接口的,因为底层是数组,能被随机访问;自然返回的是前缀者,我们来看SynchronizedRandomAccessList
/**
* @serial include
*/
static class SynchronizedRandomAccessList<E>
extends SynchronizedList<E>
implements RandomAccess {
SynchronizedRandomAccessList(List<E> list) {
super(list);
}
.......
}
可以看到他继承了SynchronizedList
,我们进到里面看看,他是使用的同步代码块的方法实现同步,可以发现它的效率和vector
差不多
③ CopyOnWriteArrayList
用来代替Vector
和synchronizedList
Vector
和synchronizedList
的锁的粒度太大,并发效率低,而且在迭代的时候是无法编辑的(fail-fast)
适用于读操作尽可能,写操作慢一点没关系,也就是读多写少的操作
2. CopyOnWriteArrayList
就像上面说的CopyOnWriteArrayList
用来代替Vector
和synchronizedList
,主要解决下面的问题
Vector
和synchronizedList
的锁的粒度太大,并发效率低- 而且在迭代的时候是无法编辑的(fail-fast)
在说 CopyOnWriteArrayList
之前先回顾一下读写锁,对于读写锁读读共享,其他的都互斥的
而对于CopyOnWriteArrayList
对于读写锁更升了一级,读操作完全不加锁,写入也不会阻塞读操作,只要写入和写入之间才会需要同步等待
我们先来看一下它的迭代时的编辑
对于ArrayList
来说,如果在迭代的过程中对集合修改,会发生fail-fast
public class CollectionDemo {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(list);
Integer next = iterator.next();
System.out.println(next);
if(next == 2){
list.remove(3);
}
}
}
}
/*[1, 2, 3, 4]
1
[1, 2, 3, 4]
2
[1, 2, 3]
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1042)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:996)
at com.minifull.day_03.CollectionDemo.main(CollectionDemo.java:17)*/
但是对于CopyOnWriteArrayList
public class CollectionDemo {
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(list);
Integer next = iterator.next();
System.out.println(next);
if(next == 2){
list.remove(3);
}
}
}
}
/*
[1, 2, 3, 4]
1
[1, 2, 3, 4]
2
[1, 2, 3]
3
[1, 2, 3]
4*/
可以看到没有报错,而且就算我们删除了4,在之后还是打印了出来4,也就是说对于我们的改变好像一定感知都没有,拿着的一直都是之前没有改变过的那份内容
思考一下他的设计理念
CopyOnWrite
的意思就是我对计算机中的某个东西进行修改我不是直接对原内存进行修改,而是把他拷贝一份放到我自己的内存,在我自己的内存进行修改,修改完再让指向原内存的指针指向我修改完的内存;
对于CopyOnWriteArrayList
也是在写的时候copy一份新的出来再新的中进行修改,完成后改变指针的地址,这样就能保证读一直不被阻塞,效率很高;
我们再回想一下它的适用场所:适用于读操作尽可能,写操作慢一点没关系,也就是读多写少的操作,这样想就很明了
一句话概括:创建新副本,读写分离
所以可以在迭代的时候进行更新,但是会出现数据的过期
想想缺点
① 数据一致性问题:
CopyOnWrite
容器只能保证数据的最终一致性,不能保证数据的实时一致性,所以如果你希望写入的数据能被马上读到,不要使用CopyOnWrite
容器
② 内存占用问题:
因为CopyOnWrite
的写时复制机制,所以在进行写操作的时候,内存中会驻扎两个对象的内存
3. 看看源码
成员属性
首先他说一个List
,底层使用数组实现的,为了实现同步,内部使用的是ReentrantLock
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
.....
add
像我们上文说的,CopyOnWrite的思想就是创建新副本,读写分离,我们来看它的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();//接收
}
}
get
public E get(int index) {
return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
看源码get方法的确不会发生阻塞