Java并发编程之阻塞队列
实现线程安全的队伍有2种方式:
- 阻塞式的, 也就是加锁
- 非阻塞式的, 使用CAS, ConcurrentLinkedQueue就是使用的这种方式
阻塞队列提供两个附加的操作, 阻塞添加和阻塞移除:
- 阻塞添加: 当队列满时, 队列会阻塞添加元素的线程, 直到队列不满.
- 阻塞移除: 当队列空时, 队列会阻塞移除元素的线程, 直到队列不空.
操作/处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
入队方法 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
出队方法 | remove() | poll() | take() | poll(time, unit) |
检查方法 | element() | peek() |
offer()方法返回值类型为boolean, 入队失败返回false;
poll()方法若出队失败, 则返回null;
peek()方法只获取元素, 并不执行出队, 并获取不到元素时返回null;
使用场景: 生产者/消费者, 生产者向队列中添加元素, 消费者从队列中获取元素.
本文介绍的是Java中的7个阻塞式的队列.
ArrayBlockingQueue
ArrayBlockingQueue底层实现为数据, 是一个有界的阻塞队列, 此队列以FIFO的原则对元素进行排序.
public class TestArrayBlockingQueue {
// 创建一个容量为5的ArrayBlockingQueue
static BlockingQueue<String> queue = new ArrayBlockingQueue<String>(5);
public static void main(String[] args) throws Exception {
// 启动10个生产者线程, 每个生产者向queue中put自己的线程名称
// 并且put之后打印自己的名称
for(int i=0; i<10; i++) {
new Thread(new Runnable(){
public void run() {
try {
queue.put(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() + " put");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "producer-"+i).start();
}
Thread.sleep(5000); // 睡5秒, 让10个生产者都开始执行
// 打印队列的实际长度, 为5,
// 因为没有被消费, 所以只有5个生产者线程put成功,
// 其他线程被阻塞
System.out.println("queue size: " + queue.size());
// 创建10个消费者线程, 每个线程去queue中做一次take操作
// 每take一个元素, 就会有一个生产者线程被唤醒, 向queue中put元素
for(int i=0; i<10; i++) {
new Thread(new Runnable(){
public void run() {
try {
queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "concumer-"+i).start();
}
Thread.sleep(5000);
System.out.println("queue size: " + queue.size());
}
}
ArrayBlockingQueue默认是非公平队列;
公平队列是指被阻塞的线程, 可以按照被阻塞的顺序访问队列, 即先被阻塞的线程先访问队列. 非公平队列是指被阻塞的队列, 在队列可以用时, 都有同等的资格来争夺资源. 创建公平队列可以借助ArrayBlockingQueue的另一个构造方法:
BlockingQueue<String> queue = new ArrayBlockingQueue<String>(5, true);
LinkedBlockingQueue
LinkedBlockingQueue的底层实现为链表, 与ArrayBlockingQueue类似, 是一个有界的阻塞队列; 不同的是LinkedBlockingQueue可以不指定队列长度, 此时长度为Integer.MAX_VALUE
; LinkedBlockingQueue也是以FIFO原则对元素进行排序.
PriorityBlockingQueue
PriorityBlockingQueue从命名上可以看出, 它是支持优先级排序的阻塞队列.
PriorityBlockingQueue的底层实现为数据, 是一个无界的阻塞队列. 它并不阻塞生产者线程, 只阻塞消费者线程, 所以当生产者的生产速度快于消费者的消费速度时, 随着时间推移, 会最终消耗所有可用内存.
PriorityBlockingQueue支持通过自定义compareTo()
方法进行排序, 也可以通过指定Comparator
进行排序.
public class TestPriorityBlockingQueue {
static BlockingQueue<RegisterInfo> queue = new PriorityBlockingQueue<RegisterInfo>();
private static class RegisterInfo implements Comparable<RegisterInfo> {
private String name;
private int priority;
public RegisterInfo(String name, int priority) {
this.name = name;
this.priority = priority;
}
@Override
public int compareTo(RegisterInfo registerInfo) {
if(this.priority > registerInfo.getPriority()) {
return 1;
} else if (this.priority < registerInfo.getPriority()){
return -1;
} else {
return 0;
}
}
public String getName() {
return name;
}
public int getPriority() {
return priority;
}
}
public static void main(String[] args) throws Exception {
RegisterInfo info1 = new RegisterInfo("张三", 5);
RegisterInfo info2 = new RegisterInfo("李四", 7);
RegisterInfo info3 = new RegisterInfo("王五", 3);
queue.add(info1);
queue.add(info2);
queue.add(info3);
System.out.println(queue.take().getName());
System.out.println(queue.take().getName());
System.out.println(queue.take().getName());
}
}
DelayQueue
DelayQueue是一个支持可以延时获取元素的无界队列, 内部使用PriorityQueue实现. 放入DelayQueue队列中的元素必须实现Delayed接口, 创建元素时指定延时多长时间才能从队列中获取到此元素.
DelayQueue可以用于缓存的设计, 使用DelayQueue保存缓存的有效时间, 使用一个线程循环队列, 能从队列中取出元素时, 表示缓存的有效时间到了.
public class TestDelayQueue {
static BlockingQueue<CacheTime> queue = new DelayQueue<CacheTime>();
private static class CacheTime implements Delayed {
private String id;
private long time;
public CacheTime(String id, long time) {
this.id = id;
this.time = time;
}
@Override
public int compareTo(Delayed delayed) {
long diff = 0L;
if(delayed instanceof CacheTime) {
CacheTime other = (CacheTime) delayed;
diff = this.time - other.time;
} else {
diff = this.time - delayed.getDelay(TimeUnit.MILLISECONDS);
}
if(diff > 0) {
return 1;
} else if(diff < 0) {
return -1;
} else {
return 0;
}
}
@Override
public long getDelay(TimeUnit unit) {
return time - System.currentTimeMillis();
}
public String getId() {
return this.id;
}
}
public static void main(String[] args) throws Exception {
long now = System.currentTimeMillis();
CacheTime ct1 = new CacheTime("001", now + 3000);
CacheTime ct2 = new CacheTime("002", now + 5000);
CacheTime ct3 = new CacheTime("003", now + 1000);
queue.add(ct1);
queue.add(ct2);
queue.add(ct3);
for(int i=0; i<3; i++) {
System.out.println(queue.take().getId());
}
}
}
CacheTime类用于模拟缓存有效时间, id为缓存id, time为缓存有效时间.
CacheTime实现了Delayed接口:
getDelay()
方法用于返回此元素剩余的延时时间
compareTo()
方法用于排序
在main()方法, 创建了3个CacheTime对象, 并设定其延时时间为3秒后, 5秒后, 1秒后; 将其加入到DelayQueue中, 循环DelayQueue, 元素将根据延时时间由少到多依次延时出队.
SynchronousQueue
SynchronousQueue是一个不存元素的阻塞队列, 它的容量为0. 每次执行put操作必须等待一个take操作.
SynchronousQueue支持公平锁. 其内部分别使用TransferQueue和TransferStack保存被阻塞线程的引用.
这里的TransferQueue是SynchronousQueue的内部类, 不是那继承了BlockingQueue的TransferQueue接口.
public class TestSynchronousQueue {
static BlockingQueue<String> queue = new SynchronousQueue<String>();
public static void main(String[] args) throws Exception {
new Thread(new Runnable(){
@Override
public void run() {
try {
queue.put("hello");
System.out.println("put complete...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "producer").start();
Thread.sleep(1000);
System.out.println("queue size: " + queue.size());
new Thread(new Runnable(){
@Override
public void run() {
try {
System.out.println("take: " + queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "consumer").start();
}
}
示例中, producer线程put之后将一直阻塞, 直到consumer线程执行take操作.
但是, 由于add方法非阻塞, 所以, 如果没有consumer线程等待消费数据, add方法则抛出异常.
SynchronousQueue的一个使用场景就是在线程池中. Executors.newCachedThreadPool()
内部就是使用了SynchronousQueue.
LinkedTransferQueue
LinkedTransferQueue是一个用链表实现的无界阻塞队列.
LinkedTransferQueue实现了TransferQueue接口, 扩展了transfer和tryTransfer两个方法.
transfer()
: 生产者线程向队列中添加元素时, 如果正有消费者线程等待消费, 则直接将元素传输给生产者线程; 如果没有消费者等待消费, 则将元素存放在队列中, 并一直等到消费者线程消费之后才返回.tryTransfer()
: 试着将元素传输给消费者线程, 如果有消费者线程等待接收元素返回true, 否则返回false. 也就是说, tryTransfer()方法无论有没有消费者线程等待接收元素都会立即返回. 如果是tryTransfer(E e, long timeout, TimeUnit unit)
方法, 如果没有消费者线程则等待指定时间后再返回.
public class TestLinkedTransferQueue {
static LinkedTransferQueue<String> queue = new LinkedTransferQueue<String>();
public static void main(String[] args) {
new Thread(new Runnable(){
@Override
public void run() {
try {
queue.transfer("hello world");
System.out.println(Thread.currentThread().getName() + " end.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "producer").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("queue size: " + queue.size());
new Thread(new Runnable(){
@Override
public void run() {
try {
System.out.println(queue.take());
System.out.println(Thread.currentThread().getName() + " end.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "consumer").start();
}
}
一般情况下, 使用LinkedTransferQueue, 调用transfer方法传输元素时, 都会有一个消费者等在那儿, 准备接收这个元素.