Java并发编程之ConcurrentHashMap

什么是ConcurrentHashMap

我们知道,HashMap是线程不安全的。为解决HashMap在高并发环境下不能使用的问题,ConcurrentHashMap诞生了,实际上,我们可以认为ConcurrentHashMap是HashMap的线程安全版本。

为什么不使用HashTable

实际上,HashTable和HashMap的实现原理几乎一样,只是HashTable不允许key和value为null,而且它也是HashMap的线程安全版本。但,它为了线程安全付出的代价太大了!从源码可知,HashTable的get/put所有相关操作都是synchronized的,这相当于给哈希表加了一把大锁。多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。

ConcurrentHashMap的前世今生

ConcurrentHashMap与HashTable最大的区别,就是ConcurrentHashMap采用局部加锁技术,而HashTable使用了全局加锁技术。在jdk1.7中是对Segment加锁,而在jdk1.8是对每个数组元素加锁。

jdk1.7

在jdk1.7中,ConcurrentHashMap使用了分段锁的方式来确保线程安全。

ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。

ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。
在这里插入图片描述

该实现方式的优势为写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,但,ConcurrentHashMap的hash过程显然比普通哈希表要长,定位一个元素需要经历两次hash操作,第一次定位到Segment,第二次定位到链表头部。

jdk1.8

我在之前的博客介绍过HashMap的底层数据结构在jdk1.8已经更改为数组+链表+红黑树,ConcurrentHashMap参考了HashMap的实现,其数据结构与HashMap大体相同。但是ConcurrentHashMap的jdk1.8版本相比jdk1.7已经取消了分段锁的数据结构,并根据CAS+Synchronized来实现线程安全。

ConcurrentHashMap的继承关系
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {}
ConcurrentHashMap的部分属性
	// 表的最大容量
    private static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认表的大小
    private static final int DEFAULT_CAPACITY = 16;
    // 默认并发数
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
    // 装载因子
    private static final float LOAD_FACTOR = 0.75f;
    // 转化为红黑树的表的最小容量
    static final int MIN_TREEIFY_CAPACITY = 64;
    
	// 表
    transient volatile Node<K,V>[] table;
    // 下一个表
    private transient volatile Node<K,V>[] nextTable;
    // 对表初始化和扩容控制
    private transient volatile int sizeCtl;

我们重点介绍一下sizeCtl,它用于table[]的初始化和扩容操作,不同值的代表状态如下

  • -1:table[]正在初始化
  • -N:表示有N-1个线程正在进行扩容操作
  • 非负值:如果table[]未初始化,则表示table需要初始化的大小,如果初始化完成,则表示table[]扩容的阀值,默认是table[]容量的0.75倍
ConcurrentHashMap的构造函数
无参构造函数
public ConcurrentHashMap() {}

创建一个带有默认初始容量 (16)、加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射

public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel)
	public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) // 合法性判断
            throw new IllegalArgumentException();
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }

创建一个带有指定初始容量、加载因子和并发级别的新的空映射。注意,在JDK1.8中的并发控制都是针对具体的桶而言,即有多少个桶就可以允许多少个并发数。

put操作
    public V put(K key, V value) {
        return putVal(key, value, false);
    }
 
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException(); // 键或值为空,抛出异常
        // 键的hash值经过计算获得hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) { // 无限循环
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0) // 表为空或者表的长度为0
                // 初始化表
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 表不为空并且表的长度大于0,并且该桶不为空
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null))) // 比较并且交换值,如tab的第i项为空则用新生成的node替换
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED) // 该结点的hash值为MOVED
                // 进行结点的转移(在扩容的过程中)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) { // 加锁同步
                    if (tabAt(tab, i) == f) { // 找到table表下标为i的节点
                        if (fh >= 0) { // 该table表中该结点的hash值大于0
                            // binCount赋值为1
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) { // 无限循环
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) { // 结点的hash值相等并且key也相等
                                    // 保存该结点的val值
                                    oldVal = e.val;
                                    if (!onlyIfAbsent) // 进行判断
                                        // 将指定的value保存至结点,即进行了结点值的更新
                                        e.val = value;
                                    break;
                                }
                                // 保存当前结点
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) { // 当前结点的下一个结点为空,即为最后一个结点
                                    // 新生一个结点并且赋值给next域
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    // 退出循环
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) { // 结点为红黑树结点类型
                            Node<K,V> p;
                            // binCount赋值为2
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) { // 将hash、key、value放入红黑树
                                // 保存结点的val
                                oldVal = p.val;
                                if (!onlyIfAbsent) // 判断
                                    // 赋值结点value值
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) { // binCount不为0
                    if (binCount >= TREEIFY_THRESHOLD) // 如果binCount大于等于转化为红黑树的阈值
                        // 进行转化
                        treeifyBin(tab, i);
                    if (oldVal != null) // 旧值不为空
                        // 返回旧值
                        return oldVal;
                    break;
                }
            }
        }
        // 增加binCount的数量
        addCount(1L, binCount);
        return null;
    }

步骤大概如下

  1. 参数校验
  2. 若table[]未创建,则初始化
  3. 当table[i]后面无节点时,直接创建Node(无锁操作)
  4. 如果当前正在扩容,则帮助扩容并返回最新table[]
  5. 然后在链表或者红黑树中追加节点
  6. 最后还回去判断是否到达阀值,如到达变为红黑树结构

我们看到,代码中加锁片段用的是synchronized关键字,而不是像jdk1.7中的ReentrantLock。这一点也说明了,synchronized在新版本的JDK中优化的程度和ReentrantLock差不多了。

我们有必要了解一下initTable这个方法,用于初始化表

	private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) { // 无限循环
            if ((sc = sizeCtl) < 0) // sizeCtl小于0,则进行线程让步等待
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 比较sizeCtl的值与sc是否相等,相等则用-1替换
                try {
                    if ((tab = table) == null || tab.length == 0) { // table表为空或者大小为0
                        // sc的值是否大于0,若是,则n为sc,否则,n为默认初始容量
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        // 新生结点数组
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        // 赋值给table
                        table = tab = nt;
                        // sc为n * 3/4
                        sc = n - (n >>> 2);
                    }
                } finally {
                    // 设置sizeCtl的值
                    sizeCtl = sc;
                }
                break;
            }
        }
        // 返回table表
        return tab;
    }
get操作
	public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        // 计算key的hash值
        int h = spread(key.hashCode()); 
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) { // 表不为空并且表的长度大于0并且key所在的桶不为空
            if ((eh = e.hash) == h) { // 表中的元素的hash值与key的hash值相等
                if ((ek = e.key) == key || (ek != null && key.equals(ek))) // 键相等
                    // 返回值
                    return e.val;
            }
            else if (eh < 0) // 结点hash值小于0
                // 在桶(链表/红黑树)中查找
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) { // 对于结点hash值大于0的情况
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

get函数根据key的hash值来计算在哪个桶中,再遍历桶,查找元素,若找到则返回该结点,否则,返回null。

对ConcurrentHashMap的总结

ConcurrentHashMap是线程安全的,与同样线程安全的HashTable相比,采用了局部加锁技术,不同的桶之间的操作不会相互影响,可以并发执行,效率更高,所以在高并发环境下更推荐ConcurrentHashMap。

参考:高并发编程系列:ConcurrentHashMap的实现原理(JDK1.7和JDK1.8)
ConcurrentHashMap的JDK1.8实现
【JUC】JDK1.8源码分析之ConcurrentHashMap(一)

推荐阅读博客:探索jdk8之ConcurrentHashMap的实现机制

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值