今天来复习一下集合。在支持并发的集合中,我觉得CopyOnWriteArrayList是相对容易理解的一个。
CopyOnWrite:写时复制,就是当有线程向集合添加元素时,不是直接往旧的容器中添加元素,而是将旧的容器中的元素复制到新的容器中,读的时候读的仍然是旧容器,这样就不会影响并发读了
分析一下CopyOnWriteArrayList部分源码
1.add(E e)方法,向容器中添加元素
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();
}
}
在往容器中加元素的过程是加锁的,加锁是通过可重入锁ReentrantLock实现的,如果不加锁的话,多个线程同时添加元素会复制多次。
getArray()获得旧容器中的元素
final Object[] getArray() {
return array;
}
Arrays.copyOf(elements, len + 1)进行数组的复制,并返回复制以后新的数组
newElements[len] = e向新的集合中添加元素
setArray(newElements)将旧容器的引用指向新容器
final void setArray(Object[] a) {
array = a;
}
其余向容器中添加元素的方法,比如public void add(int index, E element)
实现思路和add(E e)大抵相同
2.get(int index),从容器中获得指定位置的元素
public E get(int index) {
return get(getArray(), index);
}
实际调用的是get(Object[] a, int index)方法
private E get(Object[] a, int index) {
return (E) a[index];
}
得到指定位置的元素就是获得数组中指定位置的元素
从代码中可以看到,对于从容器中读操作是不进行加锁的
3.remove(int index),容器中移除元素
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获得原来旧的容器
Object[] elements = getArray();
int len = elements.length;
//获得指定位置上的元素
E oldValue = get(elements, index);
//移动的距离
int numMoved = len - index - 1;
//如果集合中只有一个元素
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
//将原来的旧容器的引用指向新的引用
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
实现的基本思想和add(E e)相同,需要进行加锁,并且会对旧的容器进行复制
4.看一个CopyOnWriteArrayList的构造函数
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
一般我们自己写项目的时候都会选择使用这个构造函数。这个构造函数并没有初始化集合的大小,集合大小为0,但是它没有像ArrayList一样有扩容操作,因为在往这个容器中进行写操作时,实际上并不是往当前容器添加元素,而是会创建出一个比当前容器大1的容器,在对往这个容器中添加元素。所以不需要进行扩容。或者可以这么理解,它在每次添加元素的操作时都进行了一次扩容,每次扩容一个元素的大小
5.CopyOnWriteArrayList的缺点:
最明显的一个致命缺点就是占大量的内存,在往容器中删除元素和添加元素的时候都会在创建一个新的数组,如果垃圾收集器回收不及时的话,并且有很多线程进行写操作,可能会撑爆内存吧。
在一篇博客上看到CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。不是特别理解这一点,volatile Object[] array,array是有volatile修饰的,其保证了内存的可见性,当一个线程对一个共享变量的写操作时,写完立刻就会对其他线程立即可见,那只要写完,其他线程就能读到新添加的值。自己写了个demo进行测试,感觉延迟效果不是很明显
测试类:TestCopyOnWriteArrayList.java
public class TestCopyOnWriteArrayList {
private static CopyOnWriteArrayList<String> c = new CopyOnWriteArrayList<>();
private static long startTime;
private static long endTime;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
c.add("a");
startTime = System.currentTimeMillis(); //获得添加a以后的时间
System.out.println("添加了a");
//添加了a以后让其睡眠,让其他线程有时间执行
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
c.add("b");
System.out.println("添加了b");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
c.add("c");
System.out.println("添加了c");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
c.add("d");
System.out.println("添加了d");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// System.out.println("读取第一个元素");
String s = c.get(0);
endTime = System.currentTimeMillis();
System.out.println("读取到a花费时间:" + (endTime - startTime) + "毫秒");
System.out.println("s: " + s);
}
}).start();
}
}
运行结果:
添加了a
读取到a花费时间:1毫秒
s: a
添加了b
添加了c
添加了d
1毫秒的延迟也不是特别长吧
不知道是不是自己的例子不正确
6.CopyOnWriteArrayList的应用场景:读多写少的场景