01.为什么还需要使用读写分离的Map?
首先我们先介绍一下常用的Map
- 线程不安全
- HashMap : 非线程安全,即任一时刻可以由多个线程同时写HashMap,可能会导致数据不一致。
- LinkedListHashMap: 非线程安全,保证 加入的顺序是怎么,循环出来就是什么顺序。可以理解为维护了元素次序的HashMap。
- TreeMap:有序的集合,可以实现自定义排序。
2.线程安全
-
- HashTable : 线程安全,但是读和写都需要加锁,性能比较低。
- ConcurrentHashMap: 采用分段锁实现,但是同一段的写和读也是互斥的,所以性能稍微低
那么如何实现一个缓存呢,可以进行并发的读和写的,从上面介绍类看,
无论是HashTable 还是 ConcurrentHashMap 都会遇到读加锁的情况,所以不太适合这种场景。
那么我们下面实现可以进行读写分离的Map,即写加锁,读不加锁。
02. CopyOnWriteMap 的实现
1、实现步骤如下
- 定义一个由初始容量的map.并使用volatile 修饰,这个为了保证在多线程环境下可见性。
- 定义一个 可重入锁 ReentrantLock
- 定义 put 方法,putAll方法,get方法
2、代码实现
3、说明
- put 方法
- 首先加锁
- new 一个新的Map,然后把旧的map放进去
- 在新的Map中添加元素
- 将旧map 的引用指向 新的 map
- 释放锁
- get 方法
- get 方法不需要获取锁,直接获取即可。
03. 使用场景
假如有下面一个场景,一个比较小的电商网站(商品信息比较少),所以我们可以直接缓存在内存中,
后期添加、删除、更新都会更新内存中的缓存。
实现代码如下:
使用CopOnWriteMap需要的注意事项:
1、尽量批量添加,否则每次添加都会复制一个map容器,可能会造成频繁的GC(垃圾回收)。
2、尽量初始化时指定容量
CopyOnWriteMap 优缺点
- 优点
- 实现读写分离,读不加锁,写加锁,性能比较高,适合在高并发场景下使用。
- 对比HashTable 和 ConcurrentHashMap ,CopyOnWriteMap 优势在于读不加锁。并发高。
- 缺点
- 内存占用高的问题。
- 对写入的数据不能及时读出来
下面说明为什么内存占用高:
-
public V put(K
key, V
value) {
-
try {
-
lock.
lock();
-
Map
<K,V
> newMap
= new HashMap
<>(map);
-
V v
= newMap.put(
key,
value);
-
map
= newMap;
-
return v;
-
} catch (
Exception e) {
-
}
finally {
-
lock.
unlock();
-
}
-
return
null;
-
}
从上面看,put操作首先 创建一个新的map,然后把添加的元素添加到新map中,
最后把旧的 map引用指向新的map内存地址。
示意图如下:
执行 map = newMap; 代码后:
上面旧的map引用虽然指向了新的Map,但是旧的Map如果回收不及时,会造成内存溢出。
为什么要使用COW ?
java util 下的集合类都不能应付并发的修改和添加,比如在迭代集合的时候对此集合进行删除,
则会出现 ConcurrentModificationException异常,或者 多个线程并发的添加,
会出现 上一个添加的值被下一个值覆盖。
为了应付并发的修改,有以下两种办法
1、写时复制
2、线程安全的容器
比如: ArrayList --> CopyOnWriteList
HashSet --> CopyOnWriteSet
HashMap --> ConcurrentHashMap