源码-集合集锦-CopyOnWriteArrayList从源码到面试
CopyOnWriteArrayList
感谢大佬的图解分析CopyOnWriteArrayList,让渺小的我受益匪浅
前言
- CopyOnWriteArrayList位于java.util.concurrent包下,可想而知,这个类是为并发而设计的
- CopyOnWriteArrayList, 顾名思义,CopyOnWriteArrayList是一个写时复制,也就是说,对于CopyOnWriteArrayList,任何可变的操作(add、set、remove等等)都是伴随着写时复制的操作
四个关注点
关注点 | 结论 |
---|---|
CopyOnWriteArrayList是否允许空 | 允许 |
CopyOnWriteArrayList是否允许重复 | 允许 |
CopyOnWriteArrayList是否有序 | 有序 |
CopyOnWriteArrayList是否线程安全 | 线程安全 |
CopyOnWriteArrayList 如何实现写时复制
写时复制的体现主要体现在add方法中
字段声明:
// 声明一个全局锁,transient作用:属性在保存对象时不被存储
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
// 使用volatile修饰array数据, 保证了线程之间的可见性,但是不保证原子性,禁止指令重排
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
// 提供array的get、set方法
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
下面来看看这段代码:
public static void main(String[] args) {
List<Integer> list = new CopyOnWriteArrayList<Integer>();
list.add(1);
list.add(2);
}
构造方法:
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
final void setArray(Object[] a) {
array = a;
}
可以看到构造方法:底层只是创建了一个长度为0的Object数组,然后实例化一个CopyOnWriteArrayList,用图来表示非常简单:
add方法:
public boolean add(E e) {
// 1.获取到全局锁
final ReentrantLock lock = this.lock;
// 2.加锁
lock.lock();
try {
// 3. 获取到原来的数组
Object[] elements = getArray();
// 长度
int len = elements.length;
// 4.数组复制,并且扩容一个位置
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 5.将添加的元素加到新扩容的位置上
newElements[len] = e;
// 6.把Object array引用指向新数组
setArray(newElements);
return true;
} finally {
// 7. 解锁
lock.unlock();
}
}
画图表示add过程:
每一步都清楚地表示在图上了,一次add大致经历了几个步骤:
1、加锁
2、拿到原数组,得到新数组的大小(原数组大小+1),实例化出一个新的数组来
3、把原数组的元素复制到新数组中去
4、新数组最后一个位置设置为待添加的元素(因为新数组的大小是按照原数组大小+1来的)
5、把Object array引用指向新数组
6、解锁
整个过程看起来比较像ArrayList的扩容。有了这个基础,我们再来看一下add了一个整数2做了什么,这应该非常简单了,还是画一张图来表示:
添加过程还是上面的步骤
CopyOnWriteArrayList 透露的思想
(1) 读写分离
我们在读取CopyOnWriteArrayList的时候读取的是CopyOnWriteArrayList中的Object[] array数组,但是我们在写的时候,操作的却是复制后的一个新Object[] array数组,也就是说,在读和写的时候操作的不是同一个Object[] array数组,这就是读写分离。这种技术数据库用的非常多,在高并发下为了缓解数据库的压力,即使做了缓存也要对数据库做读写分离,读的时候使用读库,写的时候使用写库,然后读库、写库之间进行一定的同步,这样就避免同一个库上读、写的IO操作太多
(2) 最终一致性
假设有四个线程,线程1 读取CopyOnwriteArrayList中的数据,线程2,3,4分别对CopyOnwriteArrayList做出修改。对CopyOnWriteArrayList来说,线程1读取集合里面的数据,未必是最新的数据。因为线程2、线程3、线程4四个线程都修改了CopyOnWriteArrayList里面的数据,但是线程1拿到的还是最老的那个Object[] array,新添加进去的数据并没有,所以线程1读取的内容未必准确。不过这些数据虽然对于线程1是不一致的,但是对于之后的线程一定是一致的,它们拿到的Object[] array一定是三个线程都操作完毕之后的Object array[],这就是最终一致。最终一致对于分布式系统也非常重要,它通过容忍一定时间的数据不一致,提升整个分布式系统的可用性与分区容错性。当然,最终一致并不是任何场景都适用的,像火车站售票这种系统用户对于数据的实时性要求非常非常高,就必须做成强一致性的。
最后总结一点,随着CopyOnWriteArrayList中元素的增加,CopyOnWriteArrayList的修改代价将越来越昂贵,因此,CopyOnWriteArrayList适用于读操作远多于修改操作的并发场景中。
面试题
我们为什么使用CopyOnWriteArrayList
在日常开发中,难免会出现多线程场景,只是在单线程的场景中使用ArrayList、Linked等集合就可以解决问题,但是在多线程场景中,当一个线程对集合使用 迭代器遍历,另一个线程在此时又对集合的元素结构进行修改,此时就会比较集合中modCount属性,如果
modCount属性值和预期的不一样,那么就会抛出java.util.ConcurrentModificationException, 相信大家对这个异常并不陌生吧,那么在多线程环境下,为了保证线程安全, 我们有以下几种方案:
- 使用Collections.SynchronizedList来构造一个线程安全的List
- 使用线程安全的集合,比如说Verctor等
- 使用并发包JUC下的集合,比如说CopyOnWriteArrayList
那么我们为什么选择使用CopyOnWriteArrayList呢?理由就是: 前两种方案其实原理差不多,都是在方法级别上添加了Synchronized关键字做修饰,大家应该都知道,这种方法级别添加Synchronized性能并不高,锁粒度太大,可能只能使用于数据量并不太大的场景,但是CopyOnWriteArrayList采用的是读写分离和最终一致性的思想,CopyOnWriteArrayList将锁的粒度细化了,CopyOnWriteArrayList在写的时候才会上锁,并且是将原数组复制一份再进行操作,在读取操作的时候并没有对数组进行上锁,这就大大提高了并发量
CopyOnWriteArrayList 如何实现线程安全
- 定义了全局锁ReentrantLock, 在对CopyOnWriteArrayList 进行操作的时候都会先上锁,比如add、remove、set等方法都又使用到全局锁
- 使用volatile修饰了Object[] array,保证了线程之间的及时可见性
CopyOnWriteArrayList为什么并发安全且性能比Vector好?
我们知道Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获取锁,性能就会大大降低,
而CopyOnWriteArrayList只是在增删改上加ReentrantLock独占锁,但是读操作不加锁,支持并发读,CopyOnWriteArrayList支持读多写少的情况。
以上只是在学习过程中的一点拙见,还请各位多多点评
未完待续, 后面会继续补充相关面试题
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库