学习系列之并发编程(三)

多线程队列

在 Java 多线程应用中,特别是在线程池中,队列的使用率非常高。Java 提供的线程安全队列又分为了阻塞队列和非阻塞队列。

1. 阻塞队列

在 Java 线程池中,用到了阻塞队列。当创建的线程数量超过核心线程数时,新建的任务将会被放到阻塞队列中。阻塞队列有以下四种API。操作都是一样的,以ArrayBlockingQueue为例:
在这里插入图片描述
add和remove测试:

public static void test1(){ 
        // 队列的大小 
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3); 
        System.out.println(blockingQueue.add("a")); 
        System.out.println(blockingQueue.add("b")); 
        System.out.println(blockingQueue.add("c")); 
        // IllegalStateException: Queue full 抛出异常! 
        System.out.println(blockingQueue.add("d")); 
        System.out.println("=-==========="); 
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove()); 
        System.out.println(blockingQueue.remove()); 
        // java.util.NoSuchElementException 抛出异常!
        System.out.println(blockingQueue.remove()); 
    }

offer和poll测试:

   //有返回值,不抛出异常的情况
    public static void test2(){ 
        // 队列的大小 
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3); 
        System.out.println(blockingQueue.offer("a")); 
        System.out.println(blockingQueue.offer("b")); 
        System.out.println(blockingQueue.offer("c")); 
         System.out.println(blockingQueue.offer("d")); 
         // false 不抛出异常! 
        System.out.println("============================"); 
        System.out.println(blockingQueue.poll()); 
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll()); 
        System.out.println(blockingQueue.poll()); // null 不抛出异常! 
         }

     //等待,阻塞(等待超时)
    public static void test4() throws InterruptedException {
        // 队列的大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        blockingQueue.offer("a");
        blockingQueue.offer("b");
        blockingQueue.offer("c");
        // blockingQueue.offer("d",2,TimeUnit.SECONDS); // 等待超过2秒就退出
        System.out.println("===============");
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        blockingQueue.poll(2, TimeUnit.SECONDS);
        // 等待超过2秒就退出
        }

put和take测试:

/**
     *
     * 等待,阻塞(一直阻塞)
     */
    public static void test3() throws InterruptedException {
        // 队列的大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        // 一直阻塞
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");
        // blockingQueue.put("d"); // 队列没有位置了,一直阻塞
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        // 没有这个元素,一直阻塞
    }

我们可以根据自己的业务需求来选择使用哪一种阻塞队列,阻塞队列通常包括以下几种:
ArrayBlockingQueue:一个基于数组结构实现的有界阻塞队列,按 FIFO(先进先出)原则对元素进行排序,使用 ReentrantLock、Condition 来实现线程安全。根据源码,需要注意创建ArrayBlockingQueue队列的时候,必须指定队列的大小。源码如下:capacity指定队列的大小,fair指定锁是公平锁还是非公平锁。

 /** Condition for waiting takes */
    private final Condition notEmpty;

    /** Condition for waiting puts */
    private final Condition notFull;
    //锁是没有分离的,即生产和消费用的是同一个锁。
   
   ......
    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();
    }
    public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection<? extends E> c) {
        this(capacity, fair);

        final ReentrantLock lock = this.lock;
        lock.lock(); // Lock only for visibility, not mutual exclusion
        try {
            int i = 0;
            try {
                for (E e : c) {
                    checkNotNull(e);
                    items[i++] = e;
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;
            putIndex = (i == capacity) ? 0 : i;
        } finally {
            lock.unlock();
        }
    }

LinkedBlockingQueue:一个基于链表结构实现的阻塞队列,同样按 FIFO (先进先出) 原则对元素进行排序,使用 ReentrantLock、Condition 来实现线程安全,吞吐量通常要高于 ArrayBlockingQueue;通过源码发现,由于LinkedBlockingQueue的读写锁分离,所以效率会高于ArrayBlockingQueue。


		/** Current number of elements */
		private final AtomicInteger count = new AtomicInteger();
		
	    /** Lock held by take, poll, etc */
	    private final ReentrantLock takeLock = new ReentrantLock();
	
	    /** Wait queue for waiting takes */
	    private final Condition notEmpty = takeLock.newCondition();
	 
	     /** Lock held by put, offer, etc */
	    private final ReentrantLock putLock = new ReentrantLock();
	
	    /** Wait queue for waiting puts */
	    private final Condition notFull = putLock.newCondition();
	   
	   /**
	     * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
	     *
	     * @param capacity the capacity of this queue
	     * @throws IllegalArgumentException if {@code capacity} is not greater than
	     *             zero
	     */
	    public LinkedBlockingQueue(int capacity) {
	        if (capacity <= 0)
	            throw new IllegalArgumentException();
	        this.capacity = capacity;
	        last = head = new Node<E>(null);//构造链表的头尾结点,链表的初始化
	    }

以下两种作为理解
PriorityBlockingQueue:一个具有优先级的无限阻塞队列,基于二叉堆结构实现的无界限(最大值 Integer.MAX_VALUE - 8)阻塞队列,队列没有实现排序,但每当有数据变更时,都会将最小或最大的数据放在堆最上面的节点上,该队列也是使用了 ReentrantLock、Condition 实现的线程安全;
DelayQueue:一个支持延时获取元素的无界阻塞队列,基于 PriorityBlockingQueue 扩展实现,与其不同的是实现了 Delay 延时接口;
SynchronousQueue:一个不存储多个元素的阻塞队列,每次进行放入数据时, 必须等待相应的消费者取走数据后,才可以再次放入数据,该队列使用了两种模式来管理元素,一种是使用先进先出的队列,一种是使用后进先出的栈,使用哪种模式可以通过构造函数来指定。
Java 线程池 Executors 还实现了以下四种类型的 ThreadPoolExecutor,分别对应以上队列,详情如下:
在这里插入图片描述

2. 非阻塞队列

我们常用的线程安全的非阻塞队列是 ConcurrentLinkedQueue,它是一种无界线程安全队列 (FIFO),基于链表结构实现,利用 CAS 乐观锁来保证线程安全。ConcurrentLinkedQueue内部持有2个节点:head头结点,负责出列, tail尾节点,负责入列。而元素节点Node,使用item存储入列元素,next指向下一个元素节点。

    private static class Node<E> {
        volatile E item;
        volatile Node<E> next;
        //....
    }

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;  
        //....
}

入列:

   public boolean offer(E e) {
        checkNotNull(e);   //为空判断,e为null是抛异常
        final Node<E> newNode = new Node<E>(e); //将e包装成newNode
        for (Node<E> t = tail, p = t;;) {  //循环cas,直至加入成功
            //t = p = tail 
            Node<E> q = p.next;
            if (q == null) {   //判断p是否为尾节点
                //如果是,p.next = newNode
                if (p.casNext(null, newNode)) {
                    //首次添加时,p 等于t,不进行尾节点更新,所以所尾节点存在滞后性  
                    //并发环境,可能存添加/删除,tail就更难保证正确指向最后节点。
                    if (p != t) 
                        //更新尾节点为最新元素
                        casTail(t, newNode);  
                    return true;
                }
            }
            else if (p == q)
                //当tail不执行最后节点时,如果执行出列操作,很有可能将tail也给移除了    
                //此时需要对tail节点进行复位,复位到head节点
                p = (t != (t = tail)) ? t : head;
            else
                //推动tail尾节点往队尾移动
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

出列:

    public E poll() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                //入列折腾的tail,那出列折腾的就是head
                E item = p.item;
                //出列判断依据是节点的item=null
                //item != null, 并且能将操作节点的item设置null, 表示出列成功
                if (item != null && p.casItem(item, null)) {
                    if (p != h) 
                        //一旦出列成功需要对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
                    //推动head节点移动
                    p = q;
            }
        }
    }

ConcurrentLinkedQueue 的非阻塞算法实现可概括为下面 5 点:
1.使用 CAS 原子指令来处理对数据的并发访问,这是非阻塞算法得以实现的基础。
2.head/tail 并非总是指向队列的头 / 尾节点,也就是说允许队列处于不一致状态。 这个特性把入队 / 出队时,原本需要一起原子化执行的两个步骤分离开来,从而缩小了入队 / 出队时需要原子化更新值的范围到唯一变量。这是非阻塞算法得以实现的关键。
3.由于队列有时会处于不一致状态。为此,ConcurrentLinkedQueue 使用三个不变式来维护非阻塞算法的正确性。
4.以批处理方式来更新 head/tail,从整体上减少入队 / 出队操作的开销。
5.为了有利于垃圾收集,队列使用特有的 head 更新机制;为了确保从已删除节点向后遍历,可到达所有的非删除节点,队列使用了特有的向后推进策略。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值