011Java并发包007线程安全的Map相关类

本文详细介绍了Java并发包中的ConcurrentHashMap,从JDK1.7的分段锁机制到JDK1.8的改进。ConcurrentHashMap通过CAS自旋锁、volatile和synchronized实现线程安全,比Hashtable效率更高。文章还涵盖了ConcurrentHashMap的重要属性如sizeCtl,构造方法,添加、删除、获取和扩容方法,以及与Hashtable和Collections.synchronizedMap()的区别。
摘要由CSDN通过智能技术生成

部分内容来自以下博客:

https://blog.csdn.net/bill_xiang_/article/details/81122044

https://www.cnblogs.com/zhaojj/p/8942647.html

注意:本文基于JDK1.8进行记录。

1 分类

参照之前在学习集合时候的分类,可以将JUC下有关Map相关的类进行分类。

ConcurrentHashMap:继承于AbstractMap类,相当于线程安全的HashMap,是线程安全的哈希表。使用数组加链表加红黑树结构和CAS操作实现。

ConcurrentSkipListMap:继承于AbstractMap类,相当于线程安全的TreeMap,是线程安全的有序的哈希表。通过跳表实现的。

2 ConcurrentHashMap

2.1 JDK1.7的分段锁机制

Hashtable之所以效率低下主要是因为其实现使用了synchronized关键字对put等操作进行加锁,而synchronized关键字加锁是对整个对象进行加锁。也就是说在进行put等修改Hash表的操作时,锁住了整个Hash表,从而使得其表现的效率低下。

因此,在JDK1.5到1.7版本,Java使用了分段锁机制实现ConcurrentHashMap。

简而言之,ConcurrentHashMap在对象中保存了一个Segment数组,即将整个Hash表划分为多个分段。而每个Segment节点,即每个分段则类似于一个Hashtable。在执行put操作时首先根据hash算法定位到节点属于哪个Segment,然后使用ReentrantLock对该Segment加锁即可。因此,ConcurrentHashMap在多线程并发编程中可是实现多线程put操作。

Segment类是ConcurrentHashMap中的内部类,继承于ReentrantLock类。ConcurrentHashMap与Segment是组合关系,一个ConcurrentHashMap对象包含若干个Segment对象,ConcurrentHashMap类中存在Segment数组成员。

HashEntry也是ConcurrentHashMap的内部类,是单向链表节点,存储着key-value键值对。Segment与HashEntry是组合关系,Segment类中存在HashEntry数组成员,HashEntry数组中的每个HashEntry就是一个单向链表。

2.2 JDK1.8的改进

在JDK1.7的版本,ConcurrentHashMap是通过分段锁机制来实现的,所以其最大并发度受Segment的个数限制。因此,在JDK1.8中,ConcurrentHashMap的实现原理摒弃了这种设计,而是选择了与HashMap类似的数组加链表加红黑树的方式实现,而加锁则采用CAS自旋锁、volatile关键字、synchronized可重入锁、热点分段锁实现。

JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)。

JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了。

JDK1.8版本的扩容操作支持多线程并发。在之前的版本中如果Segment正在进行扩容操作,其他写线程都会被阻塞,JDK1.8改为一个写线程触发了扩容操作,其他写线程进行写入操作时,可以帮助它来完成扩容这个耗时的操作。

JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表。

2.3 重要属性

2.3.1 sizeCtl属性

标志控制符。这个参数非常重要,出现在ConcurrentHashMap的各个阶段,不同的值也表示不同情况和不同功能:

1)负数,表示正在进行初始化或扩容操作。-1表示正在进行初始化操作。-N表示正在进行扩容操作,高16位是扩容标识,与数组长度有关,最高位固定为1,低16为表示扩容线程数加1。

2)0,表示数组还未初始化。

3)正数,表示下一次进行扩容的大小,类似于扩容阈值。它的值始终是当前容量的0.75倍,如果数组节点个数大于等于sizeCtl,则进行扩容。

2.3.2 内部类Node中的hash属性

1)负数,-1表示该节点为转发节点,-2表示该节点为红黑树节点。

2)正数,表示根据key计算得到的hash值。

2.4 构造方法

需要说明的是,在构造方法里并没有对集合进行初始化操作,而是等到了添加节点的时候才进行初始化,属于懒汉式的加载方式。

另外,loadFactor参数在JDK1.8中不再有加载因子的意义,仅为了兼容以前的版本,加载因子默认为0.75并通过移位运算计算,不支持修改。

同样,concurrencyLevel参数在JDK1.8中不再有多线程运行的并发度的意义,仅为了兼容以前的版本。

// 空参构造器。
public ConcurrentHashMap() {
}

// 指定初始容量的构造器。
public ConcurrentHashMap(int initialCapacity) {
    // 参数有效性判断。
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    // 提供多余空间,避免初始化后马上扩容,防止初始容量为0,保证最小容量为2。
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
            MAXIMUM_CAPACITY :
            tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    // 设置标志控制符。
    this.sizeCtl = cap;
}

// 指定初始容量,加载因子的构造器。
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, 1);
}

// 指定初始容量,加载因子,并发度的构造器。
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
    // 参数有效性判断。
    if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    // 比较初始容量和并发度的大小,取最大值作为初始容量。
    if (initialCapacity < concurrencyLevel)
        initialCapacity = concurrencyLevel;
    // 计算最大容量。
    long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    int cap = (size >= (long)MAXIMUM_CAPACITY) ?
        MAXIMUM_CAPACITY : tableSizeFor((int)size);
    // 设置标志控制符。
    this.sizeCtl = cap;
}

// 包含指定Map集合的构造器。
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    // 设置标志控制符。
    this.sizeCtl = DEFAULT_CAPACITY;
    // 放置指定的集合。
    putAll(m);
}

2.5 初始化方法

集合并不会在构造方法里进行初始化,而是在用到集合的时候才进行初始化,在初始化的同时会设置集合的阈值sizeCtl。

在初始化的过程中为了保证线程安全,总共使用了两步操作:

1)通过CAS原子更新方法将sizeCtl设置为-1,保证只有一个线程执行初始化。

2)线程获取初始化权限后内部进行二次判断,保证只有在未初始化的情况下才能执行初始化。

// 初始化集合数组,使用CAS原子更新保证线程安全,使用volatile保证顺序和可见性。
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    // 死循环以完成初始化。
    while ((tab = table) == null || tab.length == 0) {
        // 如果sizeCtl小于0则表示正在初始化,当前线程让出CPU。
        if ((sc = sizeCtl) < 0)
            Thread.yield();
        // 如果需要初始化,并且使用CAS原子更新成功。
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                // 第一个线程初始化之后,第二个线程还会进来所以需要重新判断。类似于线程同步的二次判断。
                if ((tab = table) == null || tab.length == 0) {
                    // 如果没有指定容量则使用默认容量16。
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    // 初始化一个指定容量的节点数组。
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    // 将节点数组指向集合数组。
                    table = tab = nt;
                    // 扩容阀值,获取容量的0.75倍的值,写法更高端比直接乘高效。
                    sc = n - (n >>> 2);
                }
            } finally {
                // 将sizeCtl的值设为阈值。
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

2.6 添加方法

1)校验数据,判断传入一个key和value是否为空,如果为空就直接报错。ConcurrentHashMap是不可为空的(HashMap是可以为空的)。

2)初始化数组,判断数组是否为空,如果为空就执行初始化方法。

3)插入或更新节点,如果数组插入位置的节点为空就通过CAS操作插入节点,如果数组正在扩容就执行协助扩容方法,如果产生哈希碰撞就找到节点并更新节点或者插入节点。

4)插入节点后,链表节点判断是否要转为红黑树节点,并且需要增加容量并判断是否需要扩容。

// 添加节点。
public V put(K key, V value) {
    retur
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值