【P说】深入理解Java阻塞队列

简介

阻塞队列是一种队列,一种可以在多线程环境下使用,并且支持阻塞等待的队列。

  1. 当队列满的时候,插入元素的线程被阻塞,直达队列不满。
  2. 队列为空的时候,获取元素的线程被阻塞,直到队列不空。

方法

java的阻塞队列,要实现BlockingQueue接口:

public interface BlockingQueue<E> extends Queue<E> {

    boolean add(E e);

    boolean offer(E e);

    void put(E e) throws InterruptedException;

    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

    E take() throws InterruptedException;

    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;

    int remainingCapacity();

    boolean remove(Object o);

    public boolean contains(Object o);

    int drainTo(Collection<? super E> c);

    int drainTo(Collection<? super E> c, int maxElements);
}

常用方法的作用理解:

方法抛出异常返回值一直阻塞超时退出
插入方法addofferputOffer(time)
移除方法removepolltakePoll(time)
检查方法elementpeekN/AN/A
  • 抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalStateException(“Queuefull”)异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
  • 返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null。
  • 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。
  • 超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。

Java中的几种阻塞队列

  1. ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
    按照先进先出原则,要求设定初始大小
  2. LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
    按照先进先出原则,可以不设定初始大小,Integer.Max_Value
    eq:ArrayBlockingQueue和LinkedBlockingQueue不同:
  • 锁上面:ArrayBlockingQueue只有一个锁,LinkedBlockingQueue用了两个锁(插入和读取用了两个锁),
  • 实现上:ArrayBlockingQueue直接插入元素,LinkedBlockingQueue需要转换(转为链表节点)。
  1. PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
    默认情况下,按照自然顺序,要么实现compareTo()方法,指定构造参数Comparator
  2. DelayQueue:一个使用优先级队列实现的无界阻塞队列。
    支持延时获取的元素的阻塞队列,元素必须要实现Delayed接口。适用场景:实现自己的缓存系统,订单到期,限时支付等等。
  3. SynchronousQueue:一个不存储元素的阻塞队列。
    每一个put操作都要等待一个take操作
  4. LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
    transfer(),必须要消费者消费了以后方法才会返回,tryTransfer()无论消费者是否接收,方法都立即返回。
  5. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
    可以从队列的头和尾都可以插入和移除元素,实现工作密取,方法名带了First对头部操作,带了last从尾部操作,另外:add=addLast; remove=removeFirst; take=takeFirst

使用

ArrayBlockingQueue

本例生产者两秒中发送一条消息,而消费者一秒钟就会测试获取一条消息,因此消费者阻塞等待。

public class BlockingQueue {
    //生产者线程
    static class SCZThread extends Thread {

        private ArrayBlockingQueue<String> arrayBlockingQueue;

        private int count = 0;

        SCZThread(ArrayBlockingQueue<String> arrayBlockingQueue) {
            this.arrayBlockingQueue = arrayBlockingQueue;
        }

        @Override
        public void run() {
            //生产者每隔2秒中就发送一条消息
            while (true) {
                try {
                    Thread.sleep(2000);
                    //ArrayBlockingQueue的put在插入时,如果阻塞队列已经满了,就会阻塞,因此可能会抛出InterruptedException的中断异常
                    arrayBlockingQueue.put("来着生产者的第" + (++count) + "条消息");
                    System.out.println("发送了生产者的第" + count + "条消息");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //消费者线程
    static class XFZThread extends Thread {

        private ArrayBlockingQueue<String> arrayBlockingQueue;

        private int count = 0;

        XFZThread(ArrayBlockingQueue<String> arrayBlockingQueue) {
            this.arrayBlockingQueue = arrayBlockingQueue;
        }

        @Override
        public void run() {
            //生产者每隔1秒中就等待就收消息
            while (true) {
                try {
                    Thread.sleep(1000);
                    System.out.println("等待接收消息");
                    //ArrayBlockingQueue的take在获取时,如果阻塞队列是空的,就会阻塞,因此可能会抛出InterruptedException的中断异常
                    String message = arrayBlockingQueue.take();
                    System.out.println("收到生产者发送的消息" + message);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        //必须指定容量
        ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(10);
        new SCZThread(arrayBlockingQueue).start();
        new XFZThread(arrayBlockingQueue).start();
    }
}

结果:
在这里插入图片描述

DelayQueue

DelayQueue可以放入消息时设置过期时间,过期时间到了,就会弹出,被消费者拿到。这个队列可以用来处理需要过期时间的场景,比如redis中的key过期,订单过期。
由于放进DelayQueue的数据需要实现Delay接口,所以要先对数据进行封装。

public class ItemVo<T> implements Delayed{
	
	private long activeTime;//到期时间,单位毫秒
	private T date;
	
	//activeTime是个过期时刻
	public ItemVo(long activeTime, T date) {
		super();
		//传入的时间是毫秒,先转换为纳秒,activeTime保存的是过期时刻,因此还要加上当前系统时间
		this.activeTime = TimeUnit.NANOSECONDS.convert(activeTime, 
				TimeUnit.MILLISECONDS)+System.nanoTime();//将传入的时长转换为超时的时刻
		this.date = date;
	}
	
	public long getActiveTime() {
		return activeTime;
	}

	public T getDate() {
		return date;
	}

	//按照剩余时间排序
	@Override
	public int compareTo(Delayed o) {
		long d = getDelay(TimeUnit.NANOSECONDS)-o.getDelay(TimeUnit.NANOSECONDS);
		return (d==0)?0:((d>0)?1:-1);
	}

	//返回元素的剩余时间
	@Override
	public long getDelay(TimeUnit unit) {
		long d = unit.convert(this.activeTime-System.nanoTime(),
				TimeUnit.NANOSECONDS);
		return d;
	}
}

消费者:

public class FetchOrder implements Runnable {
	
	private DelayQueue<ItemVo<Order>> queue;
	
	public FetchOrder(DelayQueue<ItemVo<Order>> queue) {
		super();
		this.queue = queue;
	}	

	@Override
	public void run() {
		while(true) {
			try {
				ItemVo<Order> item = queue.take();
				Order order = (Order)item.getDate();
				System.out.println("get from queue:"+order.getOrderNo());
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}	
}

生产者:

public class PutOrder implements Runnable {
	
	private DelayQueue<ItemVo<Order>> queue;
	
	public PutOrder(DelayQueue<ItemVo<Order>> queue) {
		super();
		this.queue = queue;
	}

	@Override
	public void run() {
		
		//5秒到期
		Order ordeTb = new Order("Tb12345",366);
		ItemVo<Order> itemTb = new ItemVo<Order>(5000,ordeTb);
		queue.offer(itemTb);
		System.out.println("订单5秒后到期:"+ordeTb.getOrderNo());
		
		//8秒到期
		Order ordeJd = new Order("Jd54321",366);
		ItemVo<Order> itemJd = new ItemVo<Order>(8000,ordeJd);
		queue.offer(itemJd);
		System.out.println("订单8秒后到期:"+ordeJd.getOrderNo());		
		
	}	
}

源码分析

ArrayBlockingQueue

成员变量

//保存消息的数组,初始化时候需要指定该数组的长度
final Object[] items;

//下次插入数据时的下标位置
int takeIndex;

//下次获取数据时的下标位置
int putIndex;

//当前保存的消息数量
int count;

//锁
final ReentrantLock lock;

//数组不为空时的通知Condition 
private final Condition notEmpty;

//数组还没满时的通知Condition 
private final Condition notFull;

返回值插入offer

往队列加入一个消息,加入成功返回true,失败返回false。

public boolean offer(E e) {
    checkNotNull(e); //参数校验
    final ReentrantLock lock = this.lock;
    lock.lock(); //拿锁
    try {
        if (count == items.length) //队列已经满了,返回false
            return false;
        else {
            enqueue(e); //队列没有满,调用enqueue入队
            return true;
        }
    } finally {
        lock.unlock(); //释放锁
    }
}

private void enqueue(E x) {
    final Object[] items = this.items;
    items[putIndex] = x; //putIndex插入位置下标
    if (++putIndex == items.length) //判断是否已经超出最大下标,一个循环
        putIndex = 0;
    count++; //保存的消息加一
    notEmpty.signal(); //notEmpty Condition通知正在等待获取消息的线程
}

抛出异常插入add

插入失败时返回IllegalStateException(“Queue full”),成功返回true

public boolean add(E e) {
    return super.add(e); //调用了父类的add
}

//父类的add
public boolean add(E e) {
    if (offer(e)) //调用了offer方法
        return true;
    else
        throw new IllegalStateException("Queue full"); //抛出异常
}

一直阻塞插入put

插入失败时阻塞,直到成功插入,不会超时,如果一直失败那么就一直阻塞。

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(); //释放锁
    }
}

超时退出插入Offer(time)

队列满时,插入阻塞,超过超时时间则返回false。

public boolean offer(E e, long timeout, TimeUnit unit)
    throws InterruptedException {

    checkNotNull(e);
    long nanos = unit.toNanos(timeout);  //转为纳秒
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly(); //获得锁
    try {
        while (count == items.length) { //队列已经满了
            if (nanos <= 0) //判断是否过期
                return false;
            nanos = notFull.awaitNanos(nanos); //阻塞,该地方被唤醒要两种情况,一种时队列没满时,被通知,一种时超时。如果是超时,这是会这一次执行while条件进入并被判断为过期,返回false
        }
        enqueue(e);
        return true;
    } finally {
        lock.unlock(); //释放锁
    }
}

阻塞获取poll

尝试从队列中获取消息,然后获取不到就返回null

public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock(); //拿锁
    try {
        return (count == 0) ? null : dequeue(); //队列为空时,返回null,否则调用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;  //移除
    if (++takeIndex == items.length) 
        takeIndex = 0;
    count--;  //队列消息减一
    if (itrs != null)
        itrs.elementDequeued(); 
    notFull.signal(); //发出队列没满的通知
    return x;
}

阻塞获取take

队列中没有消息时,获取会阻塞

public E take() throws InterruptedException {
     final ReentrantLock lock = this.lock;
     lock.lockInterruptibly();
     try {
         while (count == 0)
             notEmpty.await(); //阻塞,等待非空通知
         return dequeue();
     } finally {
         lock.unlock();
     }
 }

超时获得poll(long timeout, TimeUnit unit)

获取超时返回null

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try { 
        while (count == 0) { //条件判断
            if (nanos <= 0)  //过期判断
                return null;
            nanos = notEmpty.awaitNanos(nanos);
        }
        return dequeue();
    } finally {
        lock.unlock();
    }
}

LinkedBlockingQueue

LinkedBlockingQueue的实现与ArrayBlockingQueue基本一致,LinkedBlockingQueue的内部使用链表存储消息,所以放进队列中的消息会被先封装成为Node。

static class Node<E> {
    E item;
    
    Node<E> next;

    Node(E x) { item = x; }
}

ArrayBlockingQueue只用了一个锁,无论时获取还是插入,这样对效率是有影响的,获取时不能插入,插入时不能获取。LinkedBlockingQueue使用两把锁,在效率上是有提升的。

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

DelayQueue

DelayQueue最特殊的地方就是可以通过take获得到期的消息

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; //线程等待的时候不要持有对头对象
                  if (leader != null) //leader表示已经有其他线程先调用了take方法,所以当前线程要先进行等待
                      available.await(); //available表示当没有其他线程在等待时的通知,因为一次只通知一个线程,当通知到这个线程时,表示该线程可以进行操作,不会有多个线程竞争的情况
                  else {
                      Thread thisThread = Thread.currentThread(); //获取当前线程
                      leader = thisThread; //设置为正在操作的线程
                      try {
                          available.awaitNanos(delay); //等待消息的过期时间,保证下次循环的时候,对头那个消息一定时超时过期的
                      } finally {
                          if (leader == thisThread)
                              leader = null;
                      }
                  }
              }
          }
      } finally {
          if (leader == null && q.peek() != null)
              available.signal(); //唤醒下一个在等待的线程
          lock.unlock(); //解锁
      }
  }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值