生产者消费者问题是一个典型的并发问题,我们要解决的就是实现同步。一般我们都会想到synchronized和lock,今天我们用synchronized来实现这个问题,所以要了解synchronized,以及它和lock的区别。
什么是synchronized?
synchronized是Java提供的一个并发控制的关键字,作用于对象上。主要有两种用法,分别是同步方法(访问对象和clss对象)和同步代码块(需要加入对象),保证了代码的原子性和可见性以及有序性,但是不会处理重排序以及代码优化的过程,但是在一个线程中执行肯定是有序的,因此是有序的。
synchronized与lock的区别
synchronized | lock |
---|---|
java的一个关键字 | 一个接口,有很多的实现类 |
无法判断锁的状态 | 可以判断是否获取了锁 |
会自动释放锁 | 只能手动在finally中释放锁,否则会死锁 |
假设A线程获取锁的时候,B线程等待,如果A阻塞了,那么B只能永远等待 | lock会尝试去获取锁,有多种获取锁的方式 |
锁的类型是可重入锁,非公平锁,不可以可以中断的 | 可重入锁,默然非公平锁(可以手动设置),可以中断 |
功能单一,适合锁少量的同步代码 | API丰富,灵活度高,适合锁大量的同步代码 |
实现生产者消费者
package JUC;
public class ProducerAndConsumer {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
resource.incresement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
resource.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
//资源类
static class Resource{
//生产资源,生产者对其加1,消费者对其减1
private int num = 0;
public synchronized void incresement() throws InterruptedException {
//对增加进行判断
if(num > 0){
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName()+"->"+num);
//通知其他线程对其操作
this.notifyAll();
}
public synchronized void decrease() throws InterruptedException {
//对减少进行判断
if(num == 0){
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName()+"->"+num);
//通知其他线程对其操作
this.notifyAll();
}
}
}
我们可以发现在A提供生产之后,B消费,很好的解决了同步问题。但是我们如果我们增加了两个线程呢?
虚假唤醒
首先我们先给它增加两个线程,看看生产者和消费者是否还会同步。
我们在A和B下面再增加一个生产者,一个消费者
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
resource.incresement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
resource.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
我们截取一小段截图看看,发现并没有同步了。出现这种现象的原因就是虚假唤醒!
我们首先来分析为什么会出现这种情况,官方文档是这样说的
线程也可以唤醒,而不会被通知,中断或超时,即所谓的虚假唤醒 。 虽然这在实践中很少会发生,但应用程序必须通过测试应该使线程被唤醒的条件来防范,并且如果条件不满足则继续等待。 换句话说,等待应该总是出现在循环中,就像这样:
synchronized (obj) {
while (<condition does not hold>)
obj.wait(timeout);
... // Perform action appropriate to condition
}
首先我们知道了解决方法,即把if语句改成while语句
static class Resource{
//生产资源,生产者对其加1,消费者对其减1
private int num = 0;
public synchronized void incresement() throws InterruptedException {
//对增加进行判断
while(num > 0){
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName()+"->"+num);
//通知其他线程对其操作
this.notifyAll();
}
public synchronized void decrease() throws InterruptedException {
//对减少进行判断
while(num == 0){
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName()+"->"+num);
//通知其他线程对其操作
this.notifyAll();
}
}
但是说到底我们对虚假唤醒的这个概念还是有点模糊,所以我们举一个例子来说明
(1)首先初始num为0,A线程将一个元素入队,此时num=1;
(2)B线程从队列中获取了一个元素,此时num = 0。
(3)D线程也想从队列中获取一个元素,但此时num = 0,D线程便只能进入阻塞(decrease.wait()),等待 num > 0。
(4)这时,C线程将一个元素入队,并调用incresement.notify()唤醒条件变量。
(5) 处于等待状态的D线程接收到C线程的唤醒信号,便准备解除阻塞状态,执行接下来的任务(获取队列中的元素)。
(6) 然而可能出现这样的情况:当D号线程准备获得队列的锁,去获取队列中的元素时,此时B号线程刚好执行完之前的元素操作,返回再去请求队列中的元素,B线程便获得队列的锁,检查到num > 0,就获取到了C号线程刚刚入队的元素,然后释放队列锁。
(7) 等到D线程获得队列锁,判断发现num == 0,B线程“偷走了”这个元素,所以对于D线程而言,这次唤醒就是“虚假”的,它需要再次等待num > 0。
总结
生产者消费者是面试中容易被问到的问题,借此可以扩展一系列的问题。一定要了解synchronized与lock的区别,以及对synchronized的使用。
本文有参考自
https://www.cnblogs.com/tqyysm/articles/9765667.html
java1.8官方文档
网友评论