深入Java阻塞队列

何为阻塞队列?

阻塞队列,重点在于阻塞二字,意思就是支持阻塞插入和阻塞移除的队列。
阻塞插入:当队列中的元素满了,插入操作线程将阻塞直至队列有空闲空间;
阻塞移除:当队列中的元素为空,就是指没有元素时,移除操作线程将阻塞直至队列不为空;
从队细容量的角度划分,队列可分为有界和无界两种,从这个角度来说,对于无界队列,插入的阻塞操作是不会发生了。

阻塞队列的经典使用场景?

1、生产-消费者模式任务:这个是最常见的吧,生产者就往阻塞队列中"插入"元素,消费者就"移除"元素;
2、延时任务,这个也常见吧,比如某些下单后30分钟未支付自动取消、用户支付后自动下发券码短信等等

jdk提供哪些阻塞队列?

1.ArrayBlockingQueue
基于数组实现的有界阻塞队列,先进先出(first-in-first-out,简称FIFO)的方式排序,默认情况下不保证线程公平性的访问队列,意思就是说,线程A先阻塞,并不意味着线程A就可以先访问到元素,都是随机的竞争访问。看看下ArrayBlockingQueue的默认构造器就一目了然了
public ArrayBlockingQueue(int capacity) { this(capacity, false); }   //第二个参数就是代表是否需要公平锁
当然,我们可以使用这个构造器来实现公平的阻塞队列,这个时候就严格的FIFO了
ArrayBlockingQueue abq = new ArrayBlockingQueue(500,true);
其公平性保证来自于此构造方法使用了可重入锁ReentrantLock
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();
}
但是显然使用了可重入锁保证公平性后会降低应用的QPS,所以到底是要公平性还是要高性能,自己权衡。其方法主要包括
1.public boolean add(E e) {……}  //向队列中插入指定元素,若对列容量满了则抛出IllegalStateException异常,success返回true,
e为空的话抛出空指针异常,也就是说,add方法插入失败的话就只能通过抛出异常的方式;
2.public boolean offer(E e) {……}   //向队列插入指定元素,若对列容量满了则返回false,success返回true,e为空的话抛出空指针异常,此方法优于add方法;
3.public void put(E e) throws InterruptedException {……}  //向队列中插入元素,若对列已满则阻塞进入等待;
4.public E poll() {……}  //从队列中移除(取出)元素,若对列中没有元素则直接返回null;
5.public E take() throws InterruptedException {……}  //从队列中移除(取出)元素,若对列中没有元素则一直阻塞;
其中方法 2~5在jdk提供的阻塞队列类中基本都有体现,接下的类中就不赘述了。

2.LinkedBlockingQueue
基于链表结构实现的有界阻塞队列,先进先出(first-in-first-out,简称FIFO)的方式排序,默认大小为 MAX_VALUE=2的31次方,不保证公平性访问元素
public LinkedBlockingQueue() { this(Integer.MAX_VALUE); }
同理也可以使用下面的构造器来实现访问的公平性
public LinkedBlockingQueue(Collection<? extends E> c) {
    this(Integer.MAX_VALUE);
    final ReentrantLock putLock = this.putLock;
    putLock.lock(); // Never contended, but necessary for visibility
    略……
}
3.PriorityBlockingQueue
支持自定义排序的无界阻塞队列,默认按自然升序排序元素
public PriorityBlockingQueue() { this(DEFAULT_INITIAL_CAPACITY, null); }
在初始化时也支持自定义比较器
public PriorityBlockingQueue(int initialCapacity,Comparator<? super E> comparator) {
    if (initialCapacity < 1)   throw new IllegalArgumentException();
    this.lock = new ReentrantLock();
    this.notEmpty = lock.newCondition();
     this.comparator = comparator;
    this.queue = new Object[initialCapacity];
}
4.DelayQueue
无界阻塞队列,支持延时获取获取元素,创建元素时可以指定延时时间,只有延期满才能从中获取元素。DelayQueue中的元素必须实现Delayed接口。这个比较常用,比如上面说到的,用户支付后30s自动发送券码短信功能(我只截取了部分代码,但是里面的延时任务逻辑还是很清晰的……)
public class SendMessageDelay implements Delayed {
    private String code;
    private String phone;
    private long timeout;

    public SendMessageDelay(String code, String phone, long timeout) {
        this.code = code;
        this.phone = phone;
        this.timeout = timeout + System.nanoTime();
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        if (o == this) return 0;
        SendMessageDelay t = (SendMessageDelay) o;
        long d = (getDelay(TimeUnit.NANOSECONDS) - t.getDelay(TimeUnit.NANOSECONDS));
        return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
    }

    //发送逻辑,省略了部分代码
    public void send() {
        try {
            SendSmsUtil.send(phone, "自定义短信文案"+code);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
@Service
public class SendMessageDelayServiceImpl implements SendMessageDelayService {

    private static Logger log = LoggerFactory.getLogger(SendMessageDelayServiceImpl.class);

    @Autowired
    private SendFailService sendFailService;

    @Override
    public boolean sendDelay(OrderInfo orderInfo) {
        boolean flag = true;
        DelayQueue<SendMessageDelay> queue = new DelayQueue<SendMessageDelay>();
        //下单30秒后执行发短息逻辑
        queue.put(new SendMessageDelay(orderInfo.getOrderId(), orderInfo.getPhone(), TimeUnit.NANOSECONDS.convert(30, TimeUnit.SECONDS)));
        try {
            //take表示获取并移除队列的超时元素,如果没有则wait当前线程,直到有元素满足超时条件返回结果,在我这里take方法就返回DendMessageDelay实例对象
            queue.take().send();
            log.info("orderid={},send message successful~", orderInfo.getOrderId());
            return flag;
        } catch (InterruptedException e) {
            log.error("orderid=" + orderInfo.getOrderId() + ",send ms exceptioin={}", e);
            //持久化 异常订单数据,有重试机制
            sendFailService.addFailRecord(orderInfo);
            flag = false;
        }
        return flag;
    }
}
5.LinkedBlockingDeque
由链表结构组成的双向阻塞队列,D就表示double,支持对队列的两端进行插入和移除元素,所以相对于单向阻塞队列,并发操作时也更快一些。可通过如下方法实例化:
//指定最大容量为固定值:2的31次方
public LinkedBlockingDeque() { this(Integer.MAX_VALUE); }
//指定容量初始化
public LinkedBlockingDeque(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
}
6.LinkedTransferQueue
基于链表结构的无界阻塞队列,比上面的几个队列多了transfer和tryTransfer方法
/*若存在消费者正在等待元素,transfer就将生产者中的元素立即传递给消费者,若没有则等待直到有消费者消费才返回*/
public void transfer(E e) throws InterruptedException {
    if (xfer(e, true, SYNC, 0) != null) {
        Thread.interrupted(); // failure possible only due to interrupt
        throw new InterruptedException();
    }
}
/*若存在消费者在等待元素,立即将生产者的元素传递给消费者,若没有也立即返回false*/
public boolean tryTransfer(E e, long timeout, TimeUnit unit)
    throws InterruptedException {
    if (xfer(e, true, TIMED, unit.toNanos(timeout)) == null)
        return true;
    if (!Thread.interrupted())
        return false;
    throw new InterruptedException();
}
7.SynchronousQueue
不存储元素的阻塞队列 ,每个阻塞插入操作都必须等待一个阻塞移除操作,否则不能继续插入元素(所谓的不存储元素就是这个意思,一放进去就得移除),从这点来看SynchronousQueue的qps高于普通的阻塞队列,默认情况下非公平性访问队列。

阻塞队列的实现原理
阻塞队列的实现从代码层面看就是使用了Condition(多条件模式),里面封装了根据条件阻塞线程的操作
public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);  //实例化可重入锁
    notEmpty = lock.newCondition();  //Condition配合Lock使用,notEmpty表示阻塞插入条件
    notFull =  lock.newCondition();  //notFull表示阻塞移除条件
}
接着看在阻塞插入和阻塞移除的await方法,需要阻塞时比较关键的一步就是LockSupport.park(this),它实现的阻塞
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    long savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this); 
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}
再接着看LockSupport.park(this)方法
/*禁止当前线程调度*/
public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    /*保存阻塞的线程*/
    setBlocker(t, blocker);
    /*阻塞线程*/
    unsafe.park(false, 0L); 
    setBlocker(t, null);
}
public native void park(boolean var1, long var2);
park是个native修饰的方法,用来阻塞当前线程,它依赖于具体的操作系统而实现,HotSpot中实现的park方法可以参考这里https://blog.csdn.net/hengyunabc/article/details/28126139
当下列情况发生时park方法会返回:
1.当前线程执行了unpark方法(多次和一次效果是一致的);
2.当前线程被中断;
3.park方法中的相对时间到期;
4.发生了未知异常;

猜你喜欢

转载自blog.csdn.net/fanrenxiang/article/details/80649456