并发容器

210 阅读31分钟

预备知识

hash

hash:哈希又名散列,把任意长度的字符串通过一种算法(散列),变化为固定长度的输出,这个输出值就是散列值。属于压缩映射,容易产生hash冲突,常见的hash算法有直接取余法等

产生hash冲突,常见的解决方法有:

  • 开发寻址,重新找一个就近内存地址
  • 再散列:重新换一个hash算法
  • 链地址法:将有哈希冲突的元素放在一个链表中(java HashMap)

Md4,md5,sha-hash都是hash算法,又称摘要算法

位运算

  • int类型的位(4*8)

​ 高位 低位

31 5 4 3 2 1 0
0 1 0 1 0 0 0

2^0 = 1;

2^1 = 2;

2^2 = 4;

所以可以算出以上的二进制位2^5+2^3 = 40;

由上面的表格可以看出,数字类型在数字渐渐变大时,是由低位慢慢向高位扩展的。

Java实际保存int型的位的时候,第31位为0的时候代表正数,1代表负数

  • 常见的位运算有:

    • 位与 & (1 & 1 = 1;i & 0 = 0;0 & 0 = 0)
    • 位或 |(1|1 = 1;1|0 = 1;0 | 0 = 0)
    • 位非 ~ (~1 = 0;~0 = 1)
    • 位异或^(1 ^ 1 = 0;1 ^ 0 = 1; 0 ^ 0 = 0)
    • << 有符号左移 >>有符号右移 >>>无符号右移。例如:8 << 2 = 32 8 >> 2 = 2;
    • 取模操作 a % (Math.pow(2,n)) = a &(Math.pow(2,n)-1)

    代码如下:

    public class IntoBinary {
    
        public static void main(String[] args){
    
    
            System.out.println("the 4 is "+Integer.toBinaryString(4));
            System.out.println("the 6 is "+Integer.toBinaryString(6));
            System.out.println("the 4&6 is "+Integer.toBinaryString(4&6));
            System.out.println("the 4|6 is "+Integer.toBinaryString(4|6));
            System.out.println("the ~4 is "+Integer.toBinaryString(~4));
            System.out.println("the 4^6 is "+Integer.toBinaryString(4^6));
            System.out.println("the 4<<2 is  "+(4<<2)+"=="+Integer.toBinaryString(4<<2));
            System.out.println("the 4>>2 is "+(4>>2)+"=="+Integer.toBinaryString(4>>2));
            System.out.println("the 345 % 16 is "+(345%16)+ " or "+(345&(16-1)));
        }
    }
    

    输出如下

    the 4 is 100
    the 6 is 110
    the 4&6 is 100
    the 4|6 is 110
    the ~4 is 11111111111111111111111111111011
    the 4^6 is 10
    the 4<<2 is  16==10000
    the 4>>2 is 1==1
    the 345 % 16 is 9 or 9
    
  • 位运算适用于

    • 权限控制
    • 物品属性多的时候,例如电商中的商品

    权限控制例子如下:

    public class Permission {
    
        //是否允许查询,二进制第1位来表示:0为不能,1为可以
        public static final int ALLOW_SELECT = 1 << 0; //0001 = 1;
    
        //是否允许查询,二进制第1位来表示:0为不能,1为可以
        public static final int ALLOW_INSERT = 1 << 1; //0010 = 2;
    
        //是否允许查询,二进制第1位来表示:0为不能,1为可以
        public static final int ALLOW_UPDATE = 1 << 2; //0100 = 4;
    
        //是否允许查询,二进制第1位来表示:0为不能,1为可以
        public static final int ALLOW_DELETE = 1 << 3; //1000 = 8;
    
        //记录总权限
        private int flag;
    
        //设置总权限
        public void setFlag(int per) {
            flag = per;
        }
    
        //增肌用户权限,1个或多个
        public void enable(int per) {
            flag = flag | per;
        }
    
        //删除权限,一个或多个
        public void disable(int per) {
            flag = flag & ~per;
    
        }
    
        //判断存在权限
        public boolean isAllow(int per) {
            return ((flag & per) == per);
        }
    
        //判断不存在的权限
        public boolean inNotAllow(int per) {
            return ((flag & per) == 0);
        }
    
        public static void main(String[] args) {
            int flag = 15;
            Permission permission = new Permission();
            permission.setFlag(flag);
            permission.disable(ALLOW_DELETE | ALLOW_INSERT);
            System.out.println("select = " + permission.isAllow(ALLOW_SELECT));
            System.out.println("update = " + permission.isAllow(ALLOW_UPDATE));
            System.out.println("insert = " + permission.isAllow(ALLOW_INSERT));
            System.out.println("delete = " + permission.isAllow(ALLOW_DELETE));
    
        }
    
    }
    

输出如下

select = true
update = true
insert = false
delete = false

ConcurrentHashMap

HashMap多线程对哦在HashMap的HashEntry形成环形数据结果,一旦形成环形结构,Entry的Next节点永不为空,就会产生死循环

HashTable是使用synchronized来保持同步的,在线程竞争激烈的情况下HashTable的效率非常低,因为当一个线程访问HashTable的同步方法时,其他线程也访问HashTable的同步方法,就会进入阻塞或者轮询,例如线程1执行put,线程2不但不能执行put连get也不能执行

putIfAbsent(),没有这个值就将其放入map,若有不放入且把原来的值返回

1.7中的原理与实现

数据结构

ConcurrentHashMap是由「Segment」数组与「HashEntry」数组组成的。

  • Segment继承自可重入锁(ReentrantLock),在ConcurrentHashMap中扮演锁的角色
  • HashEntry则是用于存储键值对数据

一个CouncurrentHashMap包含一个Segment数组,每个Segment数组包含了一个HashEntry数组,我们称之为table每个HashEntery是一个链表的数据结构

image-20200420110656383

ConcurrentHashMap中时怎样在保证多线程下提升性能的?

ConcurrentHashMap时允许多个线程并发执行,其关键在于使用了「锁分离」技术,他使用了多个锁来控制对hash表不同部分的修改。内部使用段(sagment),来表示不同的部分,每一段时一个小的hashtable,只要多个修改发生在不同的段上,就能并发执行。

源码

初始化

关键的变量:

  • **initialCapacity ** 初始化容量 默认为16
  • loadFactor 扩容因子,默认为0.75,当一个Segment里面存储的元素大于initialCapacity*loadFactor时就进行扩容,注意Segment的数量时不会扩容的,扩容的是每个Segment里面的数组
  • concurrencyLevel :并发度,默认为16,可以理解为程序运行时能够同时跟新ConcurrentHashMap的最大线程数,即Segment[]的容量。如果并发度设置过小,会带来锁竞争问题,如果并发度设置过大,原本位于同一个Segment内的访问就会扩散到不同的Segment中,CPU cache命中率会下降,从而引起程序性能下降
//初始化
//initialCapacity = 17
//loadFactor= 0.75
//concurrencyLevel = 17
public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    int ssize = 1;
    //保证Segment的数量为2的幂,若concurrencyLevel = 17,ssize = 32 sssift = 5
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
  	//定位是哪个Segment的HashCode高位偏移量
    this.segmentShift = 32 - sshift;
  	//取模的标志
    this.segmentMask = ssize - 1;
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // c =0
    int c = initialCapacity / ssize;
    // c = 17
    if (c * ssize < initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    //保证Table数组大小为2的幂,默认为2,c = 16
    while (cap < c)
        cap <<= 1;
    // create segments and segments[0]
  	//初始化Segment数组,并实际只填充Segment数组的第0个元素。
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}
//定位是哪个Segment的HashCode高位偏移量
 this.segmentShift = 32 - sshift;
//取模的标志
 this.segmentMask = ssize - 1;

用于定位元素在哪个Segment的位置,segmentShift表示偏移位数,通过前面的int类型的位描述,int类型在数字变大的过程中,低位总是比高位先填满,为保证元素在Segment总尽量分布均匀,所以计算在Segment位置时,总是去hash值的高位计算,segmentMask作用就是为了利用位运算中取模的操作: a % (Math.pow(2,n)) 等价于 a&( Math.pow(2,n)-1)

在get和put时,如何快速定位元素所在的位置

对于某个元素而言,一定是放在某个Segment中的某个table中的,所以定位分为两步

  • 定位Segment:获取key的hashcode值,进行一次再散列(wang/Jenkins),拿到再散列值后,以再散列值的「高位取模」就可以得到在哪个Segment

  • 定位table位置:同样是获取到key的在散列值后,以在散列值的全部包与table的长度进行取模,得到当前元素在table的位置

    /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code key.equals(k)},
     * then this method returns {@code v}; otherwise it returns
     * {@code null}.  (There can be at most one such mapping.)
     *
     * @throws NullPointerException if the specified key is null
     */
    public V get(Object key) {
        Segment<K,V> s; // manually integrate access methods to reduce overhead
        HashEntry<K,V>[] tab;
        int h = hash(key);//获取key的hash值
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;//进行高位取模
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
          //(long)(((tab.length - 1) & h)定位在table的位置
            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }
    
get()方法

定位到Segment和table的位置之后,依次扫面这个链表,若能找到就返回否则返回null

在高并发的情况下如何保证get到的数据是最新的

用于保存键值对的HashEntry,在设计上它是volatile的,就保证其他线程在修改的时候,get方法就能立马看到

    /**
     * ConcurrentHashMap list entry. Note that this is never exported
     * out as a user-visible Map.Entry.
     */
    static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

        HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        /**
         * Sets next field with volatile write semantics.  (See above
         * about use of putOrderedObject.)
         */
        final void setNext(HashEntry<K,V> n) {
            UNSAFE.putOrderedObject(this, nextOffset, n);
        }

        // Unsafe mechanics
        static final sun.misc.Unsafe UNSAFE;
        static final long nextOffset;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class k = HashEntry.class;
                nextOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("next"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

put方法
  • 首先定位Segment,因为map初始化的时候只初始化了Segment[0],所以当这个Segment为null时,由ensureSegment填充这个Segment
  • 对Segment加锁
  • 定位所在的table元素,并扫描table下面的链表
    • 找到时,put会覆盖原来的值,如果调用的是putIfAbsent,则不覆盖,并且返回原来的值
    • 未找到时
      • 将新元素作为链表的头节点
      • 检查table是否需要扩容
      • 将新元素放入table的数组中,这样这个数组就存储量链表的头节点
/**
 * Maps the specified key to the specified value in this table.
 * Neither the key nor the value can be null.
 *
 * <p> The value can be retrieved by calling the <tt>get</tt> method
 * with a key that is equal to the original key.
 *
 * @param key key with which the specified value is to be associated
 * @param value value to be associated with the specified key
 * @return the previous value associated with <tt>key</tt>, or
 *         <tt>null</tt> if there was no mapping for <tt>key</tt>
 * @throws NullPointerException if the specified key or value is null
 */
@SuppressWarnings("unchecked")
public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);//若该Segment未初始化,进行初始化
    return s.put(key, hash, value, false);
}

 final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            //加锁
   					HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
              //查找是否该hash值是否已经存在与table中
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                  //若存在
                    if (e != null) {
                        K k;
                      //判断值的对象与hash值都相同就进行覆盖
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                  //若不存在,新建一个链表,将链表的头节点放入table数组
                    else {
                      //设为头节点
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                      //是否需要扩容
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                          //将链表的头节点存入他变了数组
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

扩容

Segment[]不扩容,每次table扩容,数量为之前的2倍

        /**
         * Doubles size of table and repacks entries, also adding the
         * given node to new table
         */
        @SuppressWarnings("unchecked")
        private void rehash(HashEntry<K,V> node) {
            /*
             * Reclassify nodes in each list to new table.  Because we
             * are using power-of-two expansion, the elements from
             * each bin must either stay at same index, or move with a
             * power of two offset. We eliminate unnecessary node
             * creation by catching cases where old nodes can be
             * reused because their next fields won't change.
             * Statistically, at the default threshold, only about
             * one-sixth of them need cloning when a table
             * doubles. The nodes they replace will be garbage
             * collectable as soon as they are no longer referenced by
             * any reader thread that may be in the midst of
             * concurrently traversing table. Entry accesses use plain
             * array indexing because they are followed by volatile
             * table write.
             */
            HashEntry<K,V>[] oldTable = table;
            int oldCapacity = oldTable.length;
          	//容量*2
            int newCapacity = oldCapacity << 1;
            threshold = (int)(newCapacity * loadFactor);
            HashEntry<K,V>[] newTable =
                (HashEntry<K,V>[]) new HashEntry[newCapacity];
            int sizeMask = newCapacity - 1;
            for (int i = 0; i < oldCapacity ; i++) {
                HashEntry<K,V> e = oldTable[i];
                if (e != null) {
                    HashEntry<K,V> next = e.next;
                    int idx = e.hash & sizeMask;
                    if (next == null)   //  Single node on list
                        newTable[idx] = e;
                    else { // Reuse consecutive sequence at same slot
                        HashEntry<K,V> lastRun = e;
                        int lastIdx = idx;
                        for (HashEntry<K,V> last = next;
                             last != null;
                             last = last.next) {
                            int k = last.hash & sizeMask;
                            if (k != lastIdx) {
                                lastIdx = k;
                                lastRun = last;
                            }
                        }
                        newTable[lastIdx] = lastRun;
                        // Clone remaining nodes
                        for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                            V v = p.value;
                            int h = p.hash;
                            int k = h & sizeMask;
                            HashEntry<K,V> n = newTable[k];
                            newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                        }
                    }
                }
            }
            int nodeIndex = node.hash & sizeMask; // add the new node
            node.setNext(newTable[nodeIndex]);
            newTable[nodeIndex] = node;
            table = newTable;
        }

带来的好处

快速定位以及减少重排的次数

例子:现在有容量为4的数组进行扩容,元素在table中的分布如下

Hash值 15 23 34 77
在table中的下标 3 = 15%4 3 = 23%4 2= 32%4 0 = 56%%4 1 = 77%4

扩容变为8之后,分布如下

Hash值 56 34 77 15 23
下标 0 1 2 3 4 5 6

可以看见hash值为34和56的下标没变15,23,77都是在原来的基础上+4,所以减少重拍和定位次数

size方法

size先进行两次不加锁的统计,若两次的值不一样,则需要把所有的Segament进行加锁,然后统计

/**
 * Returns the number of key-value mappings in this map.  If the
 * map contains more than <tt>Integer.MAX_VALUE</tt> elements, returns
 * <tt>Integer.MAX_VALUE</tt>.
 *
 * @return the number of key-value mappings in this map
 */
public int size() {
    // Try a few times to get accurate count. On failure due to
    // continuous async changes in table, resort to locking.
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; // true if size overflows 32 bits
    long sum;         // sum of modCounts
    long last = 0L;   // previous sum
    int retries = -1; // first iteration isn't retry
    try {
        for (;;) {
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); // force creation
            }
            sum = 0L;
            size = 0;
            overflow = false;
            for (int j = 0; j < segments.length; ++j) {
                Segment<K,V> seg = segmentAt(segments, j);
                if (seg != null) {
                    sum += seg.modCount;
                    int c = seg.count;
                    if (c < 0 || (size += c) < 0)
                        overflow = true;
                }
            }
            if (sum == last)
                break;
            last = sum;
        }
    } finally {
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}
弱一致性

get和containsKey欧式通过对链表进行遍历判断是否存在key相同的节点以及获取该节点的value,但由于在遍历过程中,其他线程可能读链表结构有操作,因此get和containsKey可能获取到的是过期的数据,这一点是ConcurrentHashMap在弱一致性上的体现。

1.8

image-20200420134756787

与1.7相比重大的变化
  • 取消了Segment数组,直接用table保存数据,锁的粒度更小,减少并发冲突的概率
  • 存储数据采用了「链表」+「红黑树」的模式,纯链表的时间复杂度为O(n),红黑树则为O(logn)。性能提升很大。当key值相等的元素形成的链表中元素个数超过8个的时候,链表会转换为红黑树;当key值相等的元素形成的链表中元素个数少于6个的时候,红黑树会转化为链表。
主要的数据结果和关键变量
  • Node类,存放实际的key和value值

  • sizeCtl

    • 负数:表示进程初始化或者扩容,-1表示进行初始化,-n,表示有n-1个线程在进行扩容
    • 正数:0表示还没有初始化,>0的数初始化或者下一次进行扩容的阈值
  • TreeNode:用在红黑树,表示树的节点,TreeBin是实际放在数组中的,代表这个红黑树的根

初始化
/**
 * Creates a new, empty map with an initial table size based on
 * the given number of elements ({@code initialCapacity}), table
 * density ({@code loadFactor}), and number of concurrently
 * updating threads ({@code concurrencyLevel}).
 *
 * @param initialCapacity the initial capacity. The implementation
 * performs internal sizing to accommodate this many elements,
 * given the specified load factor.
 * @param loadFactor the load factor (table density) for
 * establishing the initial table size
 * @param concurrencyLevel the estimated number of concurrently
 * updating threads. The implementation may use this value as
 * a sizing hint.
 * @throws IllegalArgumentException if the initial capacity is
 * negative or the load factor or concurrencyLevel are
 * nonpositive
 */
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;
}

初始化只给成员变量赋值,put的时候才进行实际的数组填充

在get和put方法中是如何快速定位的
  • 对key的值进行再散列
  • 然后根据再散列hash值对table的长度取模
/**
 * Returns the value to which the specified key is mapped,
 * or {@code null} if this map contains no mapping for the key.
 *
 * <p>More formally, if this map contains a mapping from a key
 * {@code k} to a value {@code v} such that {@code key.equals(k)},
 * then this method returns {@code v}; otherwise it returns
 * {@code null}.  (There can be at most one such mapping.)
 *
 * @throws NullPointerException if the specified key is null
 */
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;
}


    /**
     * Spreads (XORs) higher bits of hash to lower and also forces top
     * bit to 0. Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */
    static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }
get(代码见上面)
  • 查看table数组中存的头节点是否存在

  • 查看是否存在树中

  • 查看是否存在链表中

put
   /**
     * Initializes table, using the size recorded in sizeCtl.
     */
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
      //循环CAS进行设置sizeCtl
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
              //已有线程在初始化 让出cpu执行权
                Thread.yield(); // lost initialization race; just spin
            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<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        //=0.75n 扩容阈值
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break; 
            }
        }
        return tab;
    }
/**
 * Maps the specified key to the specified value in this table.
 * Neither the key nor the value can be null.
 *
 * <p>The value can be retrieved by calling the {@code get} method
 * with a key that is equal to the original key.
 *
 * @param key key with which the specified value is to be associated
 * @param value value to be associated with the specified key
 * @return the previous value associated with {@code key}, or
 *         {@code null} if there was no mapping for {@code key}
 * @throws NullPointerException if the specified key or value is null
 */
public V put(K key, V value) {
    return putVal(key, value, false);
}

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
          //初始化数组
            tab = initTable();
        //(f = tabAt(tab, i = (n - 1) & hash)) == null如果数组中这个元素为null,直接吧新元素放入数组
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            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 {
            V oldVal = null;
           //	加锁
            synchronized (f) {
              	//是否存在于链表中,在链表中插入
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.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) {
                if (binCount >= TREEIFY_THRESHOLD)
                  //链表转为树
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
  //增加集合元素计数,可触发扩容
    addCount(1L, binCount);
    return null;
}
扩容

transfer()方法进行实际的扩容操作,table大小也是翻倍的形式,有一个并发扩容的机制。

//调用该扩容方法的地方有:
//java.util.concurrent.ConcurrentHashMap#addCount        向集合中插入新数据后更新容量计数时发现到达扩容阈值而触发的扩容
//java.util.concurrent.ConcurrentHashMap#helpTransfer    扩容状态下其他线程对集合进行插入、修改、删除、合并、compute 等操作时遇到 ForwardingNode 节点时触发的扩容
//java.util.concurrent.ConcurrentHashMap#tryPresize      putAll批量插入或者插入后发现链表长度达到8个或以上,但数组长度为64以下时触发的扩容
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    //计算每条线程处理的桶个数,每条线程处理的桶数量一样,如果CPU为单核,则使用一条线程处理所有桶
    //每条线程至少处理16个桶,如果计算出来的结果少于16,则一条线程处理16个桶
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    if (nextTab == null) {            // 初始化新数组(原数组长度的2倍)
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        //将 transferIndex 指向最右边的桶,也就是数组索引下标最大的位置
        transferIndex = n;
    }
    int nextn = nextTab.length;
    //新建一个占位对象,该占位对象的 hash 值为 -1 该占位对象存在时表示集合正在扩容状态,key、value、next 属性均为 null ,nextTable 属性指向扩容后的数组
    //该占位对象主要有两个用途:
    //   1、占位作用,用于标识数组该位置的桶已经迁移完毕,处于扩容中的状态。
    //   2、作为一个转发的作用,扩容期间如果遇到查询操作,遇到转发节点,会把该查询操作转发到新的数组上去,不会阻塞查询操作。
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    //该标识用于控制是否继续处理下一个桶,为 true 则表示已经处理完当前桶,可以继续迁移下一个桶的数据
    boolean advance = true;
    //该标识用于控制扩容何时结束,该标识还有一个用途是最后一个扩容线程会负责重新检查一遍数组查看是否有遗漏的桶
    boolean finishing = false; // to ensure sweep before committing nextTab
    //这个循环用于处理一个 stride 长度的任务,i 后面会被赋值为该 stride 内最大的下标,而 bound 后面会被赋值为该 stride 内最小的下标
    //通过循环不断减小 i 的值,从右往左依次迁移桶上面的数据,直到 i 小于 bound 时结束该次长度为 stride 的迁移任务
    //结束这次的任务后会通过外层 addCount、helpTransfer、tryPresize 方法的 while 循环达到继续领取其他任务的效果
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        while (advance) {
            int nextIndex, nextBound;
            //每处理完一个hash桶就将 bound 进行减 1 操作
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                //transferIndex <= 0 说明数组的hash桶已被线程分配完毕,没有了待分配的hash桶,将 i 设置为 -1 ,后面的代码根据这个数值退出当前线的扩容操作
                i = -1;
                advance = false;
            }
            //只有首次进入for循环才会进入这个判断里面去,设置 bound 和 i 的值,也就是领取到的迁移任务的数组区间
            else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            //扩容结束后做后续工作,将 nextTable 设置为 null,表示扩容已结束,将 table 指向新数组,sizeCtl 设置为扩容阈值
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            //每当一条线程扩容结束就会更新一次 sizeCtl 的值,进行减 1 操作
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                //(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT 成立,说明该线程不是扩容大军里面的最后一条线程,直接return回到上层while循环
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                //(sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT 说明这条线程是最后一条扩容线程
                //之所以能用这个来判断是否是最后一条线程,因为第一条扩容线程进行了如下操作:
                //    U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)
                //除了修改结束标识之外,还得设置 i = n; 以便重新检查一遍数组,防止有遗漏未成功迁移的桶
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null)
            //遇到数组上空的位置直接放置一个占位对象,以便查询操作的转发和标识当前处于扩容状态
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            //数组上遇到hash值为MOVED,也就是 -1 的位置,说明该位置已经被其他线程迁移过了,将 advance 设置为 true ,以便继续往下一个桶检查并进行迁移操作
            advance = true; // already processed
        else {
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    //该节点为链表结构
                    if (fh >= 0) {
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        //遍历整条链表,找出 lastRun 节点
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        //根据 lastRun 节点的高位标识(0 或 1),首先将 lastRun设置为 ln 或者 hn 链的末尾部分节点,后续的节点使用头插法拼接
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        //使用高位和低位两条链表进行迁移,使用头插法拼接链表
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        //setTabAt方法调用的是 Unsafe 类的 putObjectVolatile 方法
                        //使用 volatile 方式的 putObjectVolatile 方法,能够将数据直接更新回主内存,并使得其他线程工作内存的对应变量失效,达到各线程数据及时同步的效果
                        //使用 volatile 的方式将 ln 链设置到新数组下标为 i 的位置上
                        setTabAt(nextTab, i, ln);
                        //使用 volatile 的方式将 hn 链设置到新数组下标为 i + n(n为原数组长度) 的位置上
                        setTabAt(nextTab, i + n, hn);
                        //迁移完成后使用 volatile 的方式将占位对象设置到该 hash 桶上,该占位对象的用途是标识该hash桶已被处理过,以及查询请求的转发作用
                        setTabAt(tab, i, fwd);
                        //advance 设置为 true 表示当前 hash 桶已处理完,可以继续处理下一个 hash 桶
                        advance = true;
                    }
                    //该节点为红黑树结构
                    else if (f instanceof TreeBin) {
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        //lo 为低位链表头结点,loTail 为低位链表尾结点,hi 和 hiTail 为高位链表头尾结点
                        TreeNode<K,V> lo = null, loTail = null;
                        TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        //同样也是使用高位和低位两条链表进行迁移
                        //使用for循环以链表方式遍历整棵红黑树,使用尾插法拼接 ln 和 hn 链表
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            //这里面形成的是以 TreeNode 为节点的链表
                            TreeNode<K,V> p = new TreeNode<K,V>
                                (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        //形成中间链表后会先判断是否需要转换为红黑树:
                        //1、如果符合条件则直接将 TreeNode 链表转为红黑树,再设置到新数组中去
                        //2、如果不符合条件则将 TreeNode 转换为普通的 Node 节点,再将该普通链表设置到新数组中去
                        //(hc != 0) ? new TreeBin<K,V>(lo) : t 这行代码的用意在于,如果原来的红黑树没有被拆分成两份,那么迁移后它依旧是红黑树,可以直接使用原来的 TreeBin 对象
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                        (hc != 0) ? new TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                        (lc != 0) ? new TreeBin<K,V>(hi) : t;
                        //setTabAt方法调用的是 Unsafe 类的 putObjectVolatile 方法
                        //使用 volatile 方式的 putObjectVolatile 方法,能够将数据直接更新回主内存,并使得其他线程工作内存的对应变量失效,达到各线程数据及时同步的效果
                        //使用 volatile 的方式将 ln 链设置到新数组下标为 i 的位置上
                        setTabAt(nextTab, i, ln);
                        //使用 volatile 的方式将 hn 链设置到新数组下标为 i + n(n为原数组长度) 的位置上
                        setTabAt(nextTab, i + n, hn);
                        //迁移完成后使用 volatile 的方式将占位对象设置到该 hash 桶上,该占位对象的用途是标识该hash桶已被处理过,以及查询请求的转发作用
                        setTabAt(tab, i, fwd);
                        //advance 设置为 true 表示当前 hash 桶已处理完,可以继续处理下一个 hash 桶
                        advance = true;
                    }
                }
            }
        }
    }
}

img

size

估计的大概数量,不是精确数量

一致性

弱一致

更多的并发容器

ConcurrentSkipListMap和ConcurrentSkipListSet

TreeMap和TreeSet这两种有序容器的并发版本

跳表

SkipList,以空间换时间, 在原链表上面增加多层索引 ,但是在某个节点插入时是否成为索引,是随机决定的,所以跳表又称概率数据结构

ConcurrentLinkedQueue

无界非阻塞队列,底层是个链表遵循先进先出

  • add offer 将元素插在尾部
  • peek从头部那数据但是不移除
  • poll从头部拿数据,但是移除

写时复制容器

写时复制容器:通俗的讲,当我们往一个容器添加数据时,不直接往当前容器添加,而是先将当前容器进行copy,复制出一个新的容器,然后在新的容器里面添加元素,添加完成之后,再将原容器的引用指向新容器。这样做的好处是可以对容器进行并发的读 ,而不需要加锁,因为当前容器不会添加任何元素。所以复制容器也是读写分离的一种思想,读和写不同的容器,如果读的时候多个线程在往容器中添加数据,读还是会读到旧数据,因为写的时候读不回锁住,只能保证最终一致性

适用于读多写少的场景:例如:黑/白名单,商品的访问更新场景,

存在内存问题

阻塞队列

生产者消费者模式

  • 当队列满的时候,插入线程被阻塞,直到队列不满
  • 当队列空的时候,读线程被阻塞,直到队列不空

生产者:就是生产数据的线程。消费者就是消费数据的线程。在多线程开发中,如果生产者生产数据速度很快,而消费者处理速度很慢,那么生产者必须等待消费者,反之亦然。

为了解决生产消费能力不均衡的问题,便有了生产者消费者模式,生产者消费者模式通过一个容器来解决生产者消费者的强耦合问题。生产者消费者彼此之间不直接通信 ,所以生产者生产数据之后不必等待消费者处理,直接扔给阻塞队列,消费者不直接找生产者要数据,而是直接冲阻塞队列里面取。阻塞队列就相当于一个缓冲区,平衡生产者与消费者

常用方法

方法 抛出异常 返回值 一直阻塞 超时退出
插入方法 add offer put offer(time)
移除方法 remove Poll take poll(time)
检查方法 element peek N/A N/A
  • 抛出异常:当队列满时,如果再往队列中插入数据,会抛出IllegalStateException("Queuefull")异常,当队列空时,再往队列中取数据,会抛NoSuchElementException异常
  • 返回特殊值:当往队列插入原属的时候,会返回是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null
  • 一直阻塞:当阻塞队列满的时候,如果生产者往队列put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列为空时,如果消费者从队列里面take元素,队列会阻塞消费者线程,直到队列不为空
  • 超时退出:当队列满的时候,如果生产者往队列里面插入元素,队列会阻塞生产者线程一段时间,如果超过指定时间,生产者就会退出

常用的阻塞队列

  • ArrayBlockingQueue 一个数组组成的有界阻塞队列,按照先进先出的原则,要求设置初始化大小
  • LinkedBlockingQueue 一个由链表组成的有界阻塞队列,按照先进先出的原则,可以不设置初始化大小,默认为Intege.Max_Value
  • ArrayBlockingQueue与LinkedBlockingQueue的区别
    • 锁上面:ArrayBlokcingQueue只有一个锁,LinkedBlockingQueue有两个锁
    • 实现上:ArrayBlockingQueue直接插入数据,LinkedBlockingQueue需要转换
  • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列 默认情况下,按照字典序排序,要么实现compareTo方法,要么指定构造参数Compator
  • DelayQueue一个使用优先级的无界阻塞队列,支持延时获取元素的阻塞队列,元素必须实现Delay接口,适用场景,实现自己的缓存系统,订单到期,限时支付等
  • SynchonousQueue一个不存储元素的队列,每个put操作都要等待一个take操作
  • LinkedTransferQueue 一个链表组成的无界阻塞队列,transfer不忘队列中添加,直接给消费者,但是必须要消费者消费之后才返回,tryTransfer()无论消费者是否接收,方法都立即返回。
  • LinkedBlockingDeque 一个链表组成的双向阻塞队列,可以从队列的头或者尾进行插入和消费数据,实现工作密取,方法名带了first的是对头部操作,方法名带了last的是对尾部操作,另外:add = addLast remove = removeFirst take = takeFist

阻塞队列的原理

比如,ArrayBlockingQueue就是基于Lock和Condition实现的。

使用DelayQueue实现一个订单的延时

定义一个ItemVo实现Delayed

public class ItemVo<T> implements Delayed {

    private long activeTime;//到期时间

    private T data;

    public ItemVo(long activeTime, T data) {
        //将传人的持续时间毫秒转化为纳秒,+当前时间纳秒 = 过期时刻
        this.activeTime = System.nanoTime()+ TimeUnit.NANOSECONDS.convert(
          activeTime,TimeUnit.MILLISECONDS);
        this.data = data;
    }

    public long getActiveTime() {
        return activeTime;
    }

    public T getData() {
        return data;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(activeTime-System.nanoTime(),TimeUnit.NANOSECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        long l = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
        return (l==0)?0:(l>0)?1:-1;
    }
}

定义一个Order类

public class Order {

    private final String orderNo;
    private final double orderMoney;

    public Order(String orderNo, double orderMoney) {
        this.orderNo = orderNo;
        this.orderMoney = orderMoney;
    }

    public String getOrderNo() {
        return orderNo;
    }

    public double getOrderMoney() {
        return orderMoney;
    }
}

定义一个生产者PutOrder

public class PutOrder implements  Runnable{

    private DelayQueue<ItemVo<Order>> queue;

    public PutOrder(DelayQueue<ItemVo<Order>> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        Order  orderTb = new Order("tb123",1000);
        ItemVo<Order> itemTB = new ItemVo<>(5000,orderTb);
        queue.offer(itemTB);
        System.out.println("订单5秒后到期:"+orderTb.getOrderNo());


        Order orderJd = new Order("jd321",1000);
        ItemVo<Order> itemJd = new ItemVo<>(8000,orderJd);
        queue.offer(itemJd);
        System.out.println("订单8秒后到期:"+orderJd.getOrderNo());
    }
}

定义一个消费者FetchOrder

public class FetchOrder implements Runnable {
    private DelayQueue<ItemVo<Order>> queue;

    public FetchOrder(DelayQueue<ItemVo<Order>> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while(true){
            try {
                Order order = (Order)queue.take().getData();
                System.out.println("get from queue + "+order.getOrderNo());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

编写测试类

public class Test {

    public static void main(String[] args) throws InterruptedException {
        DelayQueue<ItemVo<Order>> queue = new DelayQueue<>();
        new Thread(new PutOrder(queue)).start();
        new Thread(new FetchOrder(queue)).start();
        //每隔500毫秒,打印个数字
        for(int i=1;i<15;i++){
            Thread.sleep(500);
            System.out.println(i*500);
        }
    }
}
	

输出是如下

订单5秒后到期:tb123
订单8秒后到期:jd321
500
1000
1500
2000
2500
3000
3500
4000
4500
get from queue + tb123
5000
5500
6000
6500
7000
get from queue + jd321