java容器-ConcurrentHashMap

文章目录

  • ConcurrentHashMap
    • 重要常量
    • 四个节点
    • 构造方法
    • tableSizeFor
    • put
    • initTable
    • helpTransfer transfer等扩容机制
    • get
    • 小结

ConcurrentHashMap

本文章源码来自 Java 8,重点是put和get方法及涉及到相关方法

重要常量


    // map容器的最大容量
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默认的初始容量
    private static final int DEFAULT_CAPACITY = 16;

    // toArry 方法转化的最大容量,超过报oom异常
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    // 并发级别
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    // 加载因子,常说的阀值不过现在基本没用了都是用 n-(n >> 2) 表示
    private static final float LOAD_FACTOR = 0.75f;

    // 转化为红黑树的节点数,在concurrentHashMap里面容量必须大于64才能进行红黑树转化
    static final int TREEIFY_THRESHOLD = 8;

    // 红黑树转化为链表的节点数
    static final int UNTREEIFY_THRESHOLD = 6;

    // 上面说的转化树所需最小容量
    static final int MIN_TREEIFY_CAPACITY = 64;

    // 扩容时最小转移容量
    private static final int MIN_TRANSFER_STRIDE = 16;

    // resize 校验码
    private static int RESIZE_STAMP_BITS = 16;
	
	/**
     * 主要用于表初始化和扩容时的控制,各种数值有不同的含义
     * -1:      表正在初始化或者扩容
     * 其他负数: 绝对值减一,就是正在扩容的线程数
     * 正数:  表示下次扩容时的阈值,超过该值后进行扩容	  
     */
	private transient volatile int sizeCtl;

四个节点

Node:table数组中的存储元素,即一个Node对象就代表一个键值对(key,value)存储在table中
TreeNode:看名字就是知道,这里是红黑的节点,但是并不是直接通过TreeNode组成树,而是包装成TreeBins然后组装成树
TreeBins:用于封装维护TreeNode,是红黑树的真正存放节点
ForwardingNode:仅仅用于扩容时的临时节点

构造方法

    // 无参构造器,用默认的值进行初始化
    public ConcurrentHashMap() {
    }

    // 指定初始表长的构造器,tableSizeFor 方法总是返回2的幂次方,将在后面分析这个算法
    public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }
		
	//  指定初始容量和负载因子
    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }

    // concurrencyLevel主要是为了兼容1.7及之前版本,它并不是实际的并发级别
    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;
    }

tableSizeFor

简单来说 这个算法的目标是得到 2的n次方 的一个初始容量,运算过程是通过或运算让每一位的数值都变成1然后通过加1的方式变成2的幂次方

    private static final int tableSizeFor(int c) {
		// 这里的目的是为了不让 c 翻倍,假设c=8(刚好为2的N次幂的时候不减的下面算法会让数值翻倍)
		// c 二进制表现为 1000,减一就变成了111,后面n+1的时候会变回1000
		// 如果1000经过运算会变成1111,加一就会变成1000 0,这样会让最后的数值翻倍
        int n = c - 1;
		// 为了方便我们假设减一后的值为1000 0001 那么下面的运算会变成
		// 1000 0001
		// 0100 0000 =1100 0001
        n |= n >>> 1;
		// 1100 0001
		// 0011 0000 =1111 0001
        n |= n >>> 2;
		// 1111 0001
		// 0000 1111 =1111 1111
        n |= n >>> 4;
		// 因为举例的数字没那么大,下面就省略了,效果是一样的就是让所有位数都变成1
        n |= n >>> 8;
        n |= n >>> 16;
		// 最后返回的就是 1111 1111 + 1 = 1 0000 0000 正好是2的九次方
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

put

主要做的事情:

  1. 检查key/value是否为空,如果为空,则抛异常,否则进行2
  2. 两次hash得到key的hash值,进行3
  3. 进入for死循环,进行4
  4. 检查table是否初始化了,如果没有,则调用initTable()进行初始化然后进行3,否则进行5
  5. 赋值并通过tabAt这个CAS方法判断节点是否已经存在,为空则插入元素break跳出进行8,否则进行6
  6. 判断此节点是否处于扩容状态,是则加入帮助扩容,然后进行8,否则进行7
  7. 产生hash碰撞锁住头结点,进行equals判断相同则替换,不同判断链表还是红黑树然后进行插入,进行8
  8. 插入成功后判断是否需要转化成树,如果是调用treeifyBin()方法尝试进行转化

casTabAt、tabAt、setTabAt 均为CAS操作不在这里细讲,其他方法将在后面分析

   public V put(K key, V value) { return putVal(key, value, false);  }
   final V putVal(K key, V value, boolean onlyIfAbsent) {
		// 验证 key value 不为空
        if (key == null || value == null) throw new NullPointerException();
		// 两次hash  spread方法本身也是一次hash
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node[] tab = table;;) {
            Node f; int n, i, fh;
			// 判断容器本身有无初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
			/* 1. i 赋值为 (n-1)& hash 可以理解为 hash % n
			   2. tabAt 寻找tab[i]
			   3. 复制给 f
			   4. 判断这个节点是否为空
			 */
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
			    // 为空说明没有碰撞采用cas保存元素
                if (casTabAt(tab, i, null,
                             new Node(hash, key, value, null)))
                    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) {
                            binCount = 1;
                            for (Node e = f;; ++binCount) {
                                K ek;
								// hash 相等 key相等 直接替换
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
								// 否则插入到链表末尾
                                Node pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
						// 如果是黑红树插入树节点
                        else if (f instanceof TreeBin) {
                            Node p;
                            binCount = 2;
                            if ((p = ((TreeBin)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;
    }

initTable

private final Node[] initTable() {
        Node[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0) // 判断当前状态,如果正在扩容,让出资源
                Thread.yield(); // lost initialization race; just spin
             // 否则通过CAS方式将SIZECTL设为-1也就是扩容状态
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node[] nt = (Node[])new Node[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);// 阀值 0.75
                    }
                } finally {
                    sizeCtl = sc; // 设置sizeCtl 为当前阀值
                }
                break;
            }
        }
        return tab;
    }

helpTransfer transfer等扩容机制

请允许我偷个懒,这里写的很不错
https://www.jianshu.com/p/487d00afe6ca

get

主要做的事情:
1、根据key调用spread计算hash值;并根据计算出来的hash值计算出该key在table出现的位置i.
2、检查table是否为空;如果为空,返回null,否则进行3
3、检查table[i]处桶位不为空;如果为空,则返回null,否则进行4
4、先检查table[i]的头结点的key是否满足条件,是则返回头结点的value;否则分别根据树、链表查询。

 public V get(Object key) {
        Node[] tab; Node e, p; int n, eh; K ek;
        int h = spread(key.hashCode());//两次hash计算出hash值
        if ((tab = table) != null && (n = tab.length) > 0 &&//table不能为null,是吧
            (e = tabAt(tab, (n - 1) & h)) != null) {//table[i]不能为空,是吧
            if ((eh = e.hash) == h) { //检查头结点
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0) //table[i]为一颗树
                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;
    }

小结

本文主要是对put 和 get 相关方法进行分析,目的是理解一点点大神的思路和实现方式,能学会一点点东西,有兴趣的小伙伴可以自行查看此容器内的其他方法。

你可能感兴趣的:(java基础)