Davids原理探究:Java并发包中并发队列(ConcurrentLinkedQueue、LinkedBlockingQueue、DelayQueue...)

文章目录

    • 原理探究:Java并发包中并发队列
      • ConcurrentLinkedQueue
      • LinkedBlockingQueue
      • ArrayBlockingQueue
      • PriorityBlockingQueue
      • DelayQueue

原理探究:Java并发包中并发队列

关注可以查看更多粉丝专享blog~

ConcurrentLinkedQueue

线程安全的无界非阻塞队列,由单向链表实现,入队出队使用CAS来保证线程安全。
Davids原理探究:Java并发包中并发队列(ConcurrentLinkedQueue、LinkedBlockingQueue、DelayQueue...)_第1张图片

  1. 无界队列
  2. 无锁算法,入队和出队使用CAS算法进行设置队首和队尾元素
  3. 由于是无锁算法,所以在获取size的时候是进行遍历操作的,在遍历过程中,已经遍历过的节点可能有增删,所以size在高并发场景下存在一定误差,而且size性能较差,所以如果只是判断队列是否有元素建议使用isEmpty(),该方法只会获取first节点是否有元素。
  4. 初始化队列时头尾节点均指向哨兵节点head = tail = new Node(null);
public int size() {
  int count = 0;
  	// 循环遍历,效率低,由于是无锁算法,所以size在高并发情况下不准确
    for (Node<E> p = first(); p != null; p = succ(p))
        if (p.item != null)
            // Collection.size() spec says to max out
            if (++count == Integer.MAX_VALUE)
                break;
    return count;
}

(题外话:在判断字符串是否为空的时候惯用写法是(null == str || “”.equals(str))其实String的equals方法也是遍历字符串,建议使用(cs == null || cs.length() == 0),还有习惯用StringUtils的,org.apache.commons.lang3.StringUtils和org.springframework.util.StringUtils是不一样的,spring使用的是equals,lang3使用的是length,建议自己封装StringUtils去继承lang3,后期可以根据自己需要去修改继承或者覆盖方法)

LinkedBlockingQueue

有界阻塞队列,由单向链表和独占锁实现。
Davids原理探究:Java并发包中并发队列(ConcurrentLinkedQueue、LinkedBlockingQueue、DelayQueue...)_第2张图片

  1. 生产消费模型
  2. 有界队列Integer.MAX_VALUE,也可以在new的时候自行制定capacity
  3. 加锁,take锁和put锁,notFull Condition条件队列控制生产者,notEmpty Condition条件队列控制消费者
    1. put锁:在每次执行offer(无阻塞尾部入队enqueue(node),已满则丢弃)、put(while阻塞入队,若队列满则等待,可被中断,中断后抛出InterruptedException)后若队列没有满则调用notFull.signal()唤醒生产者条件队列进行first节点进行生产,unlock之后判断操作前如果队列中没有元素(count.getAndIncrement() == 0)则会调用signalNotEmpty唤醒消费者条件队列的first节点进行消费;
    2. take锁:在每次执行poll(无阻塞头部出队dequeue(node))、take(while阻塞头部出队,若队列空则等待,可被中断,中断后抛出InterruptedException)后若队列还有数据则调用notEmpty.signal()唤醒消费者条件队列的first节点进行消费,unlock之后判断操作前如果队列是否满(count.getAndDecrement() == capacity)则会调用signalNotFull唤醒生产者条件队列的first节点进行消费;
    3. take锁:peek操作与poll操作相似,但只是窥视,不出队。
    4. 双重加锁(put、take锁):remove遍历查找,找到则调用unlink删除,并链接前置节点和后置节点,操作完之后如果队列是否满(count.getAndDecrement() == capacity)则会调用signalNotFull唤醒生产者条件队列的first节点进行消费;
    5. size使用AtomicInteger存储,结果准确,获取size复杂度O(1)。
public boolean offer(E e) {
	// 为空直接抛出空指针
   if (e == null) throw new NullPointerException();
    final AtomicInteger count = this.count;
    // 如果队列已满直接返回false
    if (count.get() == capacity)
        return false;
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    // 获取put锁
    putLock.lock();
    try {
    	// 如果队列未满直接调用enqueue入队,count+1
        if (count.get() < capacity) {
        	// put(E e)与offer区别在于put在获取可中断锁(putLock.lockInterruptibly();)
        	// count.get() == capacity时while阻塞,offer无阻塞直接返回
        	// enqueue方法: last = last.next = node;
            enqueue(node);
            c = count.getAndIncrement();
            // 入队后如果队列未满则唤醒生产者
            if (c + 1 < capacity)
                notFull.signal();
        }
    } finally {
    	// 释放锁
        putLock.unlock();
    }
    if (c == 0)
    	// 如果添加前队列为空则唤醒消费者消费当前入队元素
        signalNotEmpty();
    return c >= 0;
}

ArrayBlockingQueue

有界阻塞队列,由数组和独占锁实现。
Davids原理探究:Java并发包中并发队列(ConcurrentLinkedQueue、LinkedBlockingQueue、DelayQueue...)_第3张图片

  1. 由数组实现的有界队列。
  2. 独占锁ReentrantLock,同时只能有一个线程进行入队/出队操作,由入队指针(putIndex)和出队指针(takeIndex)记录入队/出队元素位置。
  3. offer(无阻塞尾部入队enqueue(node),已满则丢弃)、put(while阻塞入队,若队列满则调用notFull.await()放入notFull条件队列等待,可被中断,中断后抛出InterruptedException)入队成功之后计算队尾指针++putIndex,count++,并调用notEmpty.signal()唤醒消费者。
  4. poll(无阻塞头部出队dequeue(node)),take(while阻塞头部出队,若队列空则等待,可被中断,中断后抛出InterruptedException)出队成功之后计算队尾指针++takeIndex,count–,并调用notFull.signal()唤醒生产者。
  5. peek(无阻塞窥视,获取头部元素不移除)。
  6. size(加锁获取count值,count没有被volatile修饰,因为操作count的时候都是在获取锁之后,所以没有加volatile,获取size的时候同理,使用锁来保证内存可见性)。

PriorityBlockingQueue

带优先级的无界阻塞队列,内部使用平衡二叉堆实现,默认使用对象的compareTo方法提供比较规则,可以自定义comparators。
Davids原理探究:Java并发包中并发队列(ConcurrentLinkedQueue、LinkedBlockingQueue、DelayQueue...)_第4张图片

  1. 由数组实现的无界优先级队列,因为无界所以只有notEmpty条件队列控制消费者。
  2. allocationSpinLock是个自旋锁,使用CAS操作保证同时只有一个线程进行扩容,状态0/1,0表示当前没有扩容,1表示当前正在扩容。扩容前会unlock,其实也可以不unlock,因为扩容需要时间,为了得到更好的性能,在扩容完成后,再获取锁,将当前queue里面的元素复制到新数组。
  3. siftUpComparable、siftDownComparable,二叉树堆维护优先级。
private static <T> void siftUpComparable(int k, T x, Object[] array) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = array[parent];
        if (key.compareTo((T) e) >= 0)
            break;
        array[k] = e;
        k = parent;
    }
    array[k] = key;
}

private static <T> void siftDownComparable(int k, T x, Object[] array, int n) {
  if (n > 0) {
        Comparable<? super T> key = (Comparable<? super T>)x;
        int half = n >>> 1;           // loop while a non-leaf
        while (k < half) {
            int child = (k << 1) + 1; // assume left child is least
            Object c = array[child];
            int right = child + 1;
            if (right < n &&
                ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                c = array[child = right];
            if (key.compareTo((T) c) <= 0)
                break;
            array[k] = c;
            k = child;
        }
        array[k] = key;
    }
}
  1. put内部调用offer由于是无界队列,所以不需要阻塞。
  2. poll:获取对头元素。
  3. take:阻塞出队,直到有元素位置,可被中断,中断后抛出InterruptedException。
  4. peek:获取锁之后,return (size == 0) ? null : (E) queue[0];
  5. size:获取所之后返回size,复杂度O(1)。
  6. 示例,优先级队列创建类实现Comparable接口,并重写compareTo,自定义元素比较规则,取出顺序和先后顺序无关,只与优先级有关。
/**
 * PriorityBlockingQueue 示例
 **/
public class PriorityBlockingQueueTest {

	/**
     * 实现Comparable接口,getDelay和compareTo方法
     **/
    @Data // lombok
    static class Task implements Comparable<Task> {

        private int priority = 0;
        private String taskName;

		/**
	     * 实现compareTo方法
	     **/
        @Override
        public int compareTo( Task o ) {
            return this.priority >= o.priority ? 1 : -1;
        }
    }

    public static void main( String[] args ) {
        PriorityBlockingQueue<Task> tasks = new PriorityBlockingQueue<>();
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            Task task = new Task();
            task.setPriority(random.nextInt(10));
            task.setTaskName("taskName" + i);
            tasks.offer(task);
        }

        while (!tasks.isEmpty()) {
            Task poll = tasks.poll();
            System.out.println(poll);
        }

    }

}

DelayQueue

无界阻塞延时队列,使用独占锁实现线程同步,队列中元素需要实现Delayed接口。
Davids原理探究:Java并发包中并发队列(ConcurrentLinkedQueue、LinkedBlockingQueue、DelayQueue...)_第5张图片

  1. 并发无界阻塞延时队列,头部为最快要过期的元素。
  2. private final PriorityQueue q = new PriorityQueue();用于存放元素。leader变量基于Leader-Follower模式的变体,用于尽量减少不必要的线程等待。
  3. offer:获取锁然后q.offer入队,插入元素要实现Delayed接口,入队完成后peek获取元素,若peek() == e,则将leader置空并唤醒Condition available = lock.newCondition()的一个follwer线程,告诉它队列里面有元素了。
  4. take:获取并移除队列里面延迟时间过期的元素,如果队列没有过期元素则等待。leader不为null这说明有其它线程在执行take。如果队首元素未到过期时间,并且leader线程是当前线程,则会调用awaitNanos(delay),休眠delay时间(这期间会释放锁,所以其他线程可以执行offer操作,也可以take阻塞自己),剩余时间过期后当前线程会重新竞争到锁,然后重置leader线程为null,重新进入for循环这时候会发现队首元素已经过期了,则会直接返回队首元素。
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    // 获取可中断锁
    lock.lockInterruptibly();
    try {
    	// 循环操作
        for (;;) {
        	// 获取队首元素,不出队
            E first = q.peek();
            if (first == null)
            	// 队首元素为空则等待,并释放锁
                available.await();
            else {
            	// 队首元素不为空,则获取过期时间
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0)
                	// 若已过期则直接取出并返回
                    return q.poll();
                // 若未到期,则释放引用
                first = null; // don't retain ref while waiting
                if (leader != null)
                	// 如果leader不为null,则说明有其他县城在执行take操作,此时休眠自己
                    available.await();
                else {
                	// 如果未过期切leader为null,则将leader设置为当前线程
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                    	// 休眠等待delay时间,这期间会释放锁,所以其他线程可以执行offer操作,也可以take阻塞自己,
                    	// 这期间会释放锁,所以其他线程可以执行offer操作,也可以take阻塞自己,剩余时间过期后
                    	// 当前线程会重新竞争到锁,然后重置leader线程为null
                    	// 重新进入for循环这时候会发现队首元素已经过期了,则会直接返回队首元素
                        available.awaitNanos(delay);
                    } finally {
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
    	// 操作完成后将leader设置为null,如果队列还有元素,则唤醒条件等待队列中的队首线程进行操作
        if (leader == null && q.peek() != null)
            available.signal();
        // 释放锁
        lock.unlock();
    }
}
  1. poll操作比较简单,获取非中断锁后,无阻塞peek队首元素,若为null或者没有过期直接返回。
public E poll() {
final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        E first = q.peek();
        if (first == null || first.getDelay(NANOSECONDS) > 0)
            return null;
        else
            return q.poll();
    } finally {
        lock.unlock();
    }
}
  1. size操作,获取队列元素个数,包含过期和未过期的,复杂度O(1)。

/**
 * DelayQueueTest 示例
 **/
public class DelayQueueTest {

	/**
     * 实现Delayed接口,getDelay和compareTo方法
     **/
    @Data // lombok
    static class DelayQueueEle implements Delayed {
		// 延时时间
        private final int delayTime;
        // 到期时间
        private final long expire;
        // 任务名称
        private String taskName;

        public DelayQueueEle( int delayTime, String taskName ) {
            this.delayTime = delayTime;
            this.taskName = taskName;
            this.expire = System.currentTimeMillis() + delayTime;
        }

		/**
         * 实现getDelay方法
         **/
        @Override
        public long getDelay( TimeUnit unit ) {
            return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }

		/**
         * 实现compareTo方法
         **/
        @Override
        public int compareTo( Delayed o ) {
            return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
        }
    }

    public static void main( String[] args ) {
    	// 创建delay队列
        DelayQueue<DelayQueueEle> delayeds = new DelayQueue<>();
        // 创建延时任务
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            delayeds.offer(new DelayQueueEle(random.nextInt(500), "taskName:" + i));
        }
		// 依次取出并打印
        DelayQueueEle ele;
        // 循环,防止虚假唤醒,则不能打印全部元素
        try {
            for (; ; ) {
                while ((ele = delayeds.take()) != null) {
                    System.out.println(ele);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
//        或者
//        for (; ; ) {
//            while ((ele = delayeds.poll()) != null) {
//                System.out.println(ele);
//            }
//        }
    }

}

你可能感兴趣的:(并发,Java)