目标:
理解ConcurrentHashMap的好处,掌握ConcurrentHashMap的使用,理解ConcurrentHashMap的底层原理

引入
1.为什么要使用ConcurrentHashMap呢,有什么好处?
2.使用ConcurrentHashMap有什么要注意的?

详解
引入 ConcurrentHashMap 是为了在同步集合HashTable之间有更好的选择; HashTable 与 HashMap 、ConcurrentHashMap 主要的区别在于HashMap不是同步的、线程不安全的和不适合应用于多线程并发环境下,而 ConcurrentHashMap 是线程安全的集合容器
HashMap是Java Collection Framework的重要成员,也是Map体系中我们最为常用的一种双列集合。不过遗憾的是,HashMap不是线程安全的。也就是说,在多线程环境下,操作HashMap会导致各种各样的线程安全问题,比如在HashMap扩容在哈希时出现的死循环问题,脏读问题等。HashMap的这一缺点往往会造成诸
多不便,虽然在并发场景下Hashtable和由同步包装器包装的HashMap(Collections.synchronizedMap(Map<K,V> m) )可以代替HashMap,但是它们都是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable,
java.io.Serializable {

public synchronized V put(K key, V value) {
// 省略
}
public synchronized V remove(Object key) {

}
public synchronized V get(Object key) {
// 省略
}
public synchronized int size() {
return count;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
从上面的源码可以看出,Hashtable操作数据的所有方法都是同步方法,都使用了锁,使用当前对象作为锁,如果同一时刻只能有一个线程操作Hashtable,其他线程会被阻塞等待。

我们再来看看Collections.synchronizedMap(Map<K,V> m) 的源码
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
1
2
3
SynchronizedMap源码

private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable {
private final Map<K,V> m;
SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
mutex = this; // 使用当前对象作为锁
}
public int size() {
synchronized (mutex) {return m.size();}
}
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
public V remove(Object key) {
synchronized (mutex) {return m.remove(key);}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
通过SynchronizedMap的源码我们也发现SynchronizedMap使用当前对象作为锁,同一时刻只能有一个线程操作SynchronizedMap,其他线程还是会被阻塞。效果如下图
两个线程同时在跑Hashtable和SynchronizedMap使用synchronized来保证线程安全,使用当前对象作为锁。在线程竞争激烈的情况下效率非常低下。因为当一个线程访问Hashtable的同步方法时,其他线程访问Hashtable的同步方法可能会进入阻塞或轮询状态。如线程A使用put进行添加元素,线程B不但不能使用put方法添加元素,并且也不能使用get方法
来获取元素,所以竞争越激烈效率越低。庆幸的是,JDK为我们解决了这个问题,它为HashMap提供了一个线程安全的高效版本 ConcurrentHashMap 。在ConcurrentHashMap中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。特别地,在理想状态下,ConcurrentHashMap 可支持16个线程执行并发写操作,及任意数量线程的读操作。

ConcurrentHashMap继承的结构图:
ConcurrentHashMap继承的结构图

掌握使用ConcurrentHashMap的使用

需求:使用2个线程,每个线程循环5次,每循环一次让map的value的值增加1

使用HashMap实现:

// 需求使用2连个线程,每个线程循环5次,每循环一次让Map的value增加1
private static void testHashMap() {
final Map<String, Integer> count = new HashMap<>();
final CountDownLatch endLatch = new CountDownLatch(2);
Runnable task = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
Integer value = count.get(“a”);
if (null == value) {
count.put(“a”, 1);
} else {
count.put(“a”, value + 1);
}
}
System.out.println(“xx”);
endLatch.countDown();
}
};
new Thread(task).start();
new Thread(task).start();
try {
endLatch.await();
System.out.println(count);
} catch (Exception e) {
e.printStackTrace();
}
}
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
28
多次运行,结果不一样,testHashMap方法是两个线程操作HashMap,意图将value变为10。但是,因为多个线程用相同的key调用时,很可能会覆盖相互的结果,造成记录的次数比实际出现的次数少。
在这里插入图片描述

使用ConcurrentHashMap实现
private static void testConcurrentHashMap() {
final Map<String, Integer> count = new ConcurrentHashMap<>();
final CountDownLatch endLatch = new CountDownLatch(2);
Runnable task = new Runnable() {
@Override
public void run() {
Integer oldValue, newValue;
for (int i = 0; i < 5; i++) {
while (true) {
oldValue = count.get(“a”);
if (null == oldValue) {
newValue = 1;
if (count.putIfAbsent(“a”, newValue) == null) { // 第一次的
时候次数设置成1
break;
}
} else {
newValue = oldValue + 1; // 后面每次数量+1
if (count.replace(“a”, oldValue, newValue)) {
break;
}
}
}
}
System.out.println(“xx”);
endLatch.countDown();
}
};
new Thread(task).start();
new Thread(task).start();
try {
endLatch.await();
System.out.println(count);
} catch (Exception e) {
e.printStackTrace();
}
}
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
28
29
30
31
32
33
34
35
36
37
多次执行的结果:
多次执行的结果都是10
{a=10}

ConcurrentHashMap底层原理是什么
锁分段技术 Hashtable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问Hashtable的线
程都必须竞争同一把锁。假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访
问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是
ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一
把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
1
2
3
4
5
在这里插入图片描述

ConcurrentHashMap的put方法源码
public V put(K key, V value) {
if (value == null)
throw new NullPointerException();
int hash = hash(key.hashCode());
return segmentFor(hash).put(key, hash, value, false);
}
1
2
3
4
5
6
ConcurrentHashMap的put操作被ConcurrentHashMap委托给特定的 Segment 段来实现。也就是说,当我们向ConcurrentHashMap中put一个Key/Value对时,首先会获得Key的哈希值并对其再次哈希,然后根据最终的hash值定位到这条记录所应该插入的段,定位段的segmentFor()方法源码如下:

final Segment<K,V> segmentFor(int hash) {
return segments[(hash >>> segmentShift) & segmentMask];
}
1
2
3
根据key的hash值的高n位就可以确定元素到底在哪一个Segment中。紧接着,调用这个段的put()方法来将目标Key/Value对插入到Segment段中,Segment段的put()方法的源码如下所示:

V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock(); // 上锁
try {
int c = count;
if (c++ > threshold) // ensure capacity
rehash();
HashEntry<K,V>[] tab = table; // table是Volatile的
int index = hash & (tab.length - 1); // 定位到段中特定的桶
HashEntry<K,V> first = tab[index]; // first指向桶中链表的表头
HashEntry<K,V> e = first;
// 检查该桶中是否存在相同key的节点
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue;
if (e != null) { // 该桶中存在相同key的节点
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value; // 更新value值
}else { // 该桶中不存在相同key的节点
oldValue = null;
++modCount;
tab[index] = new HashEntry<K,V>(key, hash, first, value); // 创建HashEntry
并将其链到表头
count = c;
}
return oldValue; // 返回旧值(该桶中不存在相同key的结点,则返回null)
} finally {
unlock(); // 在finally子句中解锁
}
}
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
28
29
30
ConcurrentHashMap的get方法源码 当我们从ConcurrentHashMap中查询一个指定Key的键值对时,首先会
定位其应该存在的段,然后查询请求委托给这个段进行处理,源码如下:
public V get(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).get(key, hash);
}
1
2
3
4
Segment中get操作的源码:
V get(Object key, int hash) {
if (count != 0) { // read-volatile,首先读count变量
HashEntry<K,V> e = getFirst(hash); // 获取桶中链表头结点
while (e != null) {
if (e.hash == hash && key.equals(e.key)) { // 查找链中是否存在指定Key的键
值对
V v = e.value;
if (v != null) // 如果读到value域不为 null,直接返回
return v;
return readValueUnderLock(e); // recheck
}
e = e.next;
}
}
return null; // 如果不存在,直接返回null
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
下面我们来看看ConcurrentHashMap并发读写的几种情形:

不同Segment的写入是可以并发执行的。
在这里插入图片描述

同一Segment的写和读是可以并发执行的。
在这里插入图片描述

Segment的写入是需要上锁的,因此对同一Segment的并发写入会被阻塞。
在这里插入图片描述ConcurrentHashMap当中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。

ConcurrentHashMap 存取小结:
在ConcurrentHashMap进行存取时,首先会定位到具体的段,然后通过对具体段的存取来完成对整个ConcurrentHashMap的存取。特别地,无论是ConcurrentHashMap的读操作还是写操作都具有很高的性能:在进行读操作时不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。

ConcurrentHashMap注意事项
ConcurrentHashMap是并发效率更高的Map,实际上,并发执行Concurrent-HashMap只能保证自身的数据不被破坏,但无法保证业务的行为是否正确。错误的理解这里的线程安全,不恰当的使用ConcurrentHashMap,往往会导致出现问题。如果只调用get(),或只调用put()时,ConcurrentHashMap是线程安全的。但是在你调用完get()后,调用put()之前,如果有另外一个线程调用了map.put(name, x),你再去执行map.put(name,x),就很可能把前面的操作结果覆盖掉了。所以,即使在线程安全的情况下,还是有可能违反原子(atom)操作的规则。

ConcurrentHashMap只能保证写是同步的,不能保证先读后写的原子性。

总结:
Hashtable和SynchronizedMap使用synchronized来保证线程安全,使用当前对象作为锁,同一时刻只能有一个线程操作,其他线程会被阻塞,所以竞争越激烈效率越低。ConcurrentHashMap无论是读操作还是写操作都具有很高的性能:在进行读操作时不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响对其它段的访问。 ConcurrentHashMap只能保证单个方法是同步的,不能保证先读后写的原子性

HashTable,HashMap和ConcurrentHashMap的区别?相关推荐

  1. 聊聊传说中的散列哈希Hash算法,以及Java中的HashTable,HashMap,HashSet,ConcurrentHashMap......

    建议本文结合java源码来阅读,看了之后就什么都懂了,还有参考文献. 散列(Hash) 是一种按关键字编址的存储和检索方法 散列表(HashTable)根据元素的关键字确定元素的位置 散列函数(Has ...

  2. HashMap与ConcurrentHashMap的区别

    从JDK1.2起,就有了HashMap,正如前一篇文章所说,HashMap不是线程安全的,因此多线程操作时需要格外小心. 在JDK1.5中,伟大的Doug Lea给我们带来了concurrent包,从 ...

  3. (二)Java中的HashMap与ConcurrentHashMap的区别

    HashMap不是线程安全的,因此多线程操作时需要格外小心. 在JDK1.5中,伟大的Doug Lea给我们带来了concurrent包,从此Map也有安全的了. ConcurrentHashMap具 ...

  4. HashTable, HashMap, LinkedHashMap, ConcurrentHashMap

    HashTable: 不允许null的key或value, 线程安全 HashMap: 允许一个null的key, 无限的null value, 非线程安全 LinkedHashMap: HashMa ...

  5. HashMap和ConcurrentHashMap的区别,HashMap的底层源码。

    Hashmap本质是数组加链表.根据key取得hash值,然后计算出数组下标,如果多个key对应到同一个下标,就用链表串起来,新插入的在前面. ConcurrentHashMap:在hashMap的基 ...

  6. 面试题:HashMap和ConcurrentHashMap的区别,HashMap的底层源码。

    Hashmap本质是数组加链表.根据key取得hash值,然后计算出数组下标,如果多个key对应到同一个下标,就用链表串起来,新插入的在前面. ConcurrentHashMap:在hashMap的基 ...

  7. Android-App-启动优化全记录,hashmap和concurrenthashmap的区别

    控制线程数量 – 线程池 检查线程间的锁 ,防止依赖等待 使用合理的启动架构 微信内部使用的 mmkernel 阿里 Alpha 系统调度优化 应用启动的时候,如果主线程的工作过多,也会造成主线程过于 ...

  8. HashMap HashTable和ConcurrentHashMap的区别

    HashMap和Hashtable的区别 HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别.主要的区别有:线程安全性,同步(synchronizatio ...

  9. Hashtable,HashMap,ConcurrentHashMap都是Map的实现类,它们在处理null值的存储上有细微的区别,下列哪些说法是正确的

    多选 Hashtable,HashMap,ConcurrentHashMap都是Map的实现类,它们在处理null值的存储上有细微的区别,下列哪些说法是正确的:答案在文末 A. Hashtable的K ...

  10. 3、HashMap、HashTable和ConcurrentHashMap的区别?

    HashMap和HashTable的区别一种比较简单的回答是: (1)HashMap是非线程安全的,HashTable是线程安全的. (2)HashMap的键和值都允许有null存在,而HashTab ...

最新文章

  1. 在TMG2010中发布Web服务器场
  2. 5-MST 多生成树 //IOU模拟
  3. linux centos yum 报错 one of the configured repositories failed 解决方法
  4. StackGAN mini review
  5. Windows Phone 7“芒果”更新带来浏览器重大升级:IE Mobile 9
  6. SQLite—homework
  7. 购物车的功能——JS源码
  8. Linux 查看文件位置/查看文件路径的命令
  9. python 通信模块_python 多进程通信模块
  10. 《人人都是产品经理》读后小结
  11. python 条件判断和循环
  12. 学习时有必要做思维导图吗?
  13. SDL2.0超简单入门 100行代码实现播放wav声音文件
  14. 一对一语音视频直播双端原生+php后台源码 社交交友APP匹配语音视频聊天即时通信源码
  15. Vant组件库 引入 阿里矢量图 添加自己喜欢的 ICON
  16. 安卓java模拟器跳过付费,不用花钱买android手机,电脑端Android模拟器安装使用教程...
  17. 交替性注意力_如何培养共同注意力(Joint Attention)——社会性发展基石
  18. 零基础go还是python_零基础学习Python的经验之谈
  19. SPC是什么,有什么用
  20. 王子与公主的爱情故事新结局

热门文章

  1. HTTP1.0/1.1/2.0特性对比_转
  2. postman 使用
  3. centos7中 npm install express 时Error: Cannot find module 'express'错误
  4. HttpClient3.x之Get请求和Post请求示例
  5. 【嵌入式Linux学习七步曲之第五篇 Linux内核及驱动编程】Linux内核抢占实现机制分析...
  6. short_open_tag 惹的问题
  7. [轉]PHP执行MYSQL存储过程报错:Commands out of sync; you can't run this command now 问题的解决...
  8. 经典相声——企业信息化新“五官争功”
  9. 品味.NET经典[转载]
  10. Android7.1 Audio Debug相关方法