浅析ConcurrentHashMap(JDK1.8)

1、前言

我们都知道HashMap是线程不安全的,主要体现在put和resize的时候。
使HashMap安全,经历了HashTable,Jdk1.7 ConcurrentHashMap和Jdk1.8的ConcurrentHashMap。那么他们的区别在哪里呢?

  • HashTable:在通过使用大量的synchronized来保证线程的同步,但这样就造成了并发低,且线程上下文的切换导致性能不太高。所以现在几乎不用HashTable了。

  • ConcurrentHashMap(JDK1.7):这个版本的ConcurrentHashMap采用了Segment的形式,什么意思呢,就是给数组分段,然后给每段加锁,其本质就是通过改变锁的粒度来提高并发的性能,其思想有点类似于数据库表中的表锁和行锁。

  • ConcurrentHashMap(JDK1.8):这个版本的ConcurrentHashMap发生的变化很大,主要是是采用了CAS+volatile和少量的Synchronized来保证线程的安全.(CAS即compare and swap,是JDK提供的通过硬件保证的原子性操作,而volatile是java的关键字是一种弱的轻量级同步形式,保证了内存的可见性但是没有保证原子性。所以一般是CAS+volatile一起合作保证线程安全,这个组合在很多地方都有使用,如JUC中的LOCK)

2、源码分析

讲真,JDK1.8版本的ConCurrentHashMap的确是比较复杂的,读这个源码之前最好先把HashMap的源码读懂然后了解一下cas和volatile
虽然很复杂,但读懂后还是挺有成就感的。

  • 2.1 先从putVal()方法开始吧
final V putVal(K key, V value, boolean onlyIfAbsent) {
       if (key == null || value == null) throw new NullPointerException();
       int hash = spread(key.hashCode());//得到hash值
       int binCount = 0;
       for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
           ConcurrentHashMap.Node<K,V> f; int n, i, fh;
           if (tab == null || (n = tab.length) == 0)
               tab = initTable();//判断数组是否为空,为空的话进行初始化。
           else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//定位到数组中,如果数组对应的位置为空
               if (casTabAt(tab, i, null,
                       new ConcurrentHashMap.Node<K,V>(hash, key, value, null)))//使用CAS操作插入,然后break;
                   break;                   // no lock when adding to empty bin
           }
           else if ((fh = f.hash) == MOVED)//后面讲
               tab = helpTransfer(tab, f);
           else {//如果不为空,则需要插入到节点后面
               V oldVal = null;
               synchronized (f) {//进入同步块中
                   if (tabAt(tab, i) == f) {
                       if (fh >= 0) {//fh>0表示结构为链表
                           binCount = 1;//记录链表的长度
                           for (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {//遍历链表
                               K ek;
                               //如果key相等,则进行覆盖
                               if (e.hash == hash &&
                                       ((ek = e.key) == key ||
                                               (ek != null && key.equals(ek)))) {
                                   oldVal = e.val;
                                   if (!onlyIfAbsent)
                                       e.val = value;
                                   break;
                               }
                               ConcurrentHashMap.Node<K,V> pred = e;
                               //插到尾部
                               if ((e = e.next) == null) {
                                   pred.next = new ConcurrentHashMap.Node<K,V>(hash, key,
                                           value, null);
                                   break;
                               }
                           }
                       }
                       else if (f instanceof ConcurrentHashMap.TreeBin) {//否则为红黑树,调用红黑树的插入方法
                           ConcurrentHashMap.Node<K,V> p;
                           binCount = 2;
                           if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
                                   value)) != null) {
                               oldVal = p.val;
                               if (!onlyIfAbsent)
                                   p.val = value;
                           }
                       }
                   }
               }
               if (binCount != 0) {
                   //节点数大于8则调用treeifybin(),这里跟HashMap不一样,具体可以看这个方法
                   //只有当数组长度大于64的时候才会将链表转换为红黑树,否则就扩容。
                   if (binCount >= TREEIFY_THRESHOLD)
                       treeifyBin(tab, i);
                   if (oldVal != null)
                       return oldVal;
                   break;
               }
           }
       }
       addCount(1L, binCount);
       return null;
   }
  • 2.2 初始化方法
  • 先看一个变量
/**
    * Table initialization and resizing control.  When negative, the
    * table is being initialized or resized: -1 for initialization,
    * else -(1 + the number of active resizing threads).  Otherwise,
    * when table is null, holds the initial table size to use upon
    * creation, or 0 for default. After initialization, holds the
    * next element count value upon which to resize the table.
    */
   private transient volatile int sizeCtl;

通过阅读注释我们可以了解到,这个变量其实有两个作用

  • 第一个作用:是用来判断table的状态的,如果sizeCtl<0的话,表示这个tbale在经历初始化或者扩容的过程。

  • 第二个作用:After initialization, holds the next element count value upon which to resize the table.初始化后,根据这个值来调整表的大小,其实就是用来判断table是否需要扩容,还记得HashMap里面的0.75*16=12吗,就是这个意思。后面会用到

  • initTable()方法

private final ConcurrentHashMap.Node<K,V>[] initTable() {
       ConcurrentHashMap.Node<K,V>[] tab; int sc;
       while ((tab = table) == null || tab.length == 0) {
           if ((sc = sizeCtl) < 0)//如果sizeCtl小于0则代表当前有其他线程正在初始化或者扩容,这个时候放弃cpu时间片
               Thread.yield(); 
           //这里通过CAS操作让sizeCtl置为-1,(有的人看不懂的话,下面有详细讲解哦~)
           else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
               try {
                 //然后就是初始化table数组了,初始容量为16,
                   if ((tab = table) == null || tab.length == 0) {
                       int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                       @SuppressWarnings("unchecked")
                       ConcurrentHashMap.Node<K,V>[] nt = (ConcurrentHashMap.Node<K,V>[])new ConcurrentHashMap.Node<?,?>[n];
                       table = tab = nt;
                       //这里sc=16-4=12
                       sc = n - (n >>> 2);
                   }
               } finally {
                 //这里sizeCtl就等于12啦
                   sizeCtl = sc;
               }
               break;
           }
       }
       return tab;
   }
  • U.compareAndSwapInt(this, SIZECTL, sc, -1)CAS方法一共有四个参数,分别是对象,对象属性在对象中的偏移量,期望值,更改之后的值。

  • 然后我们通过源码来看SIZECTL就懂了。

 ...
 private static final long SIZECTL;
 ...
 static {
      try {
          U = sun.misc.Unsafe.getUnsafe();
          Class<?> k = ConcurrentHashMap.class;
          SIZECTL = U.objectFieldOffset
              (k.getDeclaredField("sizeCtl"));//得到sizeCtl在对象中的偏移量
         ...
      } catch (Exception e) {
          throw new Error(e);
      }
  }
  • 好吧,这行代码的意思就是在通过CAS操作将对象的sizeCtl属性置为-1,就是告诉其他线程,本线程正在初始化,其他线程只有执行该操作的话只有暂时释放自己的时间片
  • treeifyBin()方法(将链表转化为红黑树)
private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
          //看到没有,当数组长度小于64的时候就会先进行扩容
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
        
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) {
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null;
                          //遍历链表,转化为红黑树
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        //将红黑树的头结点插入的数组中
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }
  • tryPresize()方法(扩容)
private final void tryPresize(int size) {
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            //这里和initTable是一样的
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c;
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            //未达到扩容要求
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            else if (tab == table) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    Node<K,V>[] nt;
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                        //这里不懂为何是加一
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    //数据迁移(这个方法的代码有些复杂,暂时先不分析了)
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
            }
        }
    }
  • get()
public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
               // 判断头结点是否就是我们需要的节点
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            //遍历链表
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

暂时先分析到这里吧,后面再来修改。有问题的话欢迎批评指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值