ConcurrentHashMap浅析

一、ConcurrentHashMap的线程安全性

ConcurrentHashMap的线程安全性是指:Map的数据结构不会被破坏,比如对具有相同hash值而不相等的key进行put操作的时候,可以保证两个key都被放在链表(红黑二叉树)中。

ConcurrentHashMap的线程安全指的是它的每个方法单独调用(即原子操作)都是线程安全的,但是代码总体的互斥性并不受控制。

比如如下代码,实际上并不是原子操作,它包含了三步:

  1. map.get
  2. 加1
  3. map.put

map.put(KEY, map.get(KEY) + 1)

其中第1和第3步,单独来说都是线程安全的,由ConcurrentHashMap保证。但是由于在上面的代码中,map本身是一个共享变量。当线程A执行map.get的时候,其它线程可能正在执行map.put,这样一来当线程A执行到map.put的时候,线程A的值就已经是脏数据了,然后脏数据覆盖了真值,导致线程不安全

简单地说,ConcurrentHashMap的get方法获取到的是此时的真值,但它并不保证当你调用put方法的时候,当时获取到的值仍然是真值

如下是ConcurrentHashMap线程安全和不安全的实列。注意ThreadPool的awaitTermination的必要性,主线程需要等待线程池中所有的线程都运行完毕后才去拿ConcurrentHashMap中key为“count”的值,这样才能保证拿到的是最终的值。

package com.itheima.security.springboot.map;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class TestConcurrentHashMap {
    private static final ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();

    public static void main(String[] args) throws Exception{
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        //不安全的列子
        /*for (int i = 0; i < 8; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    testUnsafe1();
                }
            });

        }*/


        //安全的列子
        for (int i = 0; i < 8; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    testSafe2();
                }
            });

        }
        
        executorService.shutdown();
        //awaitTermination时,主线程会处于一种等待的状态,等待线程池中所有的线程都运行完毕后才继续运行。
        executorService.awaitTermination(1, TimeUnit.DAYS);
        System.out.println(map.get("count"));

    }

    private static void testUnsafe1() {
        Integer count = map.get("count");
        if (count == null) {
            map.put("count", 1);
        } else {
            map.put("count", count + 1);
        }
    }

    private static void testSafe2() {
        for (; ; ) {
            Integer count = map.get("count");
            if (count == null) {
                //不存在,则那么会向map中添加该键值对,并返回null;如果已经存在,那么不会覆盖已有的值,直接返回已经存在的值。
                if (map.putIfAbsent("count", 1) == null) {
                    break;
                }

            } else {
                if (map.replace("count", count, count + 1)) {
                    break;
                }
            }
        }
    }
}

二、ConcurrentHashMap的putVal方法

ConcurrentHashMap的put方法和putIfAbsent方法都调用了putVal方法,如下:

public V put(K key, V value) {
    return putVal(key, value, false);
}
public V putIfAbsent(K key, V value) {
    return putVal(key, value, true);
}
 /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
		//key和value的值不允许为null
        if (key == null || value == null) throw new NullPointerException();
		//计算出key的hash值
        int hash = spread(key.hashCode());
		//申明出一个标识,暂时不管
        int binCount = 0;
		//自旋,申明tab,为当前ConcurrentHashMap的数组
        for (Node<K,V>[] tab = table;;) {
			//f:当前索引位置的数据(node)
            Node<K,V> f; int n, i, fh;
			//如果数组为null或者数组的长度为0
            if (tab == null || (n = tab.length) == 0)
				//初始化数组
                tab = initTable();
			//数组已经初始化了,将数据插入到map中
			//tabAt(数组,i):获取数组中i索引位置的数据(tab[i])
			//(n - 1) & hash:数组长度-1 & key的hash值,计算出来的结果作为索引,配合tab[i]方法获取这个索引位置的数据
			//==null:说明当前索引位置没有数据,将数据插入到当前位置
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
				//casTabAt(数组,i,2,3):以CAS的方式,将数组tab中i位置的数据由2改为3
				//插入成功返回true,跳出for循环;插入失败返回false
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
			//帮助扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
				//出现了hash冲突,需要将数据挂到链表上或者添加到红黑二叉树中
                V oldVal = null;
				//锁住当前桶的位置(f:当前索引位置的数据(node))
				//f:当前桶的位置的数据(node)
                synchronized (f) {
					//获取数组中i索引位置的数据(tab[i]),判断和锁定的数据(node)是不是同一个
                    if (tabAt(tab, i) == f) {
						//fh当前桶数据的hash值,fh >= 0,当前桶下是链表或者为null;
                        if (fh >= 0) {
							//表示设置为1
                            binCount = 1;
							//把当前桶位置的数据(node):f赋值给e
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
								//把当前桶位置的数据(node)的hash值与key的hash值相等
                                if (e.hash == hash &&
									//是相同的key,则不是添加二十修改操作!!!
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
									//获取当前位置的旧值
                                    oldVal = e.val;
									//是否是putIfAbsent操作?onlyIfAbsent为false:覆盖原有的value;如果为true,什么都不做
                                    if (!onlyIfAbsent)
                                        e.val = value;
									//跳出循环
                                    break;
                                }
                                Node<K,V> pred = e;
								//说明不是修改,而是追加操作
                                if ((e = e.next) == null) {
									//如果next指向是的null,则直接插入到后面
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
						//当前桶下是红黑二叉树;
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
				
                if (binCount != 0) {
					//如果binCount大于等于8,如果大于等于8则需要判断是否需要将链表转换成红黑二叉树
                    if (binCount >= TREEIFY_THRESHOLD)
						//将链表转换成红黑二叉树
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }



三、ConcurrentHashMap的散列算法spread

static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

1.为什么要h >>> 16

h为key的hashCode,为32位,位了尽量避免hash冲突,我们就需要让hashCode的高16位也参与运算,所以对hashCode右移16位(去掉低位,高位变低位,高位补0)。

然后在与原来的hashCode进行异或运算,其结果在与0x7fffffff进行运算

要与0x7fffffff进行运算的原因?是为了保证key的hashcode的值一定为正数,因为key的hashcode值为负数由其他的意义,如下:

static final int MOVED     = -1; // hash for forwarding nodes 数据正在迁移
static final int TREEBIN   = -2; // hash for roots of trees 数据下面是一颗树
static final int RESERVED  = -3; // hash for transient reservations 该位置已经被预定

2.tabAt(tab, i = (n - 1) & hash)

在获取数组中i索引位置数据的时候,使用数组长度-1(n-1)的方式,因为数组长度为2的n次方倍,所以数组长度的二进制数低位一般为0。一个为0的数与其他数(0或1)进行与(&)运算的时候,都为0。所以为了让数据在tab[]中更加的分散,使用(n - 1) & hash方式。

四、initTable()方法

sizeCtl四个值得解释:
-1:表示map正在初始化;
小于-1:表示正在扩容;如-2表示有一个线程正在扩容,-3表示有两个线程正在扩容
0:还没有初始化;
正数:如果没有初始化,代表要初始化得长度;如果已经初始化了,代表扩容得阈值。
//初始化数组
private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
		//判断数组是否已经初始化
        while ((tab = table) == null || tab.length == 0) {
			//sizeCtl赋值给sc,判断是否小于0(正在初始化或者扩容)
            if ((sc = sizeCtl) < 0)
				//礼让一下(让线程由运行状态变成就绪状态,可能又立马获得CPU的时间片)
                Thread.yield(); // lost initialization race; just spin
			//以cas的方式把SIZECTL设置成-1
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
				//CAS成功说明我要开始初始化了
                try {
					//再次做了一次判断:判断数组是否已经初始化
                    if ((tab = table) == null || tab.length == 0) {
						//获取数组初始化的长度;如果sc>0(如果没有初始化,代表要初始化得长度;如果已经初始化了,代表扩容得阈值),那么就以sc为长度;否则就取得默认值16
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
						//table已经初始化好了
                        table = tab = nt;
						//sc>0(如果没有初始化,代表要初始化得长度;如果已经初始化了,代表扩容得阈值)
						//计算扩容的阈值,赋值给sc(0.75)
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

五、将链表转换成红黑二叉树treeifyBin(tab, i)

/**
     * Replaces all linked nodes in bin at given index unless table is
     * too small, in which case resizes instead.
     */
	 //转红黑树
    private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
			//获取数组的长度是否小于64,小于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(n << 1):尝试扩容

/**
	 * tryPresize(n << 1):长度扩容为之前的2倍
     * Tries to presize table to accommodate the given number of elements.
     *
     * @param size number of elements (doesn't need to be perfectly accurate)
     */
    private final void tryPresize(int size) {
		//对数组扩容长度做一波判断;达到最大值则取最大值;然后是保证长度是2的n次方幂:tableSizeFor(size + (size >>> 1) + 1)
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
        int sc;
		//sizeCtl四个值得解释:
		//-1:表示map正在初始化;
		//小于-1:表示正在扩容;
		//0:还没有初始化;
		//正数:如果没有初始化,代表要初始化得长度;如果已经初始化了,代表扩容得阈值。
        while ((sc = sizeCtl) >= 0) {
			//有两种可能,第一种没有初始化数组(putAll方法);第二种数组已经初始化(正常流程过来的)
            Node<K,V>[] tab = table; int n;
			//初始化数组的操作
            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) {
				//得到一个扩容戳,(长度32位的数值,高16位做扩容标识,低16位做扩容线程数)
                int rs = resizeStamp(n);
				//sc<0表示已经开始扩容了,帮助扩容。
                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);
                }
				//暂时没有线程在扩容,暂时设置SIZECTL标志,开始扩容
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
            }
        }
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值