什么时候使用CurrentHashMap
在多线程并发向HashMap中put数据时,就需要把HashMap换成ConcurrentHashMap。因为多线程环境下,使用HashMap进行put操作resize会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap.
线程安全的使用map结构可以使用HashTable和CurrentHashMap,HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为同步机制只能让一个线程去运行,其他线程既不能put 也不能get操作
1.7 的结构
ConcurrentHashMap 类中包含两个静态内部类 HashEntry (1.8中已经变成了Node实现Map.Entry<K,V>)和 Segment(继承可重入锁)。HashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象连接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。
锁分段技术
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,同个对象锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。ConcurrentHashMap使用分段锁Segment来保护不同段的数据,Segment继承ReentrantLock实现锁的功能。
1.8 结构优化
get操作为什么不用加锁
get操作全程不需要加锁是因为Node的成员val是用volatile修饰的和数组用volatile修饰没有关系。 数组用volatile修饰主要是保证在数组扩容的时候保证可见性。
get操作的高效之处在于整个get过程不需要加锁, 原因是它的get方法里将要使用的共享变量都定义成volatile,如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value。定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁。之所以不会读到过期的值,是根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。
优化总结
没有再使用Segment分段锁技术,因为分段锁结构最多支持16个线程的并发,1.8后使用Node数组+链表(长度超过8变为红黑树),Node中的 val、 next 都用了 volatile 修饰,保证了可见性,使用 cas 和 synchronized 来保证多线程安全性。总结主要有如下几点:
- Node中的 val、 next 都用了 volatile 修饰, get 操作全程无锁
- put 操作如果没有hash冲突,使用 cas 进行尝试 put 操作,如果有已有冲突节点,则用 synchronize 锁住头节点后进行 put 操作
- 冲突后将每次插入的新结点放在链表的尾部,不再用头插法而采用尾插法,防止扩容时形成死循环,链表冲突长度超过 8 变为红黑树