Java并发容器和框架


观看《Java并发编程的艺术》所做笔记

Java并发容器和框架

ConcurrentHashMap的实现原理

为什么要使用ConcurrentHashMap
  1. 在并发中HashMap是线程不安全的
  2. HashTable虽然线程安全但是在竞争锁激烈的情况下,效率低
  3. 而ConcurrentHashMap使用分段锁技术一段数据配一把锁,既保证了安全又使效率不低
JDK7ConcurrentHashMap结构
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {

ConcurrentHashMap继承自AbstractMap 实现了ConcurrentMap接口和可序列化接口

在这里插入图片描述

ConcurrentHashMap由Setment数组和HashEntry数组组成

  • HashEntry用来存储键值对 链表

  • 在Setment数组中的每个元素都包含一个HashEntry 数组+链表 (Setment继承了ReentrantLock,所以要修改HashEntry上的键值对时需要获得HashEntry对应的Setment上的锁)

ConcurrentHashMap图

在这里插入图片描述

JDK7ConcurrentHashMap初始化

查看ConcurrentHashMap(int,int,int)构造器源码

//调用无参构造时:初始化容量initialCapacity=16,负载因子loadFactor=0.75F,并发级别concurrencyLevel=16
public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    //如果三个参数不符合规范抛出非法参数异常
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    //如果并发级别高于最大值就将并发级别设置为最大值1 << 16(2的16次方)
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    //sshift记录ssize左移次数
    int sshift = 0;
    //ssize是Setment数组长度
    int ssize = 1;
    //ssize小于并发级别就左移1位,并且sshift记录左移次数
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    //segmentShift:用于定位参与散列计算的位数
    this.segmentShift = 32 - sshift;
    //segmentMask:散列运算的掩码
    this.segmentMask = ssize - 1;
    //如果初始容量大于最大容量1 << 30(2的30次方)就把初始容量设置为最大容量
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //c=初始容量/setment数组长度
    int c = initialCapacity / ssize;
    //对c进行向上取整
    if (c * ssize < initialCapacity)
        ++c;
    //HashEntry长度cap:初始化为最小HashEntry长度MIN_SEGMENT_TABLE_CAPACITY = 2; 
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    //如果cap小于c就左移1位 说明HashEntry长度不是2就是2的n次方
    while (cap < c)
        cap <<= 1;
    //创建下标0的segment
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    //创建长度为ssize的segment数组
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    //将下标0的segment加入segment数组中
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

通过源码得出的结论:

Segment数组长度

segment数组长度(ssize)由concurrentLevel并发级别求出

sszie总是比concurrentLevel大,且是2的n次方(比如concurrentLevel为:17或20或30时,ssize都是32)

将segment数组长度设为2的n次方是为了使用位运算取代模运算,提高效率

concurrentLevel最大值为2的16次方所以segment数组长度(ssize)最大为2的16次方

默认下concurrentLevel为16,所以segment数组长度默认为16

segmentShift和segmentMask定位segment

如果散列表中节点分布不均匀,很多元素都在同一个segment上,那分段锁就失去了意义,所以要让元素在segment上分布均匀

添加,删除,获取元素时,需要通过散列运算定位到segment,而散列运算需要segmentShift和segmentMask来进行充分的散列,确保每位数据都散开,减少哈希冲突

private int hash(Object k) {
    int h = hashSeed;

    if ((0 != h) && (k instanceof String)) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();
    // Spread bits to regularize both segment and index locations,
    // using variant of single-word Wang/Jenkins hash.
    h += (h <<  15) ^ 0xffffcd7d;
    h ^= (h >>> 10);
    h += (h <<   3);
    h ^= (h >>>  6);
    h += (h <<   2) + (h << 14);
    return h ^ (h >>> 16);
}

使用Wang/Jenkins hash算法将哈希码进行再散列,目的使元素能够均匀的分布在不同的segment中为了减少哈希冲突

如果segmentForHash(int)定位segment

//h = 哈希码
private Segment<K,V> segmentForHash(int h) {
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    return (Segment<K,V>) UNSAFE.getObjectVolatile(segments, u);
}
  • segmentShift用于定位参与散列运算的位数
    • segmentShift=32-sshift(ssize左移次数)
  • segmentMask是散列运算的掩码
    • segmentMask=ssize - 1

默认情况下,ssize=16,sshift=4,segmentShift=28,segmentMask=15

(h >>> segmentShift) & segmentMask : 让哈希码无符号右移28位,让高4位参与到散列计算中

HashEntry数组长度

c:先求出每个segment中应该存放多少个键值对

c = 初始化容量 / ssize (需要对c进行向上取整) 默认情况下c = 16/16=1

cap: HashEntry的长度 (最小为2,如果小于c就左移) 默认情况下c=1,cap=2

初始化segment

//lf:负载因子默认0.75F 
//threshold:是否需要扩容的阈值 threshold=(int)cap*lf(HashEntry数组长度*负载因子) 默认(int)2*0.75=1
//tab:创建的HashEntry 默认创建长度为cap=2的HashEntry
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
    this.loadFactor = lf;
    this.threshold = threshold;
    this.table = tab;
}
JDK7ConcurrentHashMap的操作
get操作
public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    //哈希码进行扩散
    int h = hash(key);
    //定位segment的哈希算法:(((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    //定位到的segment不为空且segment的HashEntry不为空
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        //循环定位HashEntry
        //定位HashEntry的哈希算法:(((tab.length - 1) & h)) << TSHIFT) + TBASE
        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;
            //找到了Key 返回
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

1. 对哈希码进行扩散
2. 定位segment
3. 定位HashEntry
4. 在HashEntry链表上寻找Key 找到返回Value 没找到返回null

定位segment哈希算法使用哈希码再扩散的高位计算

定位hash entry哈希算法使用哈希码在扩散的值直接计算

get操作是典型的读操作,get操作未使用锁,get操作中通过volatile修饰共享变量,保证内存数据的可见性,即使有其他线程对共享变量进行写操作,那这个线程也会去主内存中重新读取这个共享变量的新值,不会出现脏读

这是volatile代替锁的经典场景

put操作
public V put(K key, V value) {
    Segment<K,V> s;
    //Value不能为空
    if (value == null)
        throw new NullPointerException();
    //哈希码再扩散
    int hash = hash(key);
    //定位segment
    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) {
        	//尝试获取锁 成功则node为null,失败则进入scanAndLockForPut中循环尝试获取锁,次数过多挂起线程
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                //定位hashentry
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> first = entryAt(tab, index);
                //遍历链表hashentry,判断操作是添加还是替换
                for (HashEntry<K,V> e = first;;) {
                    //当前节点(hashentry)不为空判断是否有重复Key有则替换退出循环,没有则查看下一个节点
                    if (e != null) {
                        K k;
                        //如果有重复的Key则替换
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    //当前节点(hashentry)为空,说明要进行的操作是添加而不是替换
                    else {
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        //节点数量超过阈值 并且 hashentry数组小于最大容量 则进行扩容添加元素否则直接添加元素
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                //解锁唤醒其他线程
                unlock();
            }
            return oldValue;
        }
  1. 定位segment

  2. 尝试获取锁

    2.1 获取锁失败,进入scanAndLockForPut(key, hash, value)重复获取锁,次数太多挂起线程

    2.2 获取锁成功,定位hashentry

  3. 遍历hashentry链表,判断操作是添加还是替换

    3.1 如果当前节点(hashentry)不为空,且Key相同则是替换,替换完Value后退出

    3.2 如果当前节点(hash entry)为空,则是添加操作

    ​ 3.21 判断是否要扩容,需要扩容则扩容后添加元素,不需要则直接添加元素

  4. 添加/替换操作完之后解锁,唤醒其他线程

因为put操作是写操作,所以在多线程中需要加锁ReentrantLock

是否需要扩容

int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
  1. count是否超过阈值 count是该segment中的所有元素数量
  2. hashentry数组长度是否小于最大值

如何扩容

private void rehash(HashEntry<K,V> node) {
    HashEntry<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    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;
}

创建容量为原来容量2倍的Hashentry数组

将原数组中的元素进行再散列后插入新数组

只会对某个segment扩容,而不会对所有segment扩容

size操作
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; // 判断是否溢出
    long sum;         // 统计modCount
    long last = 0L;   // 上一次统计的modCount
    int retries = -1; // 为2时,将segment全锁住来统计size
    try {
        for (;;) {
            //RETRIES_BEFORE_LOCK=2
            //如果retries为2时,会把concurrentHashMap下的所有segment全部锁住来统计size
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); // force creation
            }
            sum = 0L;
            size = 0;
            overflow = false;
            //遍历segment 统计modCount和count
            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;
                    //size累加统计的count 并判断是否溢出
                    if (c < 0 || (size += c) < 0)
                        overflow = true;
                }
            }
            //如果该次统计modCount数与上次一致,说明这2次统计间没有写操作,这个size是正确的
            if (sum == last)
                break;
            last = sum;
        }
    } finally {
        //如果retries >2需要解锁所有segment
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}

要得到size就要把segment数组中的所有segment的count相加,在这期间有可能发生写操作(添加元素或删除元素)

最安全的方法: 把所有segment全锁起来统计count,效率低

JDK7ConcurrentHashMap的做法: 先不加锁尝试3次(retries = -1,0,1时)统计count和modCount(在发生写操作put,remove,clean时会改变modCount的值),如果前后2次统计的modCount相同说明没发生写操作,这个size是正确的,如果前后2次modCount值不同说明发生了写操作,size的值不一定正确,超过3次后则使用最安全的方法

ConcurrentLinkedQueue的实现原理

ConcurrentLinkedQueue采用CAS+失败重试的方式实现非阻塞式的线程安全队列

ConcurrentLinkedQueue结构
public class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
        implements Queue<E>, java.io.Serializable {

ConcurrentLinked继承自AbstractQueue抽象队列,实现了Queue队列接口和可序列化接口

在这里插入图片描述

ConcurrentLinkedQueue由head,tail节点组成

在这里插入图片描述

每个节点由==数据域: 节点元素(item)指针域: 指向下一个节点的引用(next)==组成

在这里插入图片描述

ConcurrentLinkedQueue初始化

查看ConcurrentLinkedQueue的无参构造

public ConcurrentLinkedQueue() {
    head = tail = new Node<E>(null);
}

查看构造可以知道ConcurrentLinkedQueue初始化时,头尾节点是同一个节点且这个节点元素为null(next也为null)

在这里插入图片描述

ConcurrentLinkedQueue操作

如果使用Idea debug ConcurrentLinkedQueue (巨坑)

在这里插入图片描述
取消这俩个勾,否则debug 会修改head,导致debug与分析的不一致

队列的主要操作由:入队,出队

入队操作

入队:就是将新元素加入队列的尾部

ConcurrentLinkedQueue中采用HOPS设计: ConcurretnLinkedQueue中的首尾节点head,tail并不一定是队列中真正的首尾节点,因为内部实现时,并不是每次入队,出队操作都会CAS的更新首尾节点,因为每次入队,出队操作都CAS更新首尾节点效率不高

HOPS延迟更新首尾节点的设计,减少CAS更新次数,提高效率

虽然这会导致tail与真正的尾节点之间有一段距离,但这只需要再循环定位真正的尾节点就可以了

查看offer()源码

源码很难看懂,建议先看后面的流程和总结,最后看源码

情况A: p是真正尾节点(它的后继节点为空),CAS设置p的后继节点为新节点,然后判断tail是否为真正尾节点,不是则CAS更新tail为真正尾节点

情况B: p==p.next 说明poll的时候,当前节点被构建为哨兵节点(自己的后继节点是自己)

情况C: 定位真正的尾节点p

public boolean offer(E e) {
    //检查入队元素是否为空,为空抛出空指针异常
    checkNotNull(e);
    //构建数据域为e的新节点
    final Node<E> newNode = new Node<E>(e);
    //循环(失败重试) t:当前的尾节点 p:真正的尾节点
    for (Node<E> t = tail, p = t;;) {
        //q:p的下一个节点
        Node<E> q = p.next;
        //情况A:
        //如果q为空(p的下一个节点为空),说明p就是尾节点
        if (q == null) {
            //CAS操作设置尾节点p的next指向新节点 
            //更新失败则说明有其他线程入队了元素,使得期望值null比较不对导致更新失败
            if (p.casNext(null, newNode)) {
                //如果p(真正的尾节点)不等于t(tail)则CAS的更新tail为新节点(也就是p的下一个节点)
                //更新失败说明其他线程已经更新了tail
                if (p != t) 
                    casTail(t, newNode);  
                return true;
            }
        }
        //情况B:
        //如果p==q说明 有其他线程指向过poll将next指向了自己(在poll中可能会将原头节点自己指向自己构建为哨兵节点)
       	//在并发中遇到这种情况时,tail不在队列上了,需要跳转到head上
        else if (p == q)
            //t != (t = tail):原来的tail(t)与现在的tail不同,说明tail被修改过
            //tail被修改过,让p=修改过的tail
            //tail未被修改过,让p=head
            p = (t != (t = tail)) ? t : head;
        //情况C:定位真正的尾节点
        //说明此时的p不是真正的尾节点,要去定位真正的尾节点
        else
            //p != t:p不等于原来的tail
            //t != (t = tail):说明tail被修改过
            //p不是原来的tail且tail被修改过则让p=被修改过的tail
            //否则让p=q(p的下一个节点)
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

offer入队主要做的事情

  1. 定位真正的尾节点
  2. 添加新节点: 使用CAS将入队的新节点设置为尾节点的next节点 (根据tail是否等于尾节点判断,是否要更新tail)

如果tail就是真正的尾节点,那么会CAS更新尾节点的next节点为新节点,且不会CAS更新tail

如果tail不是真正的尾节点,那么会先定位到真正的尾节点,再CAS更新尾节点的next节点为新节点,再CAS更新tail

入队方法只会返回true,所以不要通过返回值判断是否入队成功

出队操作

出队:就是将队列头部第一个有效节点出队

查看出队poll源码

源码很难看懂,建议先看后面的流程和总结,最后看源码

public E poll() {
    restartFromHead:
    for (;;) {
        //head不一定是真正头节点 p:真正头节点
        for (Node<E> h = head, p = h, q;;) {
            //拿到p的数据域
            E item = p.item;
			//如果p的数据域不为空 则 CAS将数据域更新为null
            if (item != null && p.casItem(item, null)) {
                //如果head不是真正头节点时 更改头节点
                if (p != h) 
                    updateHead(h, ((q = p.next) != null) ? q : p);
                //返回p的数据域
                return item;
            }
            //如果p的数据域为空且下一个节点为空 说明是空队列 更改头节点 返回null
            else if ((q = p.next) == null) {
                updateHead(h, p);
                return null;
            }
            //如果 p==p.next(哨兵节点) 进入下一次循环
            else if (p == q)
                continue restartFromHead;
            //p=q:用当前节点的下一个节点去下一轮循环查看数据域是否为空
            else
                p = q;
        }
    }
}

//h:head p:真正头节点
final void updateHead(Node<E> h, Node<E> p) {
    	//如果head不为真正头节点才CAS更新head
        if (h != p && casHead(h, p))
            //更新完head后,h不为head,h自己指向自己(构建哨兵节点)
            h.lazySetNext(h);
}

poll操作主要做的事

  1. 定位真正的头节点
  2. 删除头节点 : 数据域设置为null (根据head节点是不是真正的头节点判断要不要CAS更新head并构建新节点)

当head节点中有数据域时,说明head节点是真正头节点是待删除的,CSA更新数据域为null,不会更新head

当head节点中没数据域时,说明head节点不是真正头节点,先寻找真正头节点(p=q=p.next),找到后再CAS更新数据域为null,再调用updateHead更新head和构造哨兵节点

单线程情况下的入队

在IDEA下Debug第一次offerCAS加入新节点后,会出现1、tail.next 指向了tail本身 2、head 指向了newNode 3、p.next 也指向了tail本身的错误信息

ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
queue.add(1);
queue.add(2);

初始化

在这里插入图片描述

第一次入队

第一次入队时,t=tail,p=t,q=p.next

此时的p就是真正的尾节点,q为空,符合情况A

先CAS设置 尾节点.next = 新节点

因为此时的 p==t 所以不更新tail

在这里插入图片描述

第二次入队

此时t=tail,p=t,q=p.next

q为Node1,不为空,所以不会进入情况A

q也不等于p,所以不会进入情况B

所以进入情况C,定位真正尾节点后此时p = q = Node1

再次循环

q = p.next = null

符合情况A,CAS操作p.next = 新节点

因为 p != t 说明tail此时不是真正的尾节点,CSA更新tail为真正尾节点

在这里插入图片描述

单线程情况下的出队

初始情况

在这里插入图片描述

第一次出队

第一轮循环: head数据域为空,不是真正头节点,p=q(q=p.next),下一个节点试探

第二轮循环: p数据域为非空,CAS设置p数据域为null,因为p!=h(head不是真正的头节点),所以要更新head ,构建哨兵节点

在这里插入图片描述

第二次出队

head数据域非空,CAS更新为null

在这里插入图片描述

多线程的入队出队

offer-poll

在这里插入图片描述

线程A 进行offer 操作 ,线程B进行poll操作

线程A发现q=p.next不为空, 线程B发现head数据域为空更新head构建哨兵节点 线程B执行完

在这里插入图片描述

线程A 此时的p是哨兵节点 p=q=p.next

if (p == q)
    p = (t != (t = tail)) ? t : head;

发现 t == (t = tail) tail未被修改过 于是 p = head

在这里插入图片描述

下一轮循环发现q=p.next=null 当前p是真正尾节点,添加新节点返回

在这里插入图片描述

总结

ConcurrentLinkedQueue 是采用 CAS+失败重试 实现非阻塞式的线程安全队列

  • 入队

    • 先定位到真正的尾节点
    • 再使用CAS操作设置添加的新节点
    • 根据本次入队的tail是否是真正的尾节点判断是否要更新tail
      • tail是真正尾节点 则不更新
      • tail不是真正尾节点 则更新
  • 出队

    • 先定位到真正头节点

    • 再使用CAS操作设置真正头节点的数据域(item)为null

    • 根据本次出队的head是否是真正头节点判断是否要更新head,创建哨兵节点(原来的head自己指向自己变为哨兵节点)

      • head是真正头节点,不更新head,不创建哨兵节点
      • head不是真正头节点,更新head,创建哨兵节点

      创建哨兵节点是为了并发中的offer时能够定位到真正尾节点

Java中的阻塞队列

阻塞队列的4种处理方法
API介绍
方法名抛出异常特殊返回值阻塞等待定时阻塞等待
添加add(Object)offer(Object)put(Object)offer(Object,long,TimeUnit)
删除remove()poll()take()poll(long,TimeUnit)
查看对头元素element()peek()
使用方式

使用4种处理方式

/**
     * 测试阻塞队列抛出异常的处理方式add,remove
     * 队满add 抛出异常IllegalStateException
     * 队空remove 抛出异常NoSuchElementException
     */
    @Test
    public void test1(){
        ArrayBlockingQueue<Integer> abq = new ArrayBlockingQueue(3);
        abq.add(1);
        abq.add(2);
        abq.add(3);
        //抛出异常 java.lang.IllegalStateException: Queue full
        //abq.add(4);
        System.out.println(abq.remove());//1
        System.out.println(abq.remove());//2
        System.out.println(abq.remove());//3
        //抛出异常 java.util.NoSuchElementException
        //System.out.println(abq.remove());
    }

    /**
     * 测试阻塞队列处理方式offer,poll
     * 队满offer 返回false
     * 队空poll  返回null
     */
    @Test
    public void test2()  {
        LinkedBlockingQueue<Integer> lbq = new LinkedBlockingQueue<>(3);
        System.out.println(lbq.offer(1));//true
        System.out.println(lbq.offer(2));//true
        System.out.println(lbq.offer(3));//true
        System.out.println(lbq.offer(4));//false
        System.out.println(lbq.poll());//1
        System.out.println(lbq.poll());//2
        System.out.println(lbq.poll());//3
        System.out.println(lbq.poll());//null
    }

    /**
     * 测试阻塞队列处理方式put,take
     * 队满put阻塞
     * 队空take阻塞
     * @throws InterruptedException
     */
    @Test
    public void test3() throws InterruptedException {
        LinkedBlockingQueue<Integer> lbq = new LinkedBlockingQueue<>(3);
        lbq.put(1);
        lbq.put(1);
        lbq.put(1);
//        队满阻塞
//        lbq.put(1);
        lbq.take();
        lbq.take();
        lbq.take();
//        队空阻塞
//        lbq.take();
    }

    /**
     * 测试阻塞队列处理方式offer(e,timeout,TimeUnit),poll(timeout,TimeUnit)
     * 队满offer阻塞n时间
     * 队空poll阻塞n时间
     * @throws InterruptedException
     */
    @Test
    public void test4() throws InterruptedException {
        LinkedBlockingQueue<Integer> lbq = new LinkedBlockingQueue<>(3);
        System.out.println(lbq.offer(1, 2, TimeUnit.SECONDS));//true
        System.out.println(lbq.offer(1, 2, TimeUnit.SECONDS));//true
        System.out.println(lbq.offer(1, 2, TimeUnit.SECONDS));//true
        //队满阻塞2秒
        System.out.println(lbq.offer(1, 2, TimeUnit.SECONDS));//false

        System.out.println(lbq.poll(1, TimeUnit.SECONDS));//1
        System.out.println(lbq.poll(1, TimeUnit.SECONDS));//1
        System.out.println(lbq.poll(1, TimeUnit.SECONDS));//1
        //队空阻塞2秒
        System.out.println(lbq.poll(1, TimeUnit.SECONDS));//null
    }
总结
  1. 抛出异常:队满add 抛出异常IllegalStateExceptio队空remove 抛出异常NoSuchElementException
  2. 特殊返回值: 队满offer返回false,队空poll返回null
  3. 阻塞等待: 队满时put会阻塞线程 或 队空时take会阻塞线程
  4. 超时阻塞等待: 在阻塞等待的基础上超时退出(使用的是offer,poll)
阻塞队列

公平与不公平

公平指的是阻塞的线程按照先后顺序访问队列

如果该阻塞队列满了,还阻塞了很多线程,这些阻塞的线程按照先后顺序来访问阻塞队列

不公平就是阻塞的线程竞争访问队列

如果该阻塞队列满了,还阻塞了很多线程,这些阻塞的线程争抢访问阻塞队列

ArrayBlockingQueue

ArrayBlockingQueue是数组实现默认不公平的有界阻塞队列,阻塞队列中按照FIFO进行排序

查看ArrayBlockingQueue构造器源码

public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}

	public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        //锁是否为公平锁
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

ArrayBlockingQueue的公平性是由ReentrantLock来实现的

LinkedBlockingQueue

LinkedBlockingQueue是链表实现的有界阻塞队列,阻塞队列按照FIFO进行排序

PriorityBlockingQueue

PriorityBlockingQueue是优先级排序的无界阻塞队列,阻塞队列按照优先级进行排序

优先级排序

  1. 默认: 自然顺序升序

  2. 构造器中指定比较器Comparator 根据比较器规则排序

    	public PriorityQueue(int initialCapacity) {
        	this(initialCapacity, null);
    	}
    
    	public PriorityQueue(int initialCapacity,
                             Comparator<? super E> comparator) {
            // Note: This restriction of at least one is not actually needed,
            // but continues for 1.5 compatibility
            if (initialCapacity < 1)
                throw new IllegalArgumentException();
            this.queue = new Object[initialCapacity];
            this.comparator = comparator;
        }
    
  3. 阻塞队列中的元素类实现Comparable接口重写compareTo()方法 根据compareTo方法规则排序

相同优先级无法保证顺序

DelayQueue

Delay是一个延时获取元素的无界阻塞队列 延时最长排在队尾

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {

Delay队列元素实现Delayed接口指定延时时间

DelayQueue应用场景

  1. 缓存系统的设计: DelayQueue存放缓存有效期,当可以获取到元素时,说明缓存过期
  2. 定时任务调度: 将定时任务的时间设置为延时时间,一旦可以获取到任务就开始执行

实现Delay接口

public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}

ScheduledThreadPoolExecutorScheduledFutureTask为例子

在这里插入图片描述

  1. 创建对象时,初始化数据

    		ScheduledFutureTask(Runnable r, V result, long ns, long period) {
                super(r, result);
                //time记录当前对象延迟到什么时候可以使用,单位是纳秒
                this.time = ns;
                this.period = period;
                //sequenceNumber记录元素在队列中先后顺序  sequencer原子自增
                //AtomicLong sequencer = new AtomicLong();
                this.sequenceNumber = sequencer.getAndIncrement();
            }
    
  2. 实现Delayed接口的getDelay方法 返回当前元素还要延时多久

    public long getDelay(TimeUnit unit) {
        return unit.convert(time - now(), NANOSECONDS);
    }
    
  3. Delay接口继承了Comparable接口,目的是要实现compareTo方法来继续排序 延时最长的元素排队尾

    		public int compareTo(Delayed other) {
                if (other == this) // compare zero if same object
                    return 0;
                if (other instanceof ScheduledFutureTask) {
                    ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
                    long diff = time - x.time;
                    if (diff < 0)
                        return -1;
                    else if (diff > 0)
                        return 1;
                    else if (sequenceNumber < x.sequenceNumber)
                        return -1;
                    else
                        return 1;
                }
                long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
                return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
            }
    

实现延时阻塞队列

当消费者去延时阻塞队列中获取元素时,如果元素没有到延时时间就阻塞线程

SynchronousQueue

SynchrinousQueue是一个默认下支持非公平不存储元素的阻塞队列

每个put操作要等待一个take操作,否则不能继续添加元素会阻塞

使用公平锁

@Test
    public void test() throws InterruptedException {
        final SynchronousQueue<Integer> queue = new SynchronousQueue<Integer>(true);
        new Thread(() -> {
            try {
                queue.put(1);
                queue.put(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "put12线程").start();

        new Thread(() -> {
            try {
                queue.put(3);
                queue.put(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "put34线程").start();

        TimeUnit.SECONDS.sleep(1);
        System.out.println(Thread.currentThread().getName() + "拿出" + queue.take());
        TimeUnit.SECONDS.sleep(1);
        System.out.println(Thread.currentThread().getName() + "拿出" + queue.take());
        TimeUnit.SECONDS.sleep(1);
        System.out.println(Thread.currentThread().getName() + "拿出" + queue.take());
        TimeUnit.SECONDS.sleep(1);
        System.out.println(Thread.currentThread().getName() + "拿出" + queue.take());
    }
/*
main拿出1
main拿出3
main拿出2
main拿出4
*/

SynchronousQueue队列本身不存储元素,负责把生产者的数据传递给消费者,适合传递性的场景,在该场景下吞吐量会比ArrayBlockingQueue,LinkedBlockingQueue高

LinkedTransferQueue

LinkedTransferQueue是一个链表组成的无界阻塞队列,拥有transfer()tryTransfer()方法

  • transfer()

    如果有消费者在等待接收元素,transfer(e)会把元素e传输给消费者

    如果没有消费者在等待接收元素,transfer(e)会将元素e存放在队尾,直到有消费者获取了才返回

    	@Test
        public void test() throws InterruptedException {
            LinkedTransferQueue queue = new LinkedTransferQueue();
            new Thread(()->{
                try {
                    queue.transfer(1);
                    System.out.println(Thread.currentThread().getName()+"放入的1被取走了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"生产者").start();
    
            TimeUnit.SECONDS.sleep(3);
            System.out.println(Thread.currentThread().getName()+"取出队列中的元素");
            queue.poll();
        }
    /*
    main取出队列中的元素
    生产者放入的1被取走了
    */
    
  • tryTransfer()

    无论消费者是否消费都直接返回

    	@Test
        public void testTryTransfer() throws InterruptedException {
            LinkedTransferQueue<Integer> queue = new LinkedTransferQueue<>();
            System.out.println(queue.tryTransfer(1));//false
            System.out.println(queue.poll());//null
    
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName()+"取出"+queue.poll(2, TimeUnit.SECONDS));//消费者取出2
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"消费者").start();
            TimeUnit.SECONDS.sleep(1);
            System.out.println(queue.tryTransfer(2));//true
        }
    /*
    false
    null
    true
    消费者取出2
    */
    
  • tryTransfer(long,TimeUnit)

    在超时时间内消费者消费元素返回true,反之返回false

LinkedBlockingDeque

LinkedBlockingDeque是链表组成的双向阻塞队列

与其他阻塞队列不同的地方是: 它是双向的,支持在队头或队尾进行添加或删除

一系列带有First是对队头进行操作的方法,一系列带有Las是对队尾进行操作的方法

从添加来看add,offer都是从队尾添加,而take从队首添加

在这里插入图片描述

所以使用LinkedBlockingDeque时还是使用带First和Last方法比较好

LinkedBlockingDeque因为多了一个操作队列的入口,所以被用来实现工作窃取算法目的是可以减少线程的竞争

阻塞队列实现原理

通知模式实现阻塞队列

  • 生产者
    • 如果阻塞队列满了,生产者线程等待(等待消费者唤醒)
    • 如果阻塞队列没满就生产,生产元素加入队列,唤醒消费者消费
  • 消费者
    • 如果阻塞队列空了,消费者就等待(等待生产者唤醒)
    • 如果阻塞队列没空就消费,从队列中取出元素,唤醒生产者生产

查看ArrayBlockingQueue源码

使用Condition实现通知模式

/** Condition for waiting takes 作用于take方法*/
private final Condition notEmpty;

/** Condition for waiting puts 作用于put方法 */
private final Condition notFull;

public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
}

查看put

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    //尝试获取锁
    lock.lockInterruptibly();
    try {
        //如果队列满了 线程等待进入notFull等待队列中
        while (count == items.length)
            notFull.await();
        //队列没满 或者 被消费者唤醒后执行enqueue
        enqueue(e);
    } finally {
        //释放锁
        lock.unlock();
    }
}

	private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        //将元素加入队列
        items[putIndex] = x;
        //如果下标为队列长度更新为0 (从头开始)
        if (++putIndex == items.length)
            putIndex = 0;
        //更新元素个数
        count++;
        //唤醒 notEmpty中等待队列中的消费者线程
        notEmpty.signal();
    }

查看take方法

	public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        //尝试获取锁
        lock.lockInterruptibly();
        try {
            //如果队列为空(没有元素) 消费者线程等待 进入notEmpty等待队列中
            while (count == 0)
                notEmpty.await();
            //队列不为空  或 被生产者线程唤醒后 执行dequeue
            return dequeue();
        } finally {
            //释放锁
            lock.unlock();
        }
    }

	private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        //取出元素
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        ////如果下标为队列长度更新为0 (从头开始)
        if (++takeIndex == items.length)
            takeIndex = 0;
        //更新元素个数
        count--;
        if (itrs != null)
            //等待队列中代表该线程的节点出队
            itrs.elementDequeued();
        //唤醒notFull等待队列中的生产者线程
        notFull.signal();
        return x;
    }

查看condition的await 这里的condition是AQS下的ConditionObject

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

使用LockSupport工具类阻塞线程(线程进入等待状态 )

查看LockSupport的park方法

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}

先使用setBlocker保存要阻塞的线程,调用UNSAFE.park阻塞当前线程

//var2: 是延迟时间
public native void park(boolean var1, long var2);

park方法返回的情况:

  1. 与park对应的unpark执行
  2. 线程被中断
  3. 等待完指定的var2毫秒
  4. 发生异常

Fork/Join框架

了解For/Join框架

什么是Fork/Join框架

Java 7 提供 并发执行任务的框架 先分割(Fork)再合并(Join)

  1. 将一个任务分割为多个子任务,子任务再分割,直到分割达到阈值(自己设置阈值),不能分割为止
  2. 执行任务,任务完成合并结果

工作流程

在这里插入图片描述

工作窃取算法

在这里插入图片描述

将任务分割为互不依赖的子任务,将子任务分别放到不同队列中,每个队列都创建一个单独线程来执行队列中的任务 线程和队列一一对应

当线程A执行完队列A中的所有任务时,为了提高吞吐量不会让线程A闲下来,于是工作窃取算法让线程A去隔壁还有任务的队列窃取一个工作

为了减少线程竞争任务,队列可以采用双端队列,被窃取线程从队列头部获取任务,窃取线程去其他队列尾部窃取任务 只有队列中一个任务时才可能发生竞争

  • 优点: 充分利用线程并行计算,减少线程间的竞争
  • 缺点: 队列中只有一个任务时还是可能发生竞争, 创建双端队列消耗更多空间资源
使用Fork/Join框架

使用说明

使用ForkJoin框架需要创建ForkJoin任务(ForkJoinTask),ForkJoinTask提供在任务中执行fork(),join()的机制

一般不用直接继承ForkJoinTask,而是继承它的子类RecursiveActionRecursiveTask

  • RecursiveTask:用于有返回结果的任务
  • RecursiveAction:用于无返回结果的任务

在这里插入图片描述

它们都是抽象类,继承它们去实现compute()方法

compute()方法中就是处理任务的逻辑(根据自己的需求去实现)

ForkJoinTask需要通过ForkJoinPool来执行

使用过程

/**
 * @author Tc.l
 * @Date 2020/12/27
 * @Description:
 * 从1累加到1w
 */
public class ForkJoinTest extends RecursiveTask<Integer> {
    private int start;
    private int end;

    public ForkJoinTest(int start, int end) {
        this.start = start;
        this.end = end;
    }

	//继承有返回值结果的任务RecursiveTask 实现compute方法
    @Override
    protected Integer compute() {
        boolean flag = (end - start) <= 100;
        int temp = 0;
        //当任务范围在100之外时,分割任务
        //递归的思路一直分割到任务范围符合时再执行任务返回结果,最后合并返回
        if (!flag) {
            temp = (end + start) >> 1;
            ForkJoinTest left = new ForkJoinTest(start, temp);
            ForkJoinTest right = new ForkJoinTest(temp + 1, end);
            left.fork();
            right.fork();
            return left.join() + right.join();
        } else {
        //当任务范围在100之内时,执行任务    
            for (int i = start; i <= end; i++) {
                temp += i;
            }
            return temp;
        }
    }

    public static void main(String[] args) {
        ForkJoinTest test = new ForkJoinTest(1, 10000);
        ForkJoinPool pool = new ForkJoinPool();
        //创建ForkJoinPool,把ForkJoinTask放入池中执行
        ForkJoinTask result = pool.submit(test);
        //isCompletedAbnormally():判断任务是否被取消或抛出异常 如果有则返回true
        if (test.isCompletedAbnormally()){
            //getException(): 任务被取消则返回null 抛出异常则返回异常名
            System.out.println(test.getException());
        }
        try {
            //获得任务结果值
            System.out.println(result.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

// 50005000
  • isCompletedAbnormally()可以判断该任务是否被取消或者抛出了异常
  • getException(): 1. 任务被取消时返回null 2.抛出异常时返回异常名
Fork/Join框架实现原理

ForkJoinPool由 存放ForkJoin任务的数组 和执行任务的ForkJoinWorkerThread数组组成

查看fork源码

public final ForkJoinTask<V> fork() {
    Thread t;
    if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
        //如果当前线程是工作线程则调用工作线程的push方法
        ((ForkJoinWorkerThread)t).workQueue.push(this);
    else
        //如果当前线程不是工作线程则把任务放在静态池里
        ForkJoinPool.common.externalPush(this);
    return this;
}

查看push方法

		final void push(ForkJoinTask<?> task) {
            ForkJoinTask<?>[] a; ForkJoinPool p;
            int b = base, s = top, n;
        	//ForkJoinTask<?>[] array :存放任务的数组
        	//WorkQueue[] workQueues :执行任务的数组
            if ((a = array) != null) {    // ignore if queue removed
                int m = a.length - 1;     // fenced write for task visibility
                //将任务加入存放任务的数组array中
                U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);
                U.putOrderedInt(this, QTOP, s + 1);
                if ((n = s - b) <= 1) {
                    if ((p = pool) != null)
                        //唤醒(创建)一个工作线程去执行任务
                        p.signalWork(p.workQueues, this);
                }
                else if (n >= m)
                    growArray();
            }
        }

查看join源码

任务运行状态

/** The run status of this task */
volatile int status; // accessed directly by pool and workers
static final int DONE_MASK   = 0xf0000000;  // mask out non-completion bits
//已完成
static final int NORMAL      = 0xf0000000;  // must be negative  
//被取消
static final int CANCELLED   = 0xc0000000;  // must be < NORMAL
//发生异常
static final int EXCEPTIONAL = 0x80000000;  // must be < CANCELLED
//信号
static final int SIGNAL      = 0x00010000;  // must be >= 1 << 16
static final int SMASK       = 0x0000ffff;  // short bits for tags

查看join方法

	public final V join() {
        int s;
        //根据任务状态
        if ((s = doJoin() & DONE_MASK) != NORMAL)
            //状态不是已完成则根据情况抛出异常
            reportException(s);
        //状态是已完成则返回结果
        return getRawResult();
    }

	private void reportException(int s) {
        //任务状态是被取消则抛出CancellationException
        if (s == CANCELLED)
            throw new CancellationException();
        //任务状态是发生异常则抛出相应异常
        if (s == EXCEPTIONAL)
            rethrow(getThrowableException());
    }

查看doJoin方法

private int doJoin() {
    int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
    //状态小于0说明已完成 直接返回
    return (s = status) < 0 ? s :
        ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
        (w = (wt = (ForkJoinWorkerThread)t).workQueue).
        tryUnpush(this) && (s = doExec()) < 0 ? s :
        wt.pool.awaitJoin(w, this, 0L) :
    	//阻塞当前线程直到完成
        externalAwaitDone();
}
  1. 先查看任务状态,如果是已完成则直接退出
  2. 没执行完则从任务数组中取出任务并执行
  3. 执行完任务则设置状态,发生异常也要设置任务状态
  4. 阻塞当前线程直到任务完成或发生异常返回
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值