并发编程6:CopyOnWriteArrayList 的写时复制
来源:互联网 发布:音乐降调软件手机 编辑:程序博客网 时间:2024/06/06 07:21
首先提个问题:
- 线程安全的 List 集合有什么?
- CopyOnWriteArrayList 的特点以及使用场景?
如果这个问题你答不上来,那这篇文章可能就对你有些价值。
读完本文你将了解:
- CopyOnWriteArrayList简介
- CopyOnWriteArrayList源码分析Android SDK25
- 底部实现
- 读取操作
- 修改操作
- 迭代器
- CopyOnWriteArrayList源码JDK8有什么不同
- 总结
- 优缺点
- Thanks
CopyOnWriteArrayList 简介
在 Java 中多线程读写 List,有两种选择:
- Vector
- CopyOnWriteArrayList
Java 集合深入理解:古老的 Vector 中我们了解到 Vector
几乎所有的读写操作都加了 synchronized ,意味着在多线程环境下,如果有多个线程同时想要操作这个 Vector,必须串行排队等着前面的操作完了才能下一个。
就好比我和小肉一起写一个项目,每次得等她完全写完要写的,我才能接着写,这效率实在差了点。
好在有 Git,小肉想帮我修改程序员审美 UI 时,可以拷贝一份代码进行修改,修改完后再合并到远程。
git 多人协作流程:
当有人要修改代码时,拉取服务端代码到本地,先修改本地的代码,然后再提交到服务器,这样可以避免直接修改远端代码对别人造成的影响。
CopyOnWriteArrayList
是一个线程安全的随机访问列表,实现了 List
接口:
- 1
- 2
- 3
- 1
- 2
- 3
它的思想和 Git 有些相似,即使在多个线程中被并发访问, CopyOnWriteArrayList
的读操作(比如 get()
)也不会阻塞其他操作;写操作则是通过复制一份,对复制版本进行操作,不会影响原来的数据。
和 Vector
相对效率提高不少。缺点就是可能读取的不是最新的值。
批量操作(比如 addAll()
, clear()
)是原子的,也就是说不存在重排序导致的未赋值就访问等情况。
在 Java 集合源码解析:Iterator 中我们了解到:
在调用迭代器的 next,remove 方法时都会比较 expectedModCount 和 modCount 是否相等,如果不相等就会抛出 ConcurrentModificationException ,也就是成为了 fail-fast。
如果需要在遍历时操作列表,其中一种解决办法就是使用 CopyOnWriteArrayList
,它的迭代器永远不会抛出 ConcurrentModificationException
异常。原因在于:在创建一个迭代器时,它会拷贝一份列表数据,这样即使操作列表也不会影响迭代器,缺点和前面一样,可能无法反映数据的最新状态。
CopyOnWriteArrayList 源码分析(Android SDK 25)
从名字就可以看出来 CopyOnWriteArrayList
的特点是 “CopyOnWrite”(写时复制),即在写入新元素时不直接操作原容器,而是先复制一个快照,对这个快照进行操作,在操作结束后再将原容器的引用指向新引用。
底部实现
- 1
- 1
CopyOnWriteArrayList
底层也是一个数组,不同之处在于它被 volatile 修饰,对它的修改、读取具有“可见性”、“有序性”(但不保证原子性),因此可以不加同步块,直接读取。
有三种构造函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
分别创建了空的数组、使用指定集合和数组来创建新数组。
读取操作
先看简单的几个读取操作:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
就是直接读取数组的内容,简单粗暴,快速高效。
修改操作
读取时轻松,但修改时就需要做同步操作了。先看添加元素 add()
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
可以看到,在添加元素时调用 System.arraycopy()
拷贝了一个新数组,然后添加进去元素,最后将原数组的引用修改为拷贝数组。
如果其他线程在“原始数组引用更新”之前读取数据,那它访问到的就是旧数据。
方法被 synchronized
修饰,因此在这个过程中,其他调用这个方法的线程都会阻塞等待,只有这个方法结束后才有获得锁的机会。
set()
方法也是类似的:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
删除操作 `remove()
也是类似:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
迭代器
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
返回的迭代器是 CowIterator
,实现了 ListIterator
接口:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
可以看到 CopyOnWriteArrayList
或者它的子列表返回的迭代器不能修改列表数据,尤其是 Iterator.remove()
, ListIterator.add()
, ListIterator.set()
方法都会抛出 UnsupportedOperationException
异常。
CopyOnWriteArrayList 源码 JDK8 有什么不同
看完 Android-25 的 CopyOnWriteArrayList
代码,再去看 JDK8 的,发现两者差别有点大啊,真是坑。
在知乎的一个问题下了解了 android SDK 的 Java 源码为什么和 JDK 不一致,摘抄如下:
Android 使用的Java 库是Apache的Harmony, 与官方Java库接口相同,里面实现不一样。
就在06年的时候Sun公司宣布会将JDK开源,但是直到2009年4月才正式发布,而Android已经在2008年发布第一步部智能手机了。所以Google最早的时候使用了当时Apache的Java开源实现Harmony项目。
可惜Sun公司一直不承认Harmony,前不久Harmony那一帮人怒了,给Oracle放狠话说再不承认我我就抵制Java7,结果反倒把Google吓坏了,于是就出现了google宣布切换到openjdk上这条新闻。在Android N上,已经切换到OpenJDK了;基本可以认为Android SDK下的源码与JDK相同。
作者:weishu
链接:https://www.zhihu.com/question/40103672/answer/84730730
来源:知乎
JDK8 中的 CopyOnWriteArrayList
底层实现也是数组,不同之处在于使用 Lock 而不是 synchronized 进行同步:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
构造函数也有些区别:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
在使用集合作为参数构造时,做了一些优化:如果这个集合也是 CopyOnWriteArrayList
,就直接指向该集合的数组;否则复制一下。
读取操作没什么变化,都是读的数组,不同之处在于通过 getter 方法包了一层:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
添加操作 add()
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
唯一的区别就是使用了 ReentrantLock
,它和 synchronized
的区别大致如下:
- synchronized 无法中断一个正在等候获得锁的线程,也无法通过投票得到锁
- ReentrantLock 拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性
摘自:http://blog.csdn.net/fw0124/article/details/6672522
删除操作也差不多:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
总结
核心思想就两点:
- 底部实现(这里是数组) volatile 修饰,保证一致性
- 写时复制,写完更新引用
有了这两点,我们自己写个 CopyOnWriteHashMap
也是分分钟的事,有兴趣可以自己写一下。
优缺点
优点:
- 可以在多线程环境下操作 List
- 读的效率很高
缺点:
- 读的可能不是最新值
- 每次写需要创建个新数组,占用额外内存
可以看到,应该在并发读远大于并发写的情况下使用这个容器,比如保存缓存数据。
类似的思想还有个
CopyOnWriteArraySet
。
Thanks
http://ifeve.com/java-copy-on-write/
http://blog.csdn.net/fw0124/article/details/6672522
http://coolshell.cn/articles/11175.html
- 并发编程:CopyOnWriteArrayList 的写时复制
- 并发编程6:CopyOnWriteArrayList 的写时复制
- 并发编程6:CopyOnWriteArrayList 的写时复制
- CopyOnWriteArrayList——写时复制的容器
- 并发编程一:CopyOnWriteArrayList
- Java并发编程---CopyOnWriteArrayList
- Java并发编程:CopyOnWriteArrayList
- Java并发编程:CopyOnWriteArrayList
- java并发编程之 CopyOnWriteArrayList
- Java并发编程:并发容器之CopyOnWriteArrayList
- Java并发编程:并发容器之CopyOnWriteArrayList
- Java并发编程:并发容器之CopyOnWriteArrayList
- Java并发编程:并发容器之CopyOnWriteArrayList
- 【Java并发编程】并发容器之CopyOnWriteArrayList
- Java并发编程:并发容器之CopyOnWriteArrayList
- Java并发编程:并发容器之CopyOnWriteArrayList
- Java并发编程:并发容器之CopyOnWriteArrayList
- Java并发编程:并发容器之CopyOnWriteArrayList
- java.lang.OutOfMemoryError:GC overhead limit exceeded
- BootStrap学习简介
- Java设计模式--门面模式【Facade Pattern】
- Java 日期时间
- java提高篇(十四)-----字符串
- 并发编程6:CopyOnWriteArrayList 的写时复制
- Ch_02 算法分析(最大子序列问题)
- 测试初学者常用的LINUX命令
- 关于树节点的问题
- java提高篇(十五)-----关键字final
- SafeQueue读写示例
- TP 查询时候标题带有&的字符,查询没有结果!打出SQL语句去数据库查,返回有结果!
- 线程基础知识
- 手机测试功能点