深入浅出java并发编程(ConcurrentHashMap)

参考资料

精妙绝伦的并发艺术品 — ConcurrentHashMap是如何保证线程安全的

一文澄清网上对 ConcurrentHashMap 的一个流传甚广的误解!

快速上手

使用方法跟普通的map没有区别,唯一区别:key和value均不能为空

if (key == null || value == null) throw new NullPointerException();

put如何保证线程安全

    public V put(K key, V value) {
        return putVal(key, value, false);
    }

putVal

第一行:K,V的校验

 if (key == null || value == null) throw new NullPointerException();

第二行:hash值的计算,在HashMap的基础上有所变化

	int hash = spread(key.hashCode());

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

第三行:binCount 初始化

	int binCount = 0;

第四行:for循环和部分值的初始化

        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
        }

第一个处理(initTable)

源码解析

for循环中第一个if,(主要作用:如果Node数组为空执行initTable)

            if (tab == null || (n = tab.length) == 0)
                tab = initTable();

思考1:当多个线程同时进入该方法,如何保证只有一个线程进行初始化。

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab;
        int sc;
        //在初始化动作完成之前,所有线程while循环
        while ((tab = table) == null || tab.length == 0) {
        	//如果没有竞争sc=0,如果<0代表有其他线程修改,让出执行。
            if ((sc = sizeCtl) < 0)
                Thread.yield(); 
            //CAS操作,SIZECTL是sizeCtl的偏移量,比较字段sizeCtl和局部变量sc,
            //如果相同说明没有其他线程修改sizeCtl,将sizeCtl设置为-1
            //由于CAS在CPU指令级别是原子的,所以只有一个线程可以进入下面这段代码。
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {            
                try {
                    if ((tab = table) == null || tab.length == 0) {
                    	//三元表达式,sc>0,n=sc,否则n=DEFAULT_CAPACITY=16
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        //Node数组初始化
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        //sc= 16-4=12
                        sc = n - (n >>> 2);
                    }
                } finally {
                	//12
                    sizeCtl = sc;
                }
                break;
            }
        }
        //初始化动作完成,返回tab
        return tab;
    }

说明:SIZECTL是sizeCtl的偏移量,Unsafe中通过offset(偏移量)来操作对象中的字段,也就是这里的sizeCtl。

总结:

  1. sc = sizeCtl 和 compareAndSwapInt(this, SIZECTL, sc, -1) 保证了只有一个线程可以将sizeCtl修改为-1,进而执行后续的初始化动作。(CAS)
  2. volatile Node<K,V>[] table 保证了Node数组table的可见性,从而避免其他线程重复初始化。(volatile)
  3. 执行初始化的线程在finally 代码块修改sizeCtl 的值,从而让后续线程可以正确的CAS。(数值还原),如果缺失finally会导致其他线程永远无法结束。

一点思考:

  • if代码块能否换成其他语句? continue(浪费CPU时间,重复判断if)、break(跳出while,破坏语义返回null数组)、sleep(休眠时间不好指定)

demo1

public class InitTable {

    /**
     * 关于这里的可见性,我认为是要加的,但是我没有测出来
     */
    Integer[] table;
    volatile int sizeCtl;

    static long SIZECTL;

    static sun.misc.Unsafe U;
    static Class<?> k;

    static {
        try {
            k = InitTable.class;
            U = UnsafeUtils.getUnsafe();
            SIZECTL = U.objectFieldOffset(k.getDeclaredField("sizeCtl"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    Integer[] initTableV1() {
        Integer[] tab;
        int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0) {
                Thread.yield();
            } else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        Integer[] nt = new Integer[10];
                        table = tab = nt;
                        sc = 10;
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }
}

启动类

public class InitTableClient {
    public static void main(String[] args) {
        InitTable initTable = new InitTable();
        List<Integer[]> list = new ArrayList<>();
        for (int i = 0; i < 15; i++) {
            new Thread(() -> {
                Integer[] integers = initTable.initTableV1();
                list.add(integers);
            }, String.valueOf(i)).start();
        }
        System.out.println("");
    }
}

关于volatile table的思考,尽管我这里没有加volatile关键字,最终集合中的list都是同一个对象,没有出现重复创建的情况,后续如果知道了原因会在这里补充。

思考1:除了volatile table,还有其他操作会让线程从主存中拿到table。

第二个处理(tabAt&casTabAt)

源码解析

第二个else if,(主要作用,如果数组第i个元素为空,CAS创建一个数组节点)

			//获取tab数组第(n-1)&hash处的位置的Node,如果Node为空
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            	//cas,如果该位置没有被其他线程修改,创建一个新的Node
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                  
            }

tabXX源码

    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }

主要思想,通过tabAt方法获得Node数组指定索引处的值,如果为空,使用casTabAt来确保只有一个线程创建Node对象,也就是坑位。

demo2

public class TabAt {

    Integer[] tab = new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    static Unsafe U = UnsafeUtils.getUnsafe();
    /**
     * 数组的基础偏移量
     */
    static final long ABASE;
    static final long ASHIFT;

    static Class<?> k = TabAt.class;
    static Class<?> ak = Integer[].class;

    public Integer[] getTab() {
        return tab;
    }

    static {
        ABASE = U.arrayBaseOffset(ak);
        System.out.println("ABASE=" + ABASE);
        int scale = U.arrayIndexScale(ak);
        System.out.println("scale=" + scale);
        if ((scale & (scale - 1)) != 0) {
            throw new Error("data type scale not a power of two");
        }
        ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
        System.out.println("ASHIFT=" + ASHIFT);
    }

    /**
     * 获取数组第i个元素的值
     *
     * @param tab
     * @param i
     * @return
     */
    static final Integer tabAt(Integer[] tab, long i) {
        return (Integer) U.getObjectVolatile(tab, ((long) i << ASHIFT) + ABASE);
    }

    /**
     * cas数组第i个元素的值,如果是c变为v
     *
     * @param tab
     * @param i
     * @param c
     * @param v
     * @return
     */
    static final boolean casTabAt(Integer[] tab, int i, Integer c, Integer v) {
        return U.compareAndSwapObject(tab, ((long) i << ASHIFT) + ABASE, c, v);
    }

    /**
     * 将数组第i个元素修改为v
     *
     * @param tab
     * @param i
     * @param v
     */
    static final void setTabAt(Integer[] tab, int i, Integer v) {
        U.putObjectVolatile(tab, ((long) i << ASHIFT) + ABASE, v);
    }


    Integer changeVal(int index, int val) {
        Integer oldVal = tabAt(tab, index);
        boolean casTabAt = casTabAt(tab, index, oldVal, val);
        System.out.println("casTabAt=" + casTabAt);
        // setTabAt(tab, index, val);
        return oldVal;
    }

}

启动类

public class TabAtClient {
    public static void main(String[] args) {
        TabAt tab = new TabAt();
        Random random = new Random();
        for (int i = 0; i < tab.getTab().length; i++) {
            int nextInt = random.nextInt(10);
            tab.changeVal(i, nextInt);
            System.out.println("nextInt:" + nextInt);
        }
        System.out.println(Arrays.toString(tab.getTab()));
    }
}

输出

ABASE=16
scale=4
ASHIFT=2
casTabAt=true
nextInt:0
casTabAt=true
nextInt:7
casTabAt=true
nextInt:2
casTabAt=true
nextInt:8
casTabAt=true
nextInt:5
casTabAt=true
nextInt:5
casTabAt=true
nextInt:8
casTabAt=true
nextInt:7
casTabAt=true
nextInt:7
casTabAt=true
nextInt:5
[0, 7, 2, 8, 5, 5, 8, 7, 7, 5]

第三个处理

源码解析

第三个处理,fh = f.hash,== -1 时处理,f 在第二步被赋值为tabAt的结果

            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);

helpTransfer源码

    final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    	//定义nextTab和sc
        Node<K,V>[] nextTab; int sc;
        //如果tab不为空&&f是ForwardingNode类型&&将f转为ForwardingNode后,nextTable不为空
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            //计算一个rs值
            int rs = resizeStamp(tab.length);
            //
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }

resizeStamp源码

    static final int resizeStamp(int n) {
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }
MOVED字段
    static final int MOVED     = -1; // hash for forwarding nodes

查找MOVED字段在类中的使用

在这里插入图片描述

第四个处理 else

    else {
    			//定义oldVal
                V oldVal = null;
                //锁定f,f=tabAt。。。。,相当于数组的某一个元素位置,一个坑对应一把锁
                synchronized (f) {
                	//判断数组第i个位置是否等于f
                    if (tabAt(tab, i) == f) {
                    	//fh是f节点的hash
                        if (fh >= 0) {
                        	//初始化为1
                            binCount = 1;
                            //沿着f链表查找
                            for (Node<K,V> e = f;; ++binCount) {
                            	//定义K泛型ek
                                K ek;
                                //如果判断相等
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                     //oldVal = e.val
                                    oldVal = e.val;
                                    //如果不是没有写入
                                    if (!onlyIfAbsent)
                                    	//写入新值
                                        e.val = value;
                                    //跳出循环
                                    break;
                                }
                                //如果节点不相等
                                Node<K,V> pred = e;
                                //直到空节点的时候
                                if ((e = e.next) == null) {
                                	//让这个key加入链表
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        //如果f是树节点
                        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) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }

方法最后,循环后处理

        addCount(1L, binCount);
        return null;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值