队列Queue:
队列,是一种数据结构。除了优先级队列和LIFO队列外,队列都是以FIFO(先进先出)的方式对各个元素进行排序的。无论使用哪种排序方式,队列的头都是调用remove()或poll()移除元素的。在FIFO队列中,所有新元素都插入队列的末尾。
Queue中的方法:
Queue中的方法不难理解,6个,每2对是一个也就是总共3对。看一下JDK API就知道了:
注意一点就好,Queue通常不允许插入Null,尽管某些实现(比如LinkedList)是允许的,但是也不建议。
阻塞队列BlockingQueue
阻塞与队列:
写入:如果队列满了,就必须阻塞等待
取:如果队列是空的,就必须阻塞等待生产
BlockingQueue概述
只讲BlockingQueue,因为BlockingQueue是Queue中的一个重点,并且通过BlockingQueue我们再次加深对于生产者/消费者模型的理解。其他的Queue都不难,通过查看JDK API和简单阅读源码完全可以理解他们的作用。
BlockingQueue,顾名思义,阻塞队列。BlockingQueue是在java.util.concurrent下的,因此不难理解,BlockingQueue是为了解决多线程中数据高效安全传输而提出的。
多线程中,很多场景都可以使用队列实现,比如经典的生产者/消费者模型,通过队列可以便利地实现两者之间数据的共享,定义一个生产者线程,定义一个消费者线程,通过队列共享数据就可以了。
当然现实不可能都是理想的,比如消费者消费速度比生产者生产的速度要快,那么消费者消费到 一定程度上的时候,必须要暂停等待一下了(使消费者线程处于WAITING状态)。BlockingQueue的提出,就是为了解决这个问题的,他不用程序员去控制这些细节,同时还要兼顾效率和线程安全。
阻塞队列所谓的"阻塞",指的是某些情况下线程会挂起(即阻塞),一旦条件满足,被挂起的线程又会自动唤醒。使用BlockingQueue,不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,这些内容BlockingQueue都已经做好了
BlockingQueue的四组API
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待(阻塞队列特有) | 超时等待 |
---|---|---|---|---|
添加 | add | offer | put | offer(,,) |
移除 | remove | poll | take | poll(,) |
判断队列首 | element | peek |
1. 抛出异常:
添加add():
public class Test1 {
public static void main(String[] args) {
test1();
}
public static void test1() {
ArrayBlockingQueue queue = new ArrayBlockingQueue(3);
System.out.println(queue.add("a"));
System.out.println(queue.add("b"));
System.out.println(queue.add("c"));
System.out.println(queue.add("d"));
}
}
结果:队列已满异常
true
true
true
Exception in thread "main" java.lang.IllegalStateException: Queue full
at java.base/java.util.AbstractQueue.add(AbstractQueue.java:98)
at java.base/java.util.concurrent.ArrayBlockingQueue.add(ArrayBlockingQueue.java:326)
at com.yhx.juc.BlockQueue.Test1.put(Test1.java:21)
at com.yhx.juc.BlockQueue.Test1.main(Test1.java:12)
取出remove():
代码:
public static void test1() {
ArrayBlockingQueue queue = new ArrayBlockingQueue(3);
System.out.println(queue.add("a"));
System.out.println(queue.add("b"));
System.out.println(queue.add("c"));
// System.out.println(queue.add("d"));
System.out.println("=======");
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
}
结果:队列为空异常
true
true
true
=======
a
b
c
Exception in thread "main" java.util.NoSuchElementException
at java.base/java.util.AbstractQueue.remove(AbstractQueue.java:117)
at com.yhx.juc.BlockQueue.Test1.test1(Test1.java:26)
at com.yhx.juc.BlockQueue.Test1.main(Test1.java:12)
判断队列首element():
public static void test1() {
ArrayBlockingQueue queue = new ArrayBlockingQueue(3);
System.out.println(queue.add("a"));
System.out.println(queue.add("b"));
System.out.println(queue.add("c"));
// System.out.println(queue.add("d"));
System.out.println("=======");
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.element());
System.out.println(queue.remove());
System.out.println(queue.element());
// System.out.println(queue.remove());
}
结果:
true
true
true
=======
a
b
c
c
Exception in thread "main" java.util.NoSuchElementException
at java.base/java.util.AbstractQueue.element(AbstractQueue.java:136)
at com.yhx.juc.BlockQueue.Test1.test1(Test1.java:27)
at com.yhx.juc.BlockQueue.Test1.main(Test1.java:12)
2.不抛出异常:
添加offer():
public static void test2() {
ArrayBlockingQueue queue = new ArrayBlockingQueue(3);
System.out.println(queue.offer("a"));
System.out.println(queue.offer("b"));
System.out.println(queue.offer("c"));
System.out.println(queue.offer("d"));
}
结果:可以看到无法添加的时候,返回false
true
true
true
false
取出poll:
代码:
public static void test2() {
ArrayBlockingQueue queue = new ArrayBlockingQueue(3);
System.out.println(queue.offer("a"));
System.out.println(queue.offer("b"));
System.out.println(queue.offer("c"));
//System.out.println(queue.offer("d"));
System.out.println("=======");
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
}
结果:取不到返回null
true
true
true
=======
a
b
c
null
判断队列首peek:
public static void test2() {
ArrayBlockingQueue queue = new ArrayBlockingQueue(3);
System.out.println(queue.offer("a"));
System.out.println(queue.offer("b"));
System.out.println(queue.offer("c"));
System.out.println("=======");
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.peek());
System.out.println(queue.poll());
System.out.println(queue.peek());
}
结果:
true
true
true
=======
a
b
c
c
null
3. 阻塞(阻塞队列特有方法)
添加put:
public static void test3() throws InterruptedException {
ArrayBlockingQueue queue = new ArrayBlockingQueue(3);
queue.put("a");
queue.put("b");
queue.put("c");
queue.put("d"); // 队列没有位置了,一直阻塞
}
结果:可以看到程序一直在执行,没有停止,因为d在阻塞等待插入
取出take():
public static void test3() throws InterruptedException {
ArrayBlockingQueue queue = new ArrayBlockingQueue(3);
queue.put("a");
queue.put("b");
queue.put("c");
// queue.put("d"); // 队列没有位置了,一直阻塞
queue.take();
queue.take();
queue.take();
queue.take(); // 无法取出,一直阻塞
}
结果:还是向上次那样,程序一直在执行,没有停止,处于阻塞状态
4.超时退出
添加offer(E e, long timeout, TimeUnit unit):
设置阻塞2秒还拿不到就退出:
public static void test4() throws InterruptedException {
ArrayBlockingQueue queue = new ArrayBlockingQueue(2);
System.out.println("插入啦" + System.currentTimeMillis());
queue.offer("a");
queue.offer("b");
queue.offer("c", 2, TimeUnit.SECONDS); //
System.out.println("退出啦" + System.currentTimeMillis());
}
结果:
插入啦1595096226652
退出啦1595096228667
取出E poll(long timeout, TimeUnit unit):
public static void test4() throws InterruptedException {
ArrayBlockingQueue queue = new ArrayBlockingQueue(2);
System.out.println("插入啦" + System.currentTimeMillis());
queue.offer("a");
queue.offer("b");
queue.offer("c", 2, TimeUnit.SECONDS); //
System.out.println("退出啦" + System.currentTimeMillis());
System.out.println("=======");
System.out.println("我取啦" + System.currentTimeMillis());
queue.poll();
queue.poll();
queue.poll(2, TimeUnit.SECONDS);
System.out.println("又退啦" + System.currentTimeMillis());
}
结果:
插入啦1595096429562
退出啦1595096431576
=======
我取啦1595096431576
又退啦1595096433579
ArrayBlockingQueue:
上述的代码都是使用的ArrayBlockingQueue类来实现的。
他是一个基于数组的阻塞队列,必须指定队列大小。比较简单。ArrayBlockingQueue中只有一个ReentrantLock对象,这意味着生产者和消费者无法并行运行(见下面的代码)。另外,创建ArrayBlockingQueue时,可以指定ReentrantLock是否为公平锁,默认采用非公平锁。
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
LinkedBlockingQueue:
基于链表的阻塞队列,和ArrayBlockingQueue差不多。不过LinkedBlockingQueue如果不指定队列容量大小,会默认一个类似无限大小的容量,之所以说是类似是因为这个无限大小是Integer.MAX_VALUE,这么说就好理解ArrayBlockingQueue为什么必须要制定大小了,如果ArrayBlockingQueue不指定大小的话就用Integer.MAX_VALUE,那将造成大量的空间浪费,但是基于链表实现就不一样的,一个一个节点连起来而已。另外,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();
SynchronousQueue同步队列:
一种没有缓冲的等待队列。
什么叫做没有缓冲区,ArrayBlocking中有数组用于存储队列,LinkedBlockingQueue有链表村村队列:
/** The queued items */
private final E[] items;
static class Node<E> {
E item;
/**
* One of:
* - the real successor Node
* - this Node, meaning the successor is head.next
* - null, meaning there is no successor (this is the last node)
*/
Node<E> next;
Node(E x) { item = x; }
}
生产者/消费者操作数据实际上都是通过这两个"中介"来操作数据的,但是SynchronousQueue则是生产者直接把数据给消费者(消费者直接从生产者这里拿数据),好像又回到了没有生产者/消费者模型的老办法了。换句话说,每一个插入操作必须等待一个线程对应的移除操作。
放进入一个东西,必须等这个东西被取出去之后,才能再放。
SynchronousQueue的两种模式:
- 公平模式
采用公平锁,并配合一个FIFO队列(Queue)来管理多余的生产者和消费者 - 非公平模式
采用非公平锁,并配合一个LIFO栈(Stack)来管理多余的生产者和消费者,这也是SynchronousQueue默认的模式
示例:
public static void main(String[] args) {
BlockingQueue<Integer> queue = new SynchronousQueue<>(); // 同步队列
new Thread(() -> {
try {
for (int i = 1; i <= 3; i++) {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "==put" + i);
queue.put(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
for (int i = 1; i <= 3; i++) {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "==get" + queue.take());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
结果:
Thread-0==put1
Thread-1==get1
Thread-0==put2
Thread-1==get2
Thread-0==put3
Thread-1==get3
阻塞队列实现生产者消费者模型:
public static void main(String[] args) {
AtomicInteger a = new AtomicInteger(0);
final ArrayBlockingQueue queue = new ArrayBlockingQueue(5);
new Thread(() -> {
for (int i = 1; i < 1000; i++) {
try {
queue.put(i);
System.out.println("生产:" + i);
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true) {
try {
System.out.println("消费:" + queue.take());
TimeUnit.SECONDS.sleep(8);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
结果:
消费:1
生产:1
生产:2
生产:3
生产:4
生产:5
生产:6
消费:2
生产:7
消费:3
生产:8
消费:4
生产:9
消费:5
生产:10
解释一下:
- 先打印消费,是因为异步的关系,消费的输出语句先执行了,接着生产1消费1,相当于此时队列中没有数据;
- 接着由于消费时间较长,生产者连续生产了5个商品,接着不再生产,等待消费者过来消费
- 消费者消费了2,接着生产者生产,生产了一个之后队列又满了,等待消费者消费;
- 以此反复,生产者生产一个等待被消费,消费者消费后生产者再生产。