luohui 2020-01-12
ConcurrentHashMap 是一个能够保证线程安全的并发容器
。
HashTable 是一个典型的同步容器
。虽然 HashTable 的所有方法都用 synchronzied 修饰,但是如果我们编程时将 get 和 put 这类的操作写成了非原子操作,就会有线程安全问题了。虽然单个方法都是线程安全的,但是组合到一起就不是了。所以只能叫同步容器
。另外使用 synchronized 锁使得 HashTable 的效率比较低。
Collections.synchronizedMap()方法同理。
ConcurrentHashMap 在 JDK 1.7 中采用分段锁计数,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 都需要锁。JDK 1.8 弃用了分段锁,采用 CAS + synchronized 来保证线程安全,也可以叫节点锁。
HashTable 低效的原因就是因为所有访问 HashTable 的线程都争夺一把锁。如果容器有很多把锁,每次只锁住容器的一部分数据。当多个线程访问容器里面的不同部分时线程就不存在锁的竞争,这样可以提高访问效率。1.7 中锁住的力度是分段,1.8 中锁住的力度是节点。
JDK 1.7 ConcurrentHashMap 的结构:
get 方法
整个 get 操作不需要加锁。那么它时如何保证读操作线程安全的那,原因是所有 Entry 都用 volatile 修饰了,可以保证线程之间的可见性,这也是 volatile 替换锁的经典应用场景。
put 方法
/** * 存放node的数组,大小是2的幂次方 */ transient volatile Node[] table; /** * 扩容时用于存放数据的变量,平时为null */ private transient volatile Node[] nextTable; /** * 通过CAS更新,记录容器的容量大小 */ private transient volatile long baseCount; /** * 控制标志符 * 负数: 代表正在进行初始化或扩容操作,其中-1表示正在初始化,-N 表示有N-1个线程正在进行扩容操作 * 正数或0: 代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,类似于扩容阈值 * 它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。 * 实际容量 >= sizeCtl,则扩容 */ private transient volatile int sizeCtl; /** * 下次transfer方法的起始下标index加上1之后的值 */ private transient volatile int transferIndex; /** * CAS自旋锁标志位 */ private transient volatile int cellsBusy; /** * counter cell表,长度总为2的幂次 */ private transient volatile CounterCell[] counterCells;
Node 节点类与 HashMap 中定义很相似,value 和 next 属性都使用 volatile 保证了内存的可见性。
static class Node implements Map.Entry { final int hash; final K key; volatile V val; volatile Node next; ... }
put 方法
ConcurrentHashMap的put操作与HashMap很相似,但ConcurrentHashMap不允许null作为key和value,并且由于需要保证线程安全,有以下两个多线程情况:
①.如果一个或多个线程正在对ConcurrentHashMap进行扩容操作,当前线程也要进入扩容的操作中。这个扩容的操作之所以能被检测到,是因为transfer方法会将已经操作过扩容桶头结点置为ForwardingNode节点,如果检测到需要插入的位置被该节点占有,就帮助进行扩容。
②.如果检测到要插入的节点是非空且不是ForwardingNode节点,就对这个节点加锁,这样就保证了线程安全。
给定一个key来确定value的时候,必须满足两个条件 key相同 hash值相同,对于节点可能在链表或树上的情况,需要分别去查找。