Java 集合框架(七):ConcurrentHashMap

luohui 2020-01-12

ConcurrentHashMap

ConcurrentHashMap 是一个能够保证线程安全的并发容器

为什么使用concurrentHashMap

HashTable 是一个典型的同步容器。虽然 HashTable 的所有方法都用 synchronzied 修饰,但是如果我们编程时将 get 和 put 这类的操作写成了非原子操作,就会有线程安全问题了。虽然单个方法都是线程安全的,但是组合到一起就不是了。所以只能叫同步容器。另外使用 synchronized 锁使得 HashTable 的效率比较低。

Collections.synchronizedMap()方法同理。

ConcurrentHashMap 在 JDK 1.7 中采用分段锁计数,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 都需要锁。JDK 1.8 弃用了分段锁,采用 CAS + synchronized 来保证线程安全,也可以叫节点锁

ConcurrentHashMap 为什么高效

HashTable 低效的原因就是因为所有访问 HashTable 的线程都争夺一把锁。如果容器有很多把锁,每次只锁住容器的一部分数据。当多个线程访问容器里面的不同部分时线程就不存在锁的竞争,这样可以提高访问效率。1.7 中锁住的力度是分段,1.8 中锁住的力度是节点

先来看 JDK 1.7 的原理。

JDK 1.7 ConcurrentHashMap 的结构:

Java 集合框架(七):ConcurrentHashMap

  1. ConcurrentHashMap 由 Segment 和 HashEntry 组成。
  2. Segment 继承自 ReentrantLock。
  3. HashEntry 用来存储键值对。
  4. ConcurrentHashMap 包含了一个 Segment 数组,每个 Segment 包含一个 HashEntry 数组并且守护它。当修改 HashEntry 数组数据时,需要先获得 Segment 锁。每个 HashEntry 元素又是链表结构元素。

Java 集合框架(七):ConcurrentHashMap

get 方法

Java 集合框架(七):ConcurrentHashMap

  1. 根据 key,计算出 hashcode。
  2. 根据 hashCode 定位 Segment,如果 Segment 不为空,Segment 里面的 table 也不为空,对 Segment 里面的 Entry 进行遍历,如果 key 存在,返回 key 对应的 value。
  3. 否则返回 null。

整个 get 操作不需要加锁。那么它时如何保证读操作线程安全的那,原因是所有 Entry 都用 volatile 修饰了,可以保证线程之间的可见性,这也是 volatile 替换锁的经典应用场景。

Java 集合框架(七):ConcurrentHashMap

put 方法

Java 集合框架(七):ConcurrentHashMap

  1. 计算 key 的 hashcode。
  2. 根据 hashcode 计算出 Segment。
  3. 调用 Segment 的 put 方法。

Java 集合框架(七):ConcurrentHashMap

  1. 获取锁,保证 put 线程安全。
  2. 定位到具体的 HashEntry。
  3. 遍历 HashEntry 链表,查看 key 是否存在,存在则更新,不存在则插入。
  4. 释放锁。

JDK 1.8 的 ConcurrentHashMap

/**
     * 存放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节点,就对这个节点加锁,这样就保证了线程安全。

get方法

给定一个key来确定value的时候,必须满足两个条件 key相同 hash值相同,对于节点可能在链表或树上的情况,需要分别去查找。

相关推荐