java Blocking Queue

一、Java中的阻塞队列

  在多线程之间通信中,多个线程共享一个进程所分配的资源,共享内存是一种常见的通信方式,而阻塞队列则是其实现方式的一种,例如经典的生产者-消费者模式。

  A Queue that addtionally supports operations that wait for the queue to become non-empty when retrieving an element, and  wait for space to become avialable in the queue when storing an element.

  阻塞队列中提供了2个操作:

    队列为空时,获取元素的线程会阻塞队列一直至队列非空。

    队列为满时,存储元素的线程会阻塞队列非满。

  

  

  Java中的阻塞队列有7种:

  • ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
  • PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
  • DelayQueue:一个使用优先级队列实现的无界阻塞队列。
  • SynchronousQueue:一个不存储元素的阻塞队列。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

  下面进行说明

 

二、ArrayBlockingQueue

使用数组实现队列

 2.1 构造器

public ArrayBlockingQueue(int capacity,
                          boolean fair,
                          @NotNull Collection<? extends E> c)
//Creates an ArrayBlockingQueue with the given (fixed) capacity, the specified access policy and initially containing the elements of the given collection, added in traversal order of the collection's iterator.

参数说明:

  capacity: 队列的容量

  fair: 默认是false,如果是true的话则移除元素的顺序符合FIFO顺序,false的话没有顺序

  c: 预填充元素

实现主要如下:

 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();
    }

 

 

2.2 put方法实现

/** Main lock guarding all access */
    final ReentrantLock lock;

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

    /** Condition for waiting puts */
    private final Condition notFull;

由于数组这种数据结构的特殊性,若想要线程安全的添加或者删除,都必须将整个数组锁住,因此这里实现使用一把锁, 以及2个条件队列。

public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

这段代码非常容易理解,注意点:

(1) 可重入锁

(2) 使用2个条件队列:Java concurrent包对多条件队列的支持,古老的wait和notify方法也可以实现,只是由于条件队列中的条件不同,必须使用notifyall(),损失了性能。

(3) 可中断锁

 2.3 take() 方法说明

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

 

总结: ArrayBlockingQueue应该是最简单的阻塞队列实现了,由于数组结构的特殊性,使用了一把锁和2个条件队列,锁的方式是可中断锁。

三、LinkedBlockingQueue

使用链表实现队列,构造器使用方式和ArrayBlockingQueue一样

 

 public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

 

注意到初始化的时候创建了一个空节点...

 

3.1 使用2把锁实现

这里使用了2把锁,2个条件队列实现,而且在属性定义中就直接初始化了。一个存锁,一个取锁,一个非空条件队列,一个非满条件队列

 

  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();

 

3.2 put的实现

 public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

说明:

  • 除了锁之外,使用了原子变量来记录链表大小,因为使用了2把锁来锁节点,全局链表的大小使用线程安全的原子变量确实比较合适
  • 此时锁的是putLock,存锁

下面看singalNotEmpty()方法:

 

 private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

 

发现唤醒时使用了取锁,使用了连续的锁。我们大致梳理一下顺序,在不考虑异常的情况下:

(1) 当前线程竞争存锁putLock并将其锁住

(2) 条件队列notFull(非满条件)等待

(3) 链表中插入元素

(4) 原子变量自增

(5) if(c+1<capacity),则条件队列notFull唤醒操作,通知其他的put线程,链表未达上限,依旧可以插入元素

(6) 释放putLock锁

(7) 如果c==0,原子变量修改-1->0,表明插入成功,通知其他take线程可以去取出元素

  i. 当前线程竞争takeLock,并且锁住

  ii. notEmpty.signal,通知其他在wait take lock的线程可以取出元素

  iii. 释放takeLock

 

3.3 take的实现

类似,略

3.4 总结

(1) 使用了2把不同的锁

(2) 使用原子变量控制容量

(3) 使用链表数据结构

为什么可以用2把锁提升性能,减少锁竞争?
注意下面2个方法:入队方法操作的是last,出对方法操作的first,正是由于链表结构的特殊性,可以使用2把锁来提高锁粒度,从而减少锁竞争。

 private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }

    /**
     * Removes a node from head of queue.
     *
     * @return the node
     */
    private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }

 

四、Synchronousqueue

个人感觉http://ifeve.com/java-synchronousqueue/讲的比较详细.这里引用一段话描述:

不像ArrayBlockingQueue或LinkedListBlockingQueue,SynchronousQueue内部并没有数据缓存空间,你不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试着取走的时候才可能存在,不取走而只想偷窥一下是不行的,当然遍历这个队列的操作也是不允许的。队列头元素是第一个排队要插入数据的线程,而不是要交换的数据。数据是在配对的生产者和消费者线程之间直接传递的,并不会将数据缓冲数据到队列中。可以这样来理解:生产者和消费者互相等待对方,握手,然后一起离开。

package concurrent.demo.synchronous_queue;

import concurrent.demo.Utils;

import java.util.concurrent.CountDownLatch;

/**
 * <B>系统名称:</B><BR>
 * <B>模块名称:</B><BR>
 * <B>中文类名:</B><BR>
 * <B>概要说明:</B><BR>
 *
 * @author carl.yu
 * @since 2016/6/14
 */
public class NativeSynchronousQueue<E> {

    E item = null;
    boolean putting = false;

    //保证放进去之后,其他的取走了,其他的put线程才允许取
    public synchronized void put(E e) throws InterruptedException {
        //1. 只有一个item为null的时候才允许放
        while (putting) {
            wait();
        }
        putting = true;
        item = e;
        //2. 通知其他的线程来取,可能会通知错了人,造成假唤醒,所以需要用条件队列putting为true来判定,继续睡吧
        notifyAll(); /*必须使用notify不能使用notifyAll*/
        /*这里的notifyAll是为了让其他的take线程醒来,而不是put线程醒来哦*/

        //3. 只有取完的线程才可以来拿
        while (item != null) {
            wait();
        }
        putting = false;
        notifyAll();
        /*这里才是为了put线程醒来*/
    }

    /*take比较简单,就是拿*/
    public synchronized E take() throws InterruptedException {
        E res = null;
        while (item == null) {
            wait();
        }
        res = item;
        item = null;
        notifyAll();
        return res;
    }


    //测试
    public static void main(String[] args) throws Exception {
        final NativeSynchronousQueue queue = new NativeSynchronousQueue();
        final CountDownLatch latch = new CountDownLatch(3);
        Thread put01 = new Thread() {
            @Override
            public void run() {
                try {
                    queue.put("1");
                    System.out.println("put01成功");
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread put02 = new Thread() {
            @Override
            public void run() {
                try {
                    queue.put("2");
                    System.out.println("put02成功");
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread take01 = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println("take成功:" + queue.take());
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread take02 = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println("take成功:" + queue.take());
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        put01.start();
        Utils.sleep(10);
        put02.start();
        Utils.sleep(10);
        //当没有线程take的时候,put永远不会成功
        take01.start();
        Utils.sleep(10);
        take02.start();
        latch.await();
        System.out.println("测试结束");
    }

 五、条件队列和线程池的使用

Java中,线程池的使用非常常见,jdk1.8中还新增了许多连接池,例如newWorkStealingPool用作并行计算...

这里主要强调条件队列的用法

5.1 使用有界队列实现

直接演示

定义任务类:

 

public class MyTask implements Runnable {

    private int taskId;
    private String taskName;
    
    public MyTask(int taskId, String taskName){
        this.taskId = taskId;
        this.taskName = taskName;
    }
    
    public int getTaskId() {
        return taskId;
    }

    public void setTaskId(int taskId) {
        this.taskId = taskId;
    }

    public String getTaskName() {
        return taskName;
    }

    public void setTaskName(String taskName) {
        this.taskName = taskName;
    }

  
    public void run() {
        try {
            System.out.println("run taskId =" + this.taskId);
            Thread.sleep(5*1000);
            //System.out.println("end taskId =" + this.taskId);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }        
    }
    
    public String toString(){
        return Integer.toString(this.taskId);
    }

}

 

自定义拒绝策略:

package com.bjsxt.height.concurrent018;

import java.net.HttpURLConnection;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

public class MyRejected implements RejectedExecutionHandler{

    
    public MyRejected(){
    }
    
   
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println("自定义处理..");
        System.out.println("当前被拒绝任务为:" + r.toString());
        

    }

}

 

 

 

public static void main(String[] args) {
        /**
         * 在使用有界队列时,若有新的任务需要执行,如果线程池实际线程数小于corePoolSize,则优先创建线程,
         * 若大于corePoolSize,则会将任务加入队列,
         * 若队列已满,则在总线程数不大于maximumPoolSize的前提下,创建新的线程,
         * 若线程数大于maximumPoolSize,则执行拒绝策略。或其他自定义方式。
         * 
         */    
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                1,                 //coreSize
                2,                 //MaxSize
                60,             //60
                TimeUnit.SECONDS, 
                new ArrayBlockingQueue<Runnable>(3)            //指定一种队列 (有界队列)
                //new LinkedBlockingQueue<Runnable>()
                , new MyRejected()
                //, new DiscardOldestPolicy()
                );
        
        MyTask mt1 = new MyTask(1, "任务1");
        MyTask mt2 = new MyTask(2, "任务2");
        MyTask mt3 = new MyTask(3, "任务3");
        MyTask mt4 = new MyTask(4, "任务4");
        MyTask mt5 = new MyTask(5, "任务5");
        MyTask mt6 = new MyTask(6, "任务6");
        
        pool.execute(mt1);
        pool.execute(mt2);
        pool.execute(mt3);
        pool.execute(mt4);
        pool.execute(mt5);
        pool.execute(mt6);
        
        pool.shutdown();
        
    }

 

 

 

逐渐增加任务数,由1增加到6发现效果如下:

(1) 当任务数<1时,加入一个任务直接创建线程去处理

(2) 继续加入任务>corePoolSize=1时,会加入队列ArrayBlockingQueue

(3) 一直加入到任务数为4,队列中3个元素,队列满了

(4) 任务数继续增加,到5,会继续创建线程一直到maxPoolSize(2),因此此时执行的任务都1和5

(5) 任务数增加为6,执行拒绝策略

 

5.2 无界队列实现

将上述代码修改为用LinkedBlockingQueue()无界队列实现,发现此时maxSize参数没有作用,拒绝策略也没有作用,只有coreSize才有作用

(1) 当任务数<coreSize,会创建线程

(2) 当任务数>coreSize,会将任务放置到无界队列中,直到系统崩溃

(3) maxSize没有任何作用

(4) 拒绝策略没有任何作用

因此,JDK在实现FixedThreadPool时,maxSize和coreSize相等

 

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

 

 

 

5.3 Synchronousqueue实现

JDK中CachedThreadPool用这种队列实现,性能非常好

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

 

  

posted @ 2016-11-05 23:02  carl_ysz  阅读(1299)  评论(0编辑  收藏  举报