ConcurrentHashMap

发布时间 2023-08-14 20:46:00作者: _Explosion!

ConcurrentHashMap是JUC包中提供的一个并发集合类,是线程安全且高效的HashMap(该文章有对HashMap的详细介绍,结构方面二者相似)实现。

之所以引入ConcurrentHashMap,是因为

  • HashMap线程不安全:在多线程环境下,使用HashMap的put操作会引起死循环,原因是多线程会导致HashMap的Entry链表形成环形数据结构,导致Entry的next节点永远不为空,就会产生死循环获取Entry。(HashMap的key,value均可为null,其他两个不行。)
  • HashTable效率低下:HashTable容器使用sychronized来保证线程安全,采取锁住整个表结构来达到同步目的,在线程竞争激烈的情况下,当一个线程访问HashTable的同步方法,其他线程也访问同步方法时,会进入阻塞或轮询状态;如线程1使用put方法时,其他线程既不能使用put方法,也不能使用get方法,效率非常低下。
  • ConcurrentHashMap的锁分段技术可提升并发访问效率:首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

以下介绍JDK8中的ConcurrentHashMap

结构

和 1.8 HashMap 结构类似,当链表节点数超过指定阈值的话,也是会转换成红黑树的,大体结构也是一样的。即先是链表,后为红黑树

put:

  1. 判断Node[]数组是否初始化,没有则进行初始化操作
  2. 通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败则进入下次循环。
  3. 检查到内部正在扩容,就帮助它一块扩容。
  4. 如果f!=null(链表/红黑树的头元素),则使用synchronized锁住f元素。如果是Node(链表结构)则执行链表的添加操作;如果是TreeNode(树型结构)则执行树添加操作。
  5. 判断链表长度已经达到临界值(默认值),当节点超过这个值就需要把链表转换为树结构。
  6. 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容

扩容

通过判断该节点的hash值是不是等于-1(MOVED),代码为(fh = f.hash) == MOVED,说明 Map 正在扩容。那么就帮助 Map 进行扩容。以加快速度。这里重点是多线程并发扩容,可以大大加快扩容速度。

关键方法

ConcurrentHashMap支持Java8中的lambda表达式,对代码进行了简化。

computeIfAbsent:如果key不存在,则调用后面的函数式接口计算,把计算后的val作为值。

 

public class ConcurrentHashMapDemo {

    private static final ConcurrentHashMap<String, Integer> USER_ACCESS_COUNT = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        //如果key不存在,则调用后面的函数式接口计算,把计算后的val作为值
        USER_ACCESS_COUNT.computeIfAbsent("cc", k -> 1);
        System.out.println(USER_ACCESS_COUNT.get("cc"));

    }
}

 

computeIfPresent:如果key存在则修改并返回value,如果不存在则返回null。

public class ConcurrentHashMapDemo {

    private static final ConcurrentHashMap<String, Integer> USER_ACCESS_COUNT = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        //如果key存在则修改,如果不存在则返回null
        System.out.println(USER_ACCESS_COUNT.computeIfPresent("cc", (k, v) -> v + 1));
    }
}

merge(合并数据)

如果指定的键尚未与(非空)值相关联,则将其与给定值相关联。

  • key - 与指定值关联的键

  • value - 如果不存在则使用的值

  • remappingFunction - 重新计算值(如果存在)的函数

public class ConcurrentHashMapDemo {

    private static final ConcurrentHashMap<String, Integer> USER_ACCESS_COUNT = new ConcurrentHashMap<>();

    //merge
    public static void main(String[] args) {
        ConcurrentHashMap<Integer, Integer> concurrentHashMap = new ConcurrentHashMap();
        Stream.of(1, 2, 3, 4, 6, 2, 3, 6, 8, 1).forEach(v -> {
            concurrentHashMap.merge(v, 5, Integer::sum);
        });
        System.out.println(concurrentHashMap);
    }
}

//输出结果:
{1=10, 2=10, 3=10, 4=5, 6=10, 8=5}

在JDK1.7和JDK1.8中的区别:

在JDK1.8主要设计上的改进有以下几点:

1、不采用segment而采用node,锁住node来实现减小锁粒度。
2、设计了MOVED状态 当resize的中过程中 线程2还在put数据,线程2会帮助resize。
3、使用3个CAS操作来确保node的一些操作的原子性,这种方式代替了锁。
4、sizeCtl的不同值来代表不同含义,起到了控制的作用。
采用synchronized而不是ReentrantLock:

1. 减少内存开销
假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承AQS来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。
2. 获得JVM的支持
可重入锁毕竟是API这个级别的,后续的性能优化空间很小。
synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。这就使得synchronized能够随着JDK版本的升级而不改动代码的前提下获得性能上的提升。

 

 

参考文章:https://blog.csdn.net/mrlin6688/article/details/104646661

     https://blog.csdn.net/qq_41432730/article/details/121318157