JDK源码解读-集合-CopyOnWriteArrayList
属性分析
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
-
lock
首先是一个重入锁,CopyOnWriteArrayList是线程安全的,所以分析的时候看看jdk中是如何利用lock完成多线程编程的
-
array
这个是具体存数据的地方,他是用volatile修饰的变量,这么做是为了保证可见性,及A线程更新了数据后B线程能感知到。
为什么有了Vector,还要有CopyOnWriteArrayList,他相比较于Vector有哪些提升
-
迭代的安全性
new Thread(()->vector.clear()).start(); for(String s:vector){ System.out.println(vector.size()); Thread.sleep(1000); }
由于其他线程修改了集合,所以会报ConcurrentModificationException。
public class CopyOnWriteArrayListTest { public static void main(String[] args) throws InterruptedException { CopyOnWriteArrayList<String> cowl = new CopyOnWriteArrayList(); cowl.add("1"); cowl.add("2"); cowl.add("3"); cowl.add("4"); new Thread(()->{ try { Thread.sleep(400); } catch (InterruptedException e) { e.printStackTrace(); } cowl.clear(); }).start(); for(String str: cowl){ System.out.println(cowl.size()+"----"+str); Thread.sleep(1000); } }
即使其他线程clear了,也不影响CopyOnWriteArrayList的迭代。
-
锁粒度的细化,性能的提高
vector都是在add/get方法级别加锁,粒度大性能差。而CopyOnWriteArrayList是在写的时候加锁,读的时候不加锁,所以提高了读性能。
CopyOnWriteArrayList原理是什么
如果有多个调用者同时访问相同的资源,当某个调用者需要修改的时候(写加锁)会复制一个副本给调用者修改,等修改完成后重新将引用指向新的对象,利用volatile,保证修改被其他线程知道,在修改期间不影响其他调用者读取数据(读不加锁)。
public class CopyOnWriteArrayListTest {
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> cowl = new CopyOnWriteArrayList();
cowl.add("1");
cowl.add("2");
cowl.add("3");
cowl.add("4");
new Thread(()->{
try {
Thread.sleep(50);
cowl.set(2,"5");
cowl.set(3,"6");
System.out.println("修改完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
for(String str: cowl){
System.out.println(str);
Thread.sleep(200);
}
for(String str: cowl){
System.out.println("-"+str);
}
}
/**
* 1
* 修改完成
* 2
* 3
* 4
* -1
* -2
* -5
* -6
*/
总结:虽然b线程
插入元素的过程是怎样的,如何保证线程安全的
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();
}
}
CopyOnWriteArrayList缺点是什么
-
内存占用
如果数据修改的太频繁,对象会进行大量的拷贝,浪费内存空间,不太适用写多读少的场景
-
数据一致性
只能保证最终的一致性,实时性较差。比如A线程在迭代元素,线程B这时候修改了集合,由于线程A迭代的是之前的快照版本,所以不会立刻受到影响。
代码验证:
public class CopyOnWriteArrayListTest { public static void main(String[] args) throws InterruptedException { CopyOnWriteArrayList<String> cowl = new CopyOnWriteArrayList(); cowl.add("1"); cowl.add("2"); cowl.add("3"); cowl.add("4"); new Thread(()->{ try { Thread.sleep(50); cowl.set(2,"5"); cowl.set(3,"6"); System.out.println("修改完成"); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); for(String str: cowl){ System.out.println(str); Thread.sleep(200); } for(String str: cowl){ System.out.println("-"+str); } /** * 1 * 修改完成 * 2 * 3 * 4 * -1 * -2 * -5 * -6 */ }
总结:即使其他线程在循环过程中对集合进行了修改,循环的仍然是快照数据。所以也说明了为什么CopyOnWriteArrayList迭代更加安全。
读写锁与CopyOnWriteArrayList对比
读写锁的写锁和读锁互斥,而CopyOnWriteArrayList可以实现写的时候不影响读。当然缺点在于实时性下降。但是如果我要求读性能非常高,一个写操作就使读操作都阻塞是相当不合理的。