队列
队列:先进先出. 阻塞队列就是基于普通队列进行的优化.
queue是线程不安全的.所以我们对原有的代码进行优化使其线程安全.
阻塞队列
1.线程安全的.
2.具有阻塞的特性
(如果针对一个已满的队列入队,此时队列的操作就会阻塞,一直阻塞到队列不满(其他线程出队列元素)如果针对一个已经空的队列进行出队列那么就会阻塞,一直阻塞到队列不空(其他线程入队列元素))
基于阻塞队列就可以实现"生产者-消费者模型"
"生产-消费者"模型
例如生产者效率很高,消费者和生产者之间就可以引入一个阻塞队列,此时如果阻塞队列已满,生产就会被阻塞,此时生产者就会等待消费者消费后进行生产.
如果消费者效率高,阻塞队列空了之后消费就会被阻塞,此时消费者就会等待生产者生产.
表述的是一种多线程编程的方法.
生产消费者模型的优劣
1.生产消费者模型能很好的做到"解耦合"
实际开发中经常涉及到"分布式系统" , 一般来说一个功能不是由一个服务器完成的,而是由多个服务器每个服务器完成一部分,再通过服务器之间的网络通信最终完成整个项目.
2.另一方面,整个系统的结构更加复杂执行效率也会下降.
3.削峰填谷
客户端与服务器之间用一个阻塞队列进行连接来缓解这个客户端请求量激增导致服务器挂掉.
Java标准库里提供了阻塞队列的数据结构
BlockingQueue 它是一个Interface
可以实现为 ArrayBlockingQueue LinkedBlockingQueue PriorityBlockingQueue
使用put和offer都是入队列,但是put带有阻塞功能,而offer没有阻塞功能.
take出队列也是带有阻塞功能的
阻塞队列没有提供获取队首元素的方法.
阻塞队列的实现
我们需要考虑两点:
1.锁
相对来说比较简单,将进队列和出队列操作都上锁,每个线程之间不能相互干扰.
2.阻塞
对于put来说,队列满了之后就需要阻塞,当队列满了之后用wait进行阻塞,此时等待take操作.
wait必须在锁里使用,我们上一步刚好给if添加了锁,此时可以使用wait.
既然有阻塞,那也应该有唤醒,什么时候进行唤醒?
当进行take操作后就可以进行唤醒操作了.所以我们在take成功了之后就可以进行notify
同理,对于出队列来说,队列空的时候也需要阻塞,并且在入队列操作之后可以唤醒出队列操作.
class BlockingQueue{
private String[] elems = null;
private int head = 0;
private int tail = 0;
private int size = 0;
private Object locker = new Object();
public BlockingQueue(int capacity){
elems = new String[capacity];
}
public String take() throws InterruptedException {
//将return置于锁外,可以提高线程的执行效率.
String s = null;
//锁需要将if包裹
synchronized (locker) {
if (size == 0) {
locker.wait();
}
s = elems[head];
head++;
if (head >= elems.length) {
head = 0;
}
size--;
locker.notify();
}
return s;
}
public void put(String elem) throws InterruptedException {
//此时上锁应该全部上锁,如果不给if上锁的话,有可能第一个线程if判断后第二个线程紧接着判断,此时就会发生连续加两次的情况,使元素溢出
synchronized (locker) {
if(size >= elems.length) {
locker.wait();
}
elems[tail] = elem;
tail++;
if (tail >= elems.length) {
tail = 0;
}
size++;
locker.notify();
}
}
}
此时又有一个问题,我们如果多次进行了take操作,此时队列已经为空了,与此同时还有两个take线程走到了wait()进行阻塞等待,而我们此时进行一个put操作,那么第一个take被唤醒后进行了notify()操作唤醒了第二个take的wait()线程,进行了两次take,此时代码就出现了线程安全问题.
此时对代码进行完善:
我们将if改为while,在唤醒之后我们可以再进行一次判断.
此时如果上述情况,我们在进行第一次take操作之后,唤醒的take线程会在进行一次判断,如果检测到队列已满便又会进入wait阻塞,此时代码线程安全.而且在我们java的标准库中提醒了我们wait要搭配while进行使用.
class BlockingQueue{
private String[] elems = null;
private int head = 0;
private int tail = 0;
private int size = 0;
private Object locker = new Object();
public BlockingQueue(int capacity){
elems = new String[capacity];
}
public String take() throws InterruptedException {
//将return置于锁外,可以提高线程的执行效率.
String s = null;
//锁需要将if包裹
synchronized (locker) {
while (size == 0) {
locker.wait();
}
s = elems[head];
head++;
if (head >= elems.length) {
head = 0;
}
size--;
locker.notify();
}
return s;
}
public void put(String elem) throws InterruptedException {
//此时上锁应该全部上锁,如果不给if上锁的话,有可能第一个线程if判断后第二个线程紧接着判断,此时就会发生连续加两次的情况,使元素溢出
synchronized (locker) {
while (size >= elems.length) {
locker.wait();
}
elems[tail] = elem;
tail++;
if (tail >= elems.length) {
tail = 0;
}
size++;
locker.notify();
}
}
}
此时我们按照生产消费者模型对代码进行调试:
public class ThreadDemo3 {
public static void main(String[] args) {
BlockingQueue queue = new BlockingQueue(6);
Thread t1 = new Thread(() -> {
int n = 0;
while(true) {
try {
queue.put(n + "");
System.out.println("生产元素" + n);
Thread.sleep(1000);
n++;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread t2 = new Thread(() -> {
while (true) {
try {
String s = queue.take();
System.out.println("消费元素"+s );
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}
我们令生产快消费慢,此时阻塞队列会对消费进行阻塞,每生产一个元素都立刻会被消费掉,但是不会报错.
同理我们也可以令生产过快,观察结果我们会发现生产队列满之后不会溢出,而是阻塞等待消费
此时我们便完成了一个简单的阻塞队列以及生产消费者模型.