看了CopyOnWriteArrayList后自己实现了一个CopyOnWriteHashMap

引言
面试官: 小伙子你有点眼熟啊,是不是去年来这面试过啊。
二胖: 啊,没有啊我这是第一次来这。
面试官: 行,那我们开始今天的面试吧,刚开始我们先来点简单的吧,java里面的容器你知道哪些啊,跟我说一说吧。
二胖: 好的,java里面常见容器有ArrayList(线程非安全)、HashMap(线程非安全)、HashSet(线程非安全),ConcurrentHashMap(线程安全)。
面试官: ArrayList 既然线程非安全那有没有线程安全的ArrayList列?
二胖: 这个。。。 好像问到知识盲点了。
面试官: 那我们今天的面试就先到这了,我待会还有一个会,后续如有通知人事会联系你的。
以上故事纯属虚构如有雷同请以本文为主。
什么是COW
在java里面说到集合容器我们一般首先会想到的是HashMap、ArrayList、HasHSet这几个容器也是平时开发中用的最多的。
这几个都是非线程安全的,如果我们有特定业务需要使用线程的安全容器列,
HashMap可以用ConcurrentHashMap代替。ArrayList可以使用Collections.synchronizedList()方法(list每个方法都用synchronized修饰) 或者使用Vector(现在基本也不用了,每个方法都用synchronized修饰)
或者使用CopyOnWriteArrayList替代。- HasHSet 可以使用
Collections.synchronizedSet或者使用CopyOnWriteArraySet来代替。(CopyOnWriteArraySet为什么不叫CopyOnWriteHashSet因为CopyOnWriteArraySet底层是采用CopyOnWriteArrayList来实现的)
我们可以看到CopyOnWriteArrayList在线程安全的容器里面多次出现。
首先我们来看看什么是CopyOnWrite?Copy-On-Write简称COW,是一种用于程序设计中的优化策略。
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
为什么要引入COW
防止ConcurrentModificationException异常
在java里面我们如果采用不正确的循环姿势去遍历List时候,如果一边遍历一边修改抛出java.util.ConcurrentModificationException错误的。
如果对ArrayList循环遍历不是很熟悉的可以建议看下这篇文章《ArrayList的删除姿势你都掌握了吗》
List<String> list = new ArrayList<>();
list.add("张三");
list.add("java金融");
list.add("javajr.cn");
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String content = iterator.next();
if("张三".equals(content)) {
list.remove(content);
}
}
上面这个栗子是会发生java.util.ConcurrentModificationException异常的,如果把ArrayList改为CopyOnWriteArrayList 是不会发生生异常的。
线程安全的容器
我们再看下面一个栗子一个线程往List里面添加数据,一个线程循环list读数据。
List<String> list = new ArrayList<>();
list.add("张三");
list.add("java金融");
list.add("javajr.cn");
Thread t = new Thread(new Runnable() {
int count = 0;
@Override
public void run() {
while (true) {
list.add(count++ + "");
}
}
});
t.start();
Thread.sleep(10000);
for (String s : list) {
System.out.println(s);
}
我们运行上述代码也会发生ConcurrentModificationException异常,如果把ArrayList换成了CopyOnWriteArrayList就一切正常。
CopyOnWriteArrayList的实现
通过上面两个栗子我们可以发现CopyOnWriteArrayList是线程安全的,下面我们就来一起看看CopyOnWriteArrayList是如何实现线程安全的。
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;
从源码中我们可以知道CopyOnWriteArrayList这和ArrayList底层实现都是通过一个Object的数组来实现的,只不过 CopyOnWriteArrayList的数组是通过volatile来修饰的,为什么需要volatile修饰建议可以看看《Java的synchronized 能防止指令重排序吗?》
还有新增了ReentrantLock。
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();
}
}
上述源码我们可以发现比较简单,有几个点需要稍微注意下
- 增加数据的时候是通过
ReentrantLock加锁操作来(在jdk11的时候采用了synchronized来替换ReentrantLock)保证多线程写的时候只有一个线程进行数组的复制,否则的话内存中会有多份被复制的数据,导致数据错乱。 - 数组是通过
volatile修饰的,根据volatile的happens-before规则,写线程对数组引用的修改是可以立即对读线程是可见的。 - 通过写时复制来保证读写实在两个不同的数据容器中进行操作。
自己实现一个COW容器
再Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet,但是并没有CopyOnWriteHashMap我们可以按照他的思路自己来实现一个CopyOnWriteHashMap
public class CopyOnWriteHashMap<K, V> implements Map<K, V>, Cloneable {
final transient ReentrantLock lock = new ReentrantLock();
private volatile Map<K, V> map;
public CopyOnWriteHashMap() {
map = new HashMap<>();
}
@Override
public V put(K key, V value) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Map<K, V> newMap = new HashMap<K, V>(map);
V val = newMap.put(key, value);
map = newMap;
return val;
} finally {
lock.unlock();
}
}
@Override
public V get(Object key) {
return map.get(key);
}
@Override
public V remove(Object key) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Map<K, V> newMap = new HashMap<K, V>(map);
if (!newMap.containsKey(key)) {
return null;
}
V v = newMap.get(key);
newMap.remove(key);
map = newMap;
return v;
}finally {
lock.unlock();
}
}
上述我们实现了一个简单的CopyOnWriteHashMap,只实现了add、remove、get方法其他剩余的方法可以自行去实现,涉及到只要数据变化的就要加锁,读无需加锁。
应用场景
CopyOnWrite并发容器适用于读多写少的并发场景,比如黑白名单、国家城市等基础数据缓存、系统配置等。这些基本都是只要想项目启动的时候初始化一次,变更频率非常的低。如果这种读多写少的场景采用 Vector,Collections包装的这些方式是不合理的,因为尽管多个读线程从同一个数据容器中读取数据,但是读线程对数据容器的数据并不会发生发生修改,所以并不需要读也加锁。
CopyOnWrite缺点
CopyOnWriteArrayList虽然是一个线程安全版的ArrayList,但其每次修改数据时都会复制一份数据出来,所以CopyOnWriteArrayList只适用读多写少或无锁读场景。我们如果在实际业务中使用CopyOnWriteArrayList,一定是因为这个场景适合而非是为了炫技。
内存占用问题
因为CopyOnWrite的写时复制机制每次进行写操作的时候都会有两个数组对象的内存,如果这个数组对象占用的内存较大的话,如果频繁的进行写入就会造成频繁的Yong GC和Full GC。
数据一致性问题
CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。读操作的线程可能不会立即读取到新修改的数据,因为修改操作发生在副本上。但最终修改操作会完成并更新容器所以这是最终一致性。
CopyOnWriteArrayList和Collections.synchronizedList()
简单的测试了下CopyOnWriteArrayList 和 Collections.synchronizedList()的读和写发现:
- 在高并发的写时CopyOnWriteArray比同步Collections.synchronizedList慢百倍
- 在高并发的读性能时CopyOnWriteArray比同步Collections.synchronizedList快几十倍。
- 高并发写时,CopyOnWriteArrayList为何这么慢呢?因为其每次add时,都用Arrays.copyOf创建新数组,频繁add时内存申请释放性能消耗大。
- 高并发读的时候CopyOnWriteArray无锁,Collections.synchronizedList有锁所以读的效率比较低下。
总结
选择CopyOnWriteArrayList的时候一定是读远大于写。如果读写都差不多的话建议选择Collections.synchronizedList。
结束
- 由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。
- 如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。
- 感谢您的阅读,十分欢迎并感谢您的关注。
巨人肩膀摘苹果
http://ifeve.com/java-copy-on-write/
看了CopyOnWriteArrayList后自己实现了一个CopyOnWriteHashMap的更多相关文章
- vs2010旗舰版后,运行调试一个项目时调试不了,提示的是:无法使用“pc”附加到应用程序“webdev.webserver40.exe(PID:2260”
具体问题描述: vs2010旗舰版后,运行调试一个项目时调试不了,能编译,按ctrl+f5 可以运行,但是就是调试就不行,提示的是:无法使用“pc”附加到应用程序“webdev.webserver40 ...
- 瞧一瞧,看一看呐,用MVC+EF快速弄出一个CRUD,一行代码都不用写,真的一行代码都不用写!!!!
瞧一瞧,看一看呐用MVC+EF快速弄出一个CRUD,一行代码都不用写,真的一行代码都不用写!!!! 现在要写的呢就是,用MVC和EF弄出一个CRUD四个页面和一个列表页面的一个快速DEMO,当然是在不 ...
- Java基础知识强化之IO流笔记52:IO流练习之 把一个文件中的字符串排序后再写入另一个文件案例
1. 把一个文件中的字符串排序后再写入另一个文件 已知s.txt文件中有这样的一个字符串:"hcexfgijkamdnoqrzstuvwybpl" 请编写程序读取数据内容,把数据排 ...
- SNF快速开发平台MVC-审核流,审核完成后会给下一个审核人发邮件,下一个审核人可以不登录系统,在邮件里进行审核处理
审核流设计和使用参考以下资料: 审核流设计 http://www.cnblogs.com/spring_wang/p/4874531.html 审核流实例 http://www.cnblogs.com ...
- 信1705-2 软工作业最大重复词查询思路: (1)将文章(一个字符串存储)按空格进行拆分(split)后,存储到一个字符串(单词)数组中。 (2)定义一个Map,key是字符串类型,保存单词;value是数字类型,保存该单词出现的次数。 (3)遍历(1)中得到的字符串数组,对于每一个单词,考察Map的key中是否出现过该单词,如果没出现过,map中增加一个元素,key为该单词,value为1(
通过学习学会了文本的访问,了解一点哈希表用途.经过网上查找做成了下面查询文章重复词的JAVA程序. 1 思 思路: (1)将文章(一个字符串存储)按空格进行拆分(split)后,存储到一个字符串(单词 ...
- Eclipse中的工程引入jar包后没有整合到一个文件夹而是全部在根目录下显示
Eclipse中的工程引入jar包后没有整合到一个文件夹而是全部在根目录下显示 解决方案: 1,在Eclipse中,点击window-->Preferences-->Java-->B ...
- "字符串"经过strip 之后还是字符串, 而"字符串"经过split 分开后,就变成了一个列表["x","xx","xxx"]
"字符串"经过strip 之后还是字符串, 而"字符串"经过split 分开后,就变成了一个列表["x","xx",&q ...
- JavaScript一个页面中有多个audio标签,其中一个播放结束后自动播放下一个,audio连续播放
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- group by 分组后 返回的是一个同属性的集合
group by 分组后 返回的是一个同属性的集合 我们可以遍历该集合
随机推荐
- 我与PHP,ULM和Vue.js不得不说的故事(我与PHP白月光的那些事儿之第三年的见异思迁番外篇)
关于PHP的认知 -----恍惚间眸眼像极了一位故人 第一年遇见,那时的它还稚嫩懵懂.像白纸一样的脸,极容易读懂.于是放荡不羁,不放真心.稍微用心,它便能高兴好久.初识它时它叫C语言,浅尝却不觉其难过 ...
- java41
2019.8.7全部回顾完毕 收获:搞懂了以前不理解的内容 学会了Markdown语法 1. 将首字母变大写 public class _02将首字母变大写 { public static void ...
- 17.java设计模式之观察者模式
基本需求: 气象站可以将每天测量到的温度,湿度,气压等等,以公告的形式发布出去(比如发布到自己的网站或第三方) 需要设计开放型API,便于其他第三方也能接入气象站获取数据 提供温度.气压和湿度的接口 ...
- vue中两行代码实现全选及子选项全部选中,则全选按钮选中,反之有一个没选中,就取消选中全选按钮
every() 方法使用指定函数检测数组中的所有元素: 如果数组中检测到有一个元素不满足,则整个表达式返回 false ,且剩余的元素不会再进行检测. 如果所有元素都满足条件,则返回 true. 逻辑 ...
- Scrum 冲刺 第四篇
Scrum 冲刺 第四篇 每日会议照片 昨天已完成工作 队员 昨日完成任务 黄梓浩 初步完成app项目架构搭建 黄清山 完成部分个人界面模块数据库的接口 邓富荣 完成部分后台首页模块数据库的接口 钟俊 ...
- CF500G / T148321 走廊巡逻
题目链接 这题是 Codeforces Goodbye 2014 的最后一题 CF500G,只是去掉了 \(u \not= x, v \not = v\) 的条件. 官方题解感觉有很多东西说的迷迷瞪瞪 ...
- 2020 AC Saber夏季赛 游记
看着 \(\text{Acwing}\) 成长的我,还是写一下游记吧(?). 刚刚中考完,手速本来就老年,更慢,算法每次写完都要调,而且还查不出错.. Day 1 初赛.紧张,因为中考四五个月没打了, ...
- Unity GameObject
GameObject 游戏对象 GameObject是unity所有实体的基类 gameObject 获取当前脚本所挂载的游戏对象 一般来说,在属性视图中能看到或修改的属性,我们同样可以在脚本中获取并 ...
- 容器服务 TKE 存储插件与云硬盘 CBS 最佳实践应用
引言 随着自研上云的深入,越来越多的有状态服务对于在 TKE 集群中使用云上存储能力的需求也越来越强烈. 目前腾讯云容器服务 TKE(Tencent Kubernetes Engine已支持在 TKE ...
- JAVA基础学习1
一.JAVA是一种具有多种功能的高级语言:1,可以用于开发web页面上的小程序,桌面上运行的应用程序: 2,用于客户端服务器资源通讯的服务器端中间件: 3,还可以用于web服务器. 二.程序设计的5个 ...