ConcurrentHashMap的线程安全处理真的能应对所有的线程安全,数据一致性的需求场景吗?如果ConcurrentHashMap解决不了的并发业务场景,就要用Hashtable来实现吗?

这个问题可以当做是并发集合这一类问题的代表,所以其他类似的集合也一并提一下:

线程安全集合类可以分为三大类:

  • 遗留的线程安全集合如 Hashtable , Vector
  • 使用 Collections 装饰的线程安全集合,如:
    • Collections.synchronizedCollection
    • Collections.synchronizedList
    • Collections.synchronizedMap
    • Collections.synchronizedSet
    • Collections.synchronizedNavigableMap
    • Collections.synchronizedNavigableSet
    • Collections.synchronizedSortedMap
    • Collections.synchronizedSortedSet
  • java.util.concurrent.*

java.util.concurrent.* 下的线程安全集合类,可以发现它们有规律,里面包含三类关键词:

Blocking、CopyOnWrite、Concurrent

  • Blocking 大部分实现基于锁,并提供用来阻塞的方法
  • CopyOnWrite 之类容器修改开销相对较重
  • Concurrent 类型的容器
    • 内部很多操作使用 cas 优化,一般可以提供较高吞吐量
    • 弱一致性遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的
      • 求大小弱一致性,size 操作未必是 100% 准确
      • 读取弱一致性

我们都知道Hashtable是绝对线程安全的,同样绝对线程安全的Map还有Collections.synchronizedMap,只不过Collections.synchronizedMap是一个装饰器的存在,什么样的Map都能被包装成线程安全的Map,那为什么还要Hashtable呢?因为hashtable是从JDK1.0就开始存在的,而HashMap和Collections.synchronizedMap是从JDK1.2开始的。虽然没什么人用vector,但是Java一直保留Vector和Hashtable,就是因为Vector 是从JDK 1.0开始的,JDK要向下兼容,而且需要避免有些老旧项目出问题。

ConcurrentHashMap,从名字上看就是并发场景用的HashMap,它突出强调的是高并发,并不是什么线程安全,数据一致性和高并发的高吞吐率是矛盾的,ConcurrentHashMap强调的高并发的高吞吐,但是既然是高并发,肯定还是要注意并发的线程安全问题,它几乎将一致性和高并发做到了最佳平衡:

  1. 只不过将线程安全性的范围缩减至最小(临界代码的范围),才能将高并发的高性能尽可能的发挥到最大。
  2. 同时,将临界代码缩减到最小,也使得我们的做高并发场景的数据一致性处理的灵活性,展现到最大。

        因为我们的业务场景是多变的,可以说有千千万万,线程安全的范围大小的要求肯定也是不尽相同,倘若我某个业务场景,要求数据的强一致性,但是这个需求也不能只有put方法做互斥处理,那就是需要我们自己在对ConcurrentHashMap读写,get和put,做整体线程安全处理(为什么不用hashMap做原子性处理?因为可能同一个业务中的另一个方法共用此Map,但是只需要put方法做互斥,防止扩容出现线程不安全等,后面会重点复现该场景)。那如果不管业务的具体需求,直接就用一个绝对线程安全的Hashtable,想想会发生什么,明明共用该集合的另一个方法只需要做弱数据一致性操作,或者直接就只需要做get操作,却被Hashtable的get方法的互斥处理,导致这业务中两个方法不能并行执行,因为互斥被阻塞,那么肯定不符合高吞吐率的设计要求。就如同我只要鸡翅搭配点薯条就够了,你偏要绑定销售,只肯卖全家桶给我。编程也如同搭积木,肯定是零件的粒度越小,搭造出的模型才能更具有多样性,搭建的工作也更具备灵活性,否则想要的那种模型构造建造做不出,搭出的模型不都一个样吗?

        所以很多人一提到ConcurrentHashMap就说它是线程安全的Map集合,虽然也没毛病,因为大部分的先获取再修改的逻辑,也有线程安全的computeIfAbsent,putIfAbsent来实现,但其实还是不太了解它的设计目的,因为它的线程安全范围真的太小了,想要依靠JDK自带的一个集合就可以解决实际应用中复杂多变并发的线程安全和高吞吐量的需求,是很不现实的。捋清楚它的设计目的和思想,那对它的源码解析肯定就好理解一些了。

OK那开始源码解读吧。

static final int spread(int h) {
    // 无符号右移16位,因为后面获取hash表index的时候,前期hash表的长度-1后,二进制非0位都在低位,那么key.hashCode()的值的高位就没有用了,
    // 高位的hashcode,和低位hashcode明明差距很大,但是因为低位相同,使得它们非常容易碰撞,这里移位就是让hashcode的高16位和低16位都参与到计算来,增加差异化
    // 充分散列hash值,最大可能避免碰撞。
    // key.hashCode(),一次散列,spread二次散列,然后扩容机制是扩容2倍,使得length() -1 的二进制始终是1111...,再与hash & 碰撞的可能就更小,所以是做了3次散列均匀分布处理
    return (h ^ (h >>> 16)) & HASH_BITS;
}
    //table初始化   
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        //将table赋值给tab,同时循环判断table是否为空,不为空说明已经有线程将table初始化了,则直接返回。
        while ((tab = table) == null || tab.length == 0) {
            //将sizeCtl(sizeControl)赋给sc,并判断是否<0,
            if ((sc = sizeCtl) < 0)
                //sc < 0说明有线程正在执行初始化操作,此时让出一下cpu,再次被调度后,继续执行while的判断。相当于等待的过程。
                Thread.yield(); // lost initialization race; just spin
            //如果sc>=0,则说明需要初始化,使用Cas的方式将sizeCtl赋值为-1,这样其他线程进来时就会走到上面的if中去。
            //根据返回值判断是否赋值成功,不成功的话,直接进行下一次循环,不成功的情况说明可能其他线程已经在初始化了。
            //Unsafe.compareAndSwapInt解释:
            //public final native bolean compareAndSwapInt(Object o, long offset, int expected, int x);
            //读取传入对象o在内存中偏移量为offset位置的值与期望值expected作比较,相等就把x值赋值给offset位置的值。方法返回true。不相等,就取消赋值,方法返回false。
            //具体到下面的if判断就是:
            //  检查ConcurrentHashMap对象在内存中偏移量为SIZECTL位置的int值(即为sizeCtl)与sc进行比较,相同就赋值为-1并返回true,不相等则取消赋值并返回false。
            //  SIZECTL是一个static final的常量,代表在当前ConcurrentHashMap对象中,sizeCtl变量在内存中的偏移量,private static final long SIZECTL;
            //  详见ConcurrentHashMap代码最后的static代码块
            //          U = sun.misc.Unsafe.getUnsafe();
            //            Class<?> k = ConcurrentHashMap.class;
            //            SIZECTL = U.objectFieldOffset(k.getDeclaredField("sizeCtl"));
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                //赋值成功后进入
                //此处体现sizeCtl的一个含义,即sizeCtl = -1,说明正在有线程对table进行初始化。
                try {
                    //再次赋值并判断tab是否为空,双重检查
                    //防止当前线程在执行上面的if和else if判断期间,有其他线程已经完成Tab的初始化
                    if ((tab = table) == null || tab.length == 0) {
                        //如果走到这,说明没有其他线程在对tab进行初始化,且在当前线程初始化完毕之前,不会有其他线程进来(通过sc < 0、U.compareAndSwapInt(this, SIZECTL, sc, -1)和双重检查实现)
                        //此时sc可能>0,也可能=0,大于0则n赋值为sc,等于0则n赋值为table默认初始大小DEFAULT_CAPACITY=16。
                        //sc > 0的情况,是调用构造方法时传入了tab的大小。
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        
                        //创建一个大小为n的Node<K,V>数组
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        //新数组赋值给tab和table
                        table = tab = nt;
                        // sc 赋值为n*0.75。
                        // n>>>2 n无符号右移2位为原来的1/4(0.25)
                        // n减掉n的1/4则为n*0.75,0.75为扩容因子。
                        sc = n - (n >>> 2);
                    }
                } finally {
                    //sc赋值给sizeCtl
                    //此处天sizeCtl的一个含义,即数组扩容的阈值。
                    sizeCtl = sc;
                }
                //不管当前线程有没有将table初始化,走到这里说明table已经被初始化完成了,可以跳出循环了
                break;
            }
        }
        return tab;
    }
    //帮助扩容
    final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        //判断oldtable(即为传入的tab) 不为空 , 传入的当前f节点是ForwardingNode 
        //而且nextTab = f.nextTable 不为空,为空说明尚没有线程对老表进行扩容,扩容迁移期间,老表已经迁移完的节点会置为ForwardingNode,
        //ForwardingNode的nextTable属性为迁移后的新表,这是为了在迁移过程中,如果有其他线程访问老表已经迁移完的元素查数据,可以通过nextTable查询。
        //满足以上条件,额进入if开始扩容
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            //仍然是先根据老表的长度计算扩容标识戳
            int rs = resizeStamp(tab.length);
            //nextTab == nextTable && table == tab  确认新还是扩容的新表,老表还是被扩容的老表。
            //(sc = sizeCtl) < 0 判断当前是否仍在扩容过程中 sizeCtl = -1怎么办?
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                // (sc >>> RESIZE_STAMP_SHIFT) != rs  判断是否是当前表的扩容标识戳,建addCount方法
                //  sc == rs + 1 || sc == rs + MAX_RESIZERS 判断是否已经所有线程退出扩容或达到扩容线程数上线
                //                                          同addCount方法一样,这个地方同样有存在bug,
                //                                          应该是应该是sc == (rs << RESIZE_STAMP_SHIFT) + 1 || sc == (rs << RESIZE_STAMP_SHIFT) + MAX_RESIZERS 
                //  transferIndex <= 0 transferIndex的含义是,当前最小的已被分配给线程的数组元素下标,因为数组是从高到底迁移的,因此这个变量可以代表多线程迁移数组的进度。
                //                     小于等于0表示,当前数组的所有元素都已经分配给线程处理,当前线程不需要再帮助扩容了。
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
              
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

砥砺code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值