JUC并发—9.并发安全集合三

大纲

1.并发安全的数组列表CopyOnWriteArrayList

2.并发安全的链表队列ConcurrentLinkedQueue

3.并发编程中的阻塞队列概述

4.JUC的各种阻塞队列介绍

5.LinkedBlockingQueue的具体实现原理

6.基于两个队列实现的集群同步机制

1.并发安全的数组列表CopyOnWriteArrayList

(1)CopyOnWriteArrayList的初始化

(2)基于锁 + 写时复制机制实现的增删改操作

(3)使用写时复制的原因是读操作不加锁 + 不使用Unsafe读取数组元素

(4)对数组进行迭代时采用了副本快照机制

(5)核心思想是通过弱一致性提升读并发

(6)写时复制的总结

(1)CopyOnWriteArrayList的初始化

并发安全的HashMap是ConcurrentHashMap

并发安全的ArrayList是CopyOnWriteArrayList

并发安全的LinkedList是ConcurrentLinkedQueue

从CopyOnWriteArrayList的构造方法可知,CopyOnWriteArrayList基于Object对象数组实现。

这个Object对象数组array会使用volatile修饰,保证了多线程下的可见性。只要有一个线程修改了数组array,其他线程可以马上读取到最新值。

//A thread-safe variant of java.util.ArrayList in which all mutative operations 
//(add, set, and so on) are implemented by making a fresh copy of the underlying array.
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { 
    ...
    //The lock protecting all mutators
    final transient ReentrantLock lock = new ReentrantLock();

    //The array, accessed only via getArray/setArray.
    private transient volatile Object[] array;
    
    //Creates an empty list.
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
    
    //Sets the array.
    final void setArray(Object[] a) {
        array = a;
    }
    ...
}

(2)基于锁 + 写时复制机制实现的增删改操作

一.使用独占锁解决对数组的写写并发问题

每个CopyOnWriteArrayList都有一个Object数组 + 一个ReentrantLock锁。在对Object数组进行增删改时,都要先获取锁,保证只有一个线程增删改。从而确保多线程增删改CopyOnWriteArrayList的Object数组是并发安全的。注意:获取锁的动作需要在执行getArray()方法前执行。

但因为获取独占锁,所以导致CopyOnWriteArrayList的写并发并性能不太好。而ConcurrentHashMap由于通过CAS设置 + 分段加锁,所以写并发性能很高。

二.使用写时复制机制解决对数组的读写并发问题

CopyOnWrite就是写时复制。写数据时不直接在当前数组里写,而是先把当前数组的数据复制到新数组里。然后再在新数组里写数据,写完数据后再将新数组赋值给array变量。这样原数组由于没有了array变量的引用,很快就会被JVM回收掉。

其中会使用System.arraycopy()方法和Arrays.copyOf()方法来复制数据到新数组,从Arrays.copyOf(elements, len + 1)可知,新数组的大小比原数组大小多1。

所以CopyOnWriteArrayList不需要进行数组扩容,这与ArrayList不一样。ArrayList会先初始化一个固定大小的数组,然后数组大小达到阈值时会扩容。

三.总结

为了解决CopyOnWriteArrayList的数组写写并发问题,使用了锁。

为了解决CopyOnWriteArrayList的数组读写并发问题,使用了写时复制。

所以CopyOnWriteArrayList可以保证多线程对数组写写 + 读写的并发安全。

//A thread-safe variant of java.util.ArrayList in which all mutative operations 
//(add, set, and so on) are implemented by making a fresh copy of the underlying array.
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { 
    ...
    //The lock protecting all mutators
    final transient ReentrantLock lock = new ReentrantLock();

    //The array, accessed only via getArray/setArray.
    private transient volatile Object[] array;
    
    //Creates an empty list.
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
    
    //Sets the array.
    final void setArray(Object[] a) {
        array = a;
    }
    
    //Gets the array. Non-private so as to also be accessible from CopyOnWriteArraySet class.
    final Object[] getArray() {
        return array;
    }
    
    //增:Appends the specified element to the end of this list.
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
    
    //删:Removes the element at the specified position in this list.
    //Shifts any subsequent elements to the left (subtracts one from their indices).  
    //Returns the element that was removed from the list.
    public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            if (numMoved == 0) {
                setArray(Arrays.copyOf(elements, len - 1));
            } else {
                //先创建新数组,新数组的大小为len-1,比原数组的大小少1
                Object[] newElements = new Object[len - 1];
                //把原数组里从0开始拷贝index个元素到新数组里,并且从新数组的0位置开始放置
                System.arraycopy(elements, 0, newElements, 0, index);
                //把原数组从index+1开始拷贝numMoved个元素到新数组里,并且从新数组的index位置开始放置;
                System.arraycopy(elements, index + 1, newElements, index, numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }
    
    //改:Replaces the element at the specified position in this list with the specified element.
    public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);

            if (oldValue != element) {
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                //Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }
    ...
}

(3)使用写时复制的原因是读操作不加锁 + 不使用Unsafe读取数组元素

CopyOnWriteArrayList的增删改采用写时复制的原因在于get操作不需加锁。get操作就是先获取array数组,然后再通过index定位返回对应位置的元素。

由于在写数据的时候,首先更新的是复制了原数组数据的新数组。所以同一时间大量的线程读取数组数据时,都会读到原数组的数据,因此读写之间不会出现并发冲突的问题。

而且在写数据的时候,在更新完新数组之后,才会更新volatile修饰的数组变量。所以读操作只需要直接对volatile修饰的数组变量进行读取,就能获取最新的数组值。

如果不使用写时复制机制,那么即便有写线程先更新了array引用的数组中的元素,后续的读线程也只是具有对使用volatile修饰的array引用的可见性,而不会具有对array引用的数组中的元素的可见性。所以此时只要array引用没有发生改变,读线程还是会读到旧的元素,除非使用Unsafe.getObjectVolatile()方法来获取array引用的数组的元素。

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    ...
    //The array, accessed only via getArray/setArray.
    private transient volatile Object[] array;

    //Gets the array.  Non-private so as to also be accessible from CopyOnWriteArraySet class.
    final Object[] getArray() {
        return array;
    }

    public E get(int index) {
        //先通过getArray()方法获取array数组,然后再通过get()方法定位到数组某位置的元素
        return get(getArray(), index);
    }
    
    private E get(Object[] a, int index) {
        return (E) a[index];
    }
    ...
}

(4)对数组进行迭代时采用了副本快照机制

CopyOnWriteArrayList的Iterator迭代器里有一个快照数组snapshot,该数组指向的就是创建迭代器时CopyOnWriteArrayList的当前数组array。

所以使用CopyOnWriteArrayList的迭代器进行迭代时,会遍历快照数组。此时如果有其他线程更新了数组array,也不会影响迭代的过程。

public class CopyOnWriteArrayListDemo {
    static List<String> list = new CopyOnWriteArrayList<String>();
    public static void main(String[] args) {
        list.add("k");
        System.out.println(list);

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    ...
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
    ...
    
    static final class COWIterator<E> implements ListIterator<E> {
        private final Object[] snapshot;
        private int cursor;
        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }
        ...
    }
    ...
}

(5)核心思想是通过最终一致性提升读并发

CopyOnWriteArrayList的核心思想是通过弱一致性来提升读写并发的能力。

CopyOnWriteArrayList基于写时复制机制存在的最大问题是最终一致性。

多个线程并发读写数组,写线程已将新数组修改好,但还没设置给array。此时其他读线程读到的(get或者迭代)都是数组array的数据,于是在同一时刻,读线程和写线程看到的数据是不一致的。这就是写时复制机制存在的问题:最终一致性或弱一致性。

(6)写时复制的总结

一.优点

读读不互斥,读写不互斥,写写互斥。同一时间只有一个线程可以写,写的同时允许其他线程来读。

二.缺点

空间换时间,写的时候内存里会出现一模一样的副本,对内存消耗大。通过数组副本可以保证大量的读不需要和写互斥。如果数组很大,可能要考虑内存占用会是数组大小的几倍。此外使用数组副本来统计数据,会存在统计数据不一致的问题。

三.使用场景

适用于读多写少的场景,这样大量的读操作不会被写操作影响,而且不要求统计数据具有实时性。

2.并发安全的链表队列ConcurrentLinkedQueue

(1)ConcurrentLinkedQueue的介绍

(2)ConcurrentLinkedQueue的构造方法

(3)ConcurrentLinkedQueue的offer()方法

(4)ConcurrentLinkedQueue的poll()方法

(5)ConcurrentLinkedQueue的peak()方法

(6)ConcurrentLinkedQueue的size()方法

(1)ConcurrentLinkedQueue的介绍

ConcurrentLinkedQueue是一种并发安全且非阻塞的链表队列(无界队列)。

ConcurrentLinkedQueue采用CAS机制来保证多线程操作队列时的并发安全。

链表队列会采用先进先出的规则来对结点进行排序。每次往链表队列添加元素时,都会添加到队列的尾部。每次需要获取元素时,都会直接返回队列头部的元素。

并发安全的HashMap是ConcurrentHashMap

并发安全的ArrayList是CopyOnWriteArrayList

并发安全的LinkedList是ConcurrentLinkedQueue

(2)ConcurrentLinkedQueue的构造方法

ConcurrentLinkedQueue是基于链表实现的,链表结点为其内部类Node。

ConcurrentLinkedQueue的构造方法会初始化链表的头结点和尾结点为同一个值为null的Node对象。

Node结点通过next指针指向下一个Node结点,从而组成一个单向链表。而ConcurrentLinkedQueue的head和tail两个指针指向了链表的头和尾结点。

public class ConcurrentLinkedQueueDemo {
    public static void main(String[] args) {
        ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<String>();
        queue.offer("张三");//向队尾添加元素
        queue.offer("李四");//向队尾添加元素
        queue.offer("王五");//向队尾添加元素
        System.out.println(queue.peek());//返回队头的元素不出队
        System.out.println(queue.poll());//返回队头的元素而且出队
        System.out.println(queue.peek());//返回队头的元素不出队
    }
}

//An unbounded thread-safe queue based on linked nodes.
//This queue orders elements FIFO (first-in-first-out).
//The head of the queue is that element that has been on the queue the longest time.
//The tail of the queue is that element that has been on the queue the shortest time. 
//New elements are inserted at the tail of the queue, 
//and the queue retrieval operations obtain elements at the head of the queue.
//A ConcurrentLinkedQueue is an appropriate choice when many threads will share access to a common collection. 
//Like most other concurrent collection implementations, this class does not permit the use of null elements.
public class ConcurrentLinkedQueue<E> extends AbstractQueue<E> implements Queue<E>, java.io.Serializable {
    ...
    private transient volatile Node<E> head;
    private transient volatile Node<E> tail;
    
    //构造方法,初始化链表队列的头结点和尾结点为同一个值为null的Node对象
    //Creates a ConcurrentLinkedQueue that is initially empty.
    public ConcurrentLinkedQueue() {
        head = tail = new Node<E>(null);
    }
    
    private static class Node<E> {
        volatile E item;
        volatile Node<E> next;
       
        private static final sun.misc.Unsafe UNSAFE;
        private static final long itemOffset;
        private static final long nextOffset;
       
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> k = Node.class;
                itemOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("item"));
                nextOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("next"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
       
        Node(E item) {
            UNSAFE.putObject(this, itemOffset, item);
        }
  
        boolean casItem(E cmp, E val) {
            return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
        }
  
        void lazySetNext(Node<E> val) {
            UNSAFE.putOrderedObject(this, nextOffset, val);
        }
  
        boolean casNext(Node<E> cmp, Node<E> val) {
            return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
        }
    }
    ...
}

(3)ConcurrentLinkedQueue的offer()方法

其中关键的代码就是"p.casNext(null, newNode))",就是把p的next指针由原来的指向空设置为指向新的结点,并且通过CAS确保同一时间只有一个线程可以成功执行这个操作。

注意:更新tail指针并不是实时更新的,而是隔一个结点再更新。这样可以减少CAS指令的执行次数,从而降低CAS操作带来的性能影响。

插入第一个元素后,tail指针指向倒数第二个节点。

插入第二个元素后,tail指针指向最后一个节点。

插入第三个元素后,tail指针指向倒数第二个节点。

插入第四个元素后,tail指针指向最后一个节点。

//An unbounded thread-safe queue based on linked nodes.
public class ConcurrentLinkedQueue<E> extends AbstractQueue<E> implements Queue<E>, java.io.Serializable {
    ...
    private transient volatile Node<E> head;
    private transient volatile Node<E> tail;
    
    private static final sun.misc.Unsafe UNSAFE;
    private static final long headOffset;
    private static final long tailOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = ConcurrentLinkedQueue.class;
            headOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("head"));
            tailOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("tail"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
    
    //构造方法,初始化链表队列的头结点和尾结点为同一个值为null的Node对象
    //Creates a ConcurrentLinkedQueue that is initially empty.
    public ConcurrentLinkedQueue() {
        head = tail = new Node<E>(null);
    }
    
    public boolean offer(E e) {
        checkNotNull(e);
        final Node<E> newNode = new Node<E>(e);
        //插第一个元素时, tail和head都是初始化时的空节点, p也指向该空节点, q是该空节点的next元素;
        //很明显q是null, p.casNext后, p的next设为第一个元素, 此时p和t相等, tail的next是第一个元素;
        //由于p==t, 于是返回true, head和tail还是指向初始化时的空节点, tail指针指向的是倒数第二个节点;
        //插第二个元素时, q成为第一个元素,不为null了, 而且p指向tail, tail的next是第一个元素, 所以p != q;
        //由于此时p和t还是一样的, 所以会将q赋值给p, 也就是p指向第一个元素了, 再次进行新一轮循环;
        //新一轮循环时, q指向第一个元素的next成为null, 所以会对第一个元素执行casNext操作;
        //也就是将第二个元素设为第一个元素的next, 设完后由于p和t不相等了, 会执行casTail设第二个元素为tail;
        //插入第三个元素时, 又会和插入第一个元素一样了, 这时tail指针指向的是倒数第二个节点;
        //插入第四个元素时, 和插入第二个元素一样, 这是tail指针指向的是最后一个节点;
        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;//p是尾结点,q是尾结点的下一个结点
            if (q == null) {
                //插入第一个元素时执行的代码
                if (p.casNext(null, newNode)) {//将新结点设置为尾结点的下一个结点
                    if (p != t) {//隔一个结点再CAS更新tail指针
                        casTail(t, newNode);
                    }
                    return true;
                }
            } else if (p == q) {
                p = (t != (t = tail)) ? t : head;
            } else {
                //插入第二个元素时执行的代码
                p = (p != t && t != (t = tail)) ? t : q;
            }
        }
    }
    
    private boolean casTail(Node<E> cmp, Node<E> val) {
        return UNSAFE.compareAndSwapObject(this, tailOffset, cmp, val);
    }
    ...
}

(4)ConcurrentLinkedQueue的poll()方法

poll()方法会将链表队列的头结点出队。

注意:更新head指针时也不是实时更新的,而是隔一个结点再更新。这样可以减少CAS指令的执行次数,从而降低CAS操作带来的性能影响。

//An unbounded thread-safe queue based on linked nodes.
public class ConcurrentLinkedQueue<E> extends AbstractQueue<E> implements Queue<E>, java.io.Serializable {
    ...
    private transient volatile Node<E> head;
    private transient volatile Node<E> tail;
    
    private static final sun.misc.Unsafe UNSAFE;
    private static final long headOffset;
    private static final long tailOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = ConcurrentLinkedQueue.class;
            headOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("head"));
            tailOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("tail"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
    
    //构造方法,初始化链表队列的头结点和尾结点为同一个值为null的Node对象
    //Creates a ConcurrentLinkedQueue that is initially empty.
    public ConcurrentLinkedQueue() {
        head = tail = new Node<E>(null);
    }
    
    public E poll() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                E item = p.item;
                if (item != null && p.casItem(item, null)) {
                    if (p != h) {//隔一个结点才CAS更新head指针
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    }
                    return item;
                } else if ((q = p.next) == null) {
                    updateHead(h, p);
                    return null;
                } else if (p == q) {
                    continue restartFromHead;
                } else {
                    p = q;
                }
            }
        }
    }
  
    final void updateHead(Node<E> h, Node<E> p) {
        if (h != p && casHead(h, p)) {
            h.lazySetNext(h);
        }
    }
  
    private boolean casHead(Node<E> cmp, Node<E> val) {
        return UNSAFE.compareAndSwapObject(this, headOffset, cmp, val);
    }
    ...
}

(5)ConcurrentLinkedQueue的peak()方法

peek()方法会获取链表的头结点,但是不会出队。

//An unbounded thread-safe queue based on linked nodes.
public class ConcurrentLinkedQueue<E> extends AbstractQueue<E> implements Queue<E>, java.io.Serializable {
    ...
    private transient volatile Node<E> head;
    
    public E peek() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                E item = p.item;
                if (item != null || (q = p.next) == null) {
                    updateHead(h, p);
                    return item;
                } else if (p == q) {
                    continue restartFromHead;
                } else {
                    p = q;
                }
            }
        }
    }
    
    final void updateHead(Node<E> h, Node<E> p) {
        if (h != p && casHead(h, p)) {
            h.lazySetNext(h);
        }
    }
    
    private boolean casHead(Node<E> cmp, Node<E> val) {
        return UNSAFE.compareAndSwapObject(this, headOffset, cmp, val);
    }
    ...
}

(6)ConcurrentLinkedQueue的size()方法

size()方法主要用来返回链表队列的大小,查看链表队列有多少个元素。size()方法不会加锁,会直接从头节点开始遍历链表队列中的每个结点。

//An unbounded thread-safe queue based on linked nodes.
public class ConcurrentLinkedQueue<E> extends AbstractQueue<E> implements Queue<E>, java.io.Serializable {
    ...
    public int size() {
        int count = 0;
        for (Node<E> p = first(); p != null; p = succ(p))
            if (p.item != null) {
                if (++count == Integer.MAX_VALUE) {
                    break;
                }
            }
        }
        return count;
    }
   
    //Returns the first live (non-deleted) node on list, or null if none.
    //This is yet another variant of poll/peek; here returning the first node, not element.
    //We could make peek() a wrapper around first(), but that would cost an extra volatile read of item,
    //and the need to add a retry loop to deal with the possibility of losing a race to a concurrent poll(). 
    Node<E> first() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                boolean hasItem = (p.item != null);
                if (hasItem || (q = p.next) == null) {
                    updateHead(h, p);
                    return hasItem ? p : null;
                } else if (p == q) {
                    continue restartFromHead;
                } else {
                    p = q;
                }
            }
        }
    }
    
    //Returns the successor of p, or the head node if p.next has been linked to self, 
    //which will only be true if traversing with a stale pointer that is now off the list.
    final Node<E> succ(Node<E> p) {
        Node<E> next = p.next;
        return (p == next) ? head : next;
    }
    ...
}

如果在遍历的过程中,有线程执行入队或者是出队的操作,此时会怎样?

从队头开始遍历,遍历到一半时:如果有线程在队列尾部进行入队操作,此时的遍历能及时看到新添加的元素。因为入队操作就是设置队列尾部节点的next指针指向新添加的结点,而入队时设置next指针属于volatile写,因此遍历时是可以看到的。如果有线程从队列头部进行出队操作,此时的遍历则无法感知有元素出队了。

所以可以总结出这些并发安全的集合:ConcurrentHashMap、CopyOnWriteArrayList和ConcurrentLinkedQueue,为了优化多线程下的并发性能,会牺牲掉统计数据的一致性。为了保证多线程写的高并发性能,会大量采用CAS进行无锁化操作。同时会让很多读操作比如常见的size()操作,不使用锁。因此使用这些并发安全的集合时,要考虑并发下的统计数据的不一致问题。

3.并发编程中的阻塞队列概述

(1)什么是阻塞队列

(2)阻塞队列提供的方法

(3)阻塞队列的应用

(1)什么是阻塞队列

队列是一种只允许在一端进行移除操作、在另一端进行插入操作的线性表,队列中允许插入的一端称为队尾,允许移除的一端称为队头。

阻塞队列就是在队列的基础上增加了两个操作:

一.支持阻塞插入

在队列满时会阻塞继续往队列中添加数据的线程,直到队列中有元素被释放。

二.支持阻塞移除

在队列空时会阻塞从队列中获取元素的线程,直到队列中添加了新的元素。

阻塞队列其实实现了一个生产者/消费者模型:生产者往队列中添加数据,消费者从队列中获取数据。队列满了阻塞生产者,队列空了阻塞消费者。

阻塞队列中的元素可能会使用数组或者链表等来进行存储。一个队列中能容纳多少个元素取决于队列的容量大小,因此阻塞队列也分为有界队列和无界队列。

有界队列指有固定大小的队列,无界队列指没有固定大小的队列。实际上无界队列也是有大小限制的,只是大小限制为非常大,可认为无界。

注意:在无界队列中,由于理论上不存在队列满的情况,所以不存在阻塞。

阻塞队列在很多地方都会用到,比如线程池、ZooKeeper。一般使用阻塞队列来实现生产者/消费者模型。

(2)阻塞队列提供的方法

阻塞队列的操作有插入、移除、检查,在队列满或者空时会有不同的效果。

一.抛出异常

当队列满的时候通过add(e)方法添加元素,会抛出异常。

当队列空的时候调用remove(e)方法移除元素,也会抛出异常。

二.返回特殊值

调用offer(e)方法向队列入队元素时,会返回添加结果true或false。

调用poll()方法从队列出队元素时,会从队列取出一个元素或null。

三.一直阻塞

在队列满了的情况下,调用插入方法put(e)向队列中插入元素时,队列会阻塞插入元素的线程,直到队列不满或者响应中断才退出阻塞。

在队列空了的情况下,调用移除方法take()从队列移除元素时,队列会阻塞移除元素的线程,直到队列不为空时唤醒线程。

四.超时退出

超时退出其实就是在offer()和poll()方法中增加了阻塞的等待时间。

(3)阻塞队列的应用

阻塞队列可以理解为线程级别的消息队列。

消息中间件可以理解为进程级别的消息队列。

所以可以通过阻塞队列来缓存线程的请求,从而达到流量削峰的目的。

相关推荐
菠菠萝宝8 分钟前
【Java八股文】10-数据结构与算法面试篇
java·开发语言·面试·红黑树·跳表·排序·lru
不会Hello World的小苗15 分钟前
Java——链表(LinkedList)
java·开发语言·链表
Allen Bright1 小时前
【Java基础-46.3】Java泛型通配符详解:解锁类型安全的灵活编程
java·开发语言
柃歌1 小时前
【UCB CS 61B SP24】Lecture 7 - Lists 4: Arrays and Lists学习笔记
java·数据结构·笔记·学习·算法
柃歌1 小时前
【UCB CS 61B SP24】Lecture 4 - Lists 2: SLLists学习笔记
java·数据结构·笔记·学习·算法
是姜姜啊!1 小时前
redis的应用,缓存,分布式锁
java·redis·spring
梨落秋溪、1 小时前
输入框元素覆盖冲突
java·服务器·前端
hrrrrb1 小时前
【Java】Java 常用核心类篇 —— 时间-日期API(上)
java·开发语言
小突突突2 小时前
模拟实现Java中的计时器
java·开发语言·后端·java-ee
七禾页话2 小时前
垃圾回收知识点
java·开发语言·jvm