上次我们说了一下项目中经常会出现的死锁问题,今天我们要说的是关于集合的问题,实际上跟锁也有一定的关系,让我们来一起看看吧。
一、简介
1、是什么
ConcurrentHashMap是Java5中新增加的一个线程安全的Map集合,可以用来替代HashTable。对于ConcurrentHashMap是如何提高其效率的,可能大多人只是知道它使用了多个锁代替HashTable中的单个锁,也就是锁分离技术(Lock Stripping)。
2、为什么
- 为什么不用HashMap
hashmap本质数据加链表。根据key取得hash值,然后计算出数组下标,如果多个key对应到同一个下标,就用链表串起来,新插入的在前面。而且hashmap在单线程情况下效率较高。
但是在多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。
- 为什么不用HashTable
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
- 为什么要用ConcurrentHashMap
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
二、深入解析
1、结构图解析
ConcurrentHashMap和Hashtable主要区别就是围绕着锁的粒度以及如何锁,可以简单理解成把一个大的HashTable分解成多个,形成了锁分离。而Hashtable的实现方式是—锁整个hash表
从图中可以看到,ConcurrentHashMap内部分为很多个Segment,每一个Segment拥有一把锁,然后每个Segment(继承ReentrantLock)下面包含很多个HashEntry列表数组。对于一个key,需要经过三次(为什么要hash三次下文会详细讲解)hash操作,才能最终定位这个元素的位置,这三次hash分别为:
对于一个key,先进行一次hash操作,得到hash值h1,也即h1 = hash1(key);
将得到的h1的高几位进行第二次hash,得到hash值h2,也即h2 = hash2(h1高几位),通过h2能够确定该元素的放在哪个Segment;
将得到的h1进行第三次hash,得到hash值h3,也即h3 = hash3(h1),通过h3能够确定该元素放置在哪个HashEntry。
2、实现原理
ConcurrentHashMap把Map分成了N个Segment(默认16),其中Segment是线程同步的,相当于分成了N个Hashtable。当实现Put方法时,在key值经过正常的hash后,还要再经过一次segmentForHash算法,用来分配具体防盗哪个Segment。后来的线程如果经过计算也是放在这个Segment下,则需要先获取锁,如果计算得出应该放在其他的Segment,则正常执行,不会影响效率,以此实现线程安全。ConcurrentHashMap使用锁分离技术,只要多个修改操作不发生在同一个Segment上,它们就可以并发进行。
有些方法需要跨段,比如size()和containsValue(),需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。
3、源码详解
总结:
这个类其实在我们项目中的缓存使用到了,利用ConcurrentHashMap来存放一些常用的信息,由于是在并发的情况下使用,考虑到性能的问题,优先选用ConcurrentHashMap类。不过我建议大家还是要多看看源码,从中会受益很多。