ConcurrentHashMap为什么是线程安全的
JDK1.7里面,ConcurrentHashMap是用Segment和HashEntry实现的,每个Segment都是继承于Reentrantlock的,在对该segment进行操作时,获取锁,结束操作释放锁。 具体来说,每一个Segment就相当于是一个小的HashTable,内部包含了多个HashEntry。而Segment通过将整个HashTable分成多个小的Segment,并使用ReentrantLock对数据进行同步,来提高并发性。而HashEntry则是哈希表中的一个节点,包含了key、value以及一个next指针,用于形成链表结构。每个HashEntry的next指针指向下一个HashEntry,从而形成了一个单链表
JDK1.8里面,没有用segment,而是用Node+CAS+synchronized实现的. 首先,这段代码通过spread方法计算出key的哈希值,并根据此值找到对应的存储桶。如果该桶为空,就调用casTabAt方法使用CAS操作直接插入新的节点;如果该桶非空,就通过synchronized同步块确保在多线程情况下安全地更新已有的键值对。 在更新已有键值对时,该方法会先检查该节点的哈希值是否与要插入的节点相同,如果是就直接将新值赋给旧值;否则,就遍历链表或树中的所有节点,寻找是否已有相同的key值,如果有,则更新其值;否则,使用CAS操作将新的节点插入到链表或树的头部。同时,该方法还需要考虑到特殊情况如扩容、树化等,并在插入或更新成功后检查是否需要执行扩容操作。
ConcurrentHashMap链表转红黑树的时机
1.当链表长度大于或等于阈值TREEIFY_THRESHOLD(默认为 8)的时候,如果同时还满足容量(数组的长度)大于或等于 MIN_TREEIFY_CAPACITY(默认为 64)的要求,就会把链表转换为红黑树 否则调用resize()方法进行扩容
2.当红黑树中的元素数量小于等于 6 时,树就会被退化成链表
ConcurrentHashMap JDK1.8 put的流程
- 根据 key 计算出 hash 值和数组索引值,获取该位置上的节点(如果该节点不为 null)或锁对象(如果该节点为 null)。
- 如果该节点不为 null,则遍历该节点对应的链表或红黑树,查找是否已经存在 key 值相同的节点,如果存在,则更新其 value 并返回旧的 value;否则将新节点插入到链表或红黑树中。
- 如果该节点为 null,则需要使用 synchronized 等方式获取该数组位置上的锁对象。如果多个线程同时需要获取该锁对象,则会采用自旋方式进行等待,直到某个线程成功获取该锁对象为止。
- 获取锁对象之后,再次判断该节点是否为 null。
- 如果该节点仍为 null,则直接插入新节点。
- 否则,如果该节点为 MOVED 标志,则需要帮助进行扩容操作。此时会调用 helpTransfer 方法来协助扩容。
- 如果该节点已经被删除,则将其替换为新节点。
- 如果该节点的 hash 值等于 MOVED 标志,则表示该节点正在被转移,此时将当前操作加入到转移任务队列中,并阻塞当前线程,等待转移任务完成。
- 如果该节点对应的链表长度大于等于 TREEIFY_THRESHOLD(8),且该节点还没有被转换为红黑树,则将该链表转换为红黑树。
- 如果该节点已经被转换为红黑树,则使用红黑树的操作进行插入。
- 遍历该节点对应的链表或红黑树,查找是否已经存在 key 值相同的节点。如果存在,更新其 value 并返回旧的 value;否则将新节点插入到链表或红黑树中。