生产者消费者问题用于很好的解释线程间通信的问题,大致意思可以解释为如果货架上物品为空,则通知生产者开始生产物品,如果货架上的物品满了,就通知消费者进行消费。在JUC中实现主要是通过synchronized和lock实现,对于synchronized,一般采用wait(等待)和notify(唤醒)进行线程的同步通信。在juc并发包下对应的lock方法为await和signal,同时使用资源监视器Condition来进行精准唤醒。
一、Synchronized(wait和notify)
下面看一下第一版的生产者消费者案例:
class Store{
private int num=0; //记录商品数量
//生产者生产
public synchronized void Producer() throws InterruptedException {
if(num==10){
this.wait(); //货架满,阻塞生产者线程,wait释放锁
}
System.out.println(Thread.currentThread().getName()+"生产了第"+(++num)+"件商品!");
this.notifyAll();//通知消费者进行消费
}
public synchronized void Consumer() throws InterruptedException {
if(num==0){
this.wait(); //货架空,阻塞消费者线程,wait会释放锁
}
System.out.println(Thread.currentThread().getName()+"消费了第"+(num--)+"件商品!");
this.notifyAll();//通知生产者进行生产
}
public static void main(String[] args) {
Store store=new Store();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
store.Producer();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"Producer A ").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
store.Consumer();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"Consumer B ").start();
}
}
执行结果:
看起来好像没什么问题,运行也是正常,生产者线程A和消费者线程B先后访问资源。初始商品数量为0时,消费者线程B被阻塞,生产者线程A进行生产,当商品数量到达10个时,生产者线程A通过wait自行阻塞,释放锁。此时被唤醒的消费者线程B获得锁进行消费。
but,我们生产者和消费者只有一个吗,我们这是juc,多线程下当然有许多生产者和消费者,我们简单再增加一个生产者C一个消费者D就可能会出现 生产者生产出大于10个商品和消费者消费负数的商品的情况。
问题就出在这段代码上:
if(num==10){
this.wait(); //货架满,阻塞生产者线程,wait释放锁
}
if(num==0){
this.wait(); //货架空,阻塞消费者线程,wait会释放锁
}
这段代码用了if来进行判断操作,出现消费者消费负数商品的原因是某个生产者线程生产完一件商品后,执行了notifyAll()
方法,这样就将其他所有阻塞状态的线程唤醒,包括两个消费者线程,在其中一个消费者线程执行正常消费过程后,此时的商品余量为0,然而另一个消费者线程抢占锁(if那儿被阻塞的消费者线程被唤醒抢占到锁了),继续进行消费,这样便使得出现了负数。生产者生产超过10个商品原因同理。
也就是说,初始状态下(即生产者尚未生产商品,商品数量为0)两个消费者线程经过if()的判断,都被阻塞,当生产者生产完毕时,唤醒两个if中阻塞的消费者线程,这两个消费者线程便不需要再次判断当前是否还有商品,当一个消费者线程进行了正常消费,另一个消费者线程直接跳出if分支,抢占锁进行消费,便出现了这种错误。
解决方法: 就是将两个if
判断改为while
,当进入等待的线程被唤醒之后继续进行while判断,从而不会出现消费负数和生产超过10个问题。
官方文档也有说明:
改正代码如下:
class Store{
private int num=0; //记录商品数量
//生产者生产
public synchronized void Producer() throws InterruptedException {
while(num==10){
this.wait(); //货架满,阻塞生产者线程,wait释放锁
}
System.out.println(Thread.currentThread().getName()+"生产了第"+(++num)+"件商品!");
this.notifyAll();//通知消费者进行消费
}
public synchronized void Consumer() throws InterruptedException {
while(num==0){
this.wait(); //货架空,阻塞消费者线程,wait会释放锁
}
System.out.println(Thread.currentThread().getName()+"消费了第"+(num--)+"件商品!");
this.notifyAll();//通知生产者进行生产
}
public static void main(String[] args) {
Store store=new Store();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
store.Producer();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"Producer A ").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
store.Consumer();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"Consumer B ").start();
}
}
二、Condition(await、Singal)
下面看一下JUC版本的,为了体现出JUC版本的一些好处,我们使线程交替的执行,完成加一和减一操作。
/**
* 线程通信:交替执行
*/
public class ProducerConsumer {
public static void main(String[] args){
Data data=new Data();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.incrementLock();
} catch (Exception e) {
e.printStackTrace();
}
}
},"线程A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrementLock();
} catch (Exception e) {
e.printStackTrace();
}
}
},"线程B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.incrementLock();
} catch (Exception e) {
e.printStackTrace();
}
}
},"线程C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrementLock();
} catch (Exception e) {
e.printStackTrace();
}
}
},"线程D").start();
//synchronized版本测试
// new Thread(()->{
// for (int i = 0; i < 10; i++) {
// try {
// data.increment();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// },"线程A").start();
// new Thread(()->{
// for (int i = 0; i < 10; i++) {
// try {
// data.increment();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// },"线程B").start();
}
}
//判断等待、业务、通知
class Data{ //数字资源类,不用实现任何线程类,实现解耦合
private int number=0;
//+1操作 synchronized锁的对象是方法的调用者
public synchronized void increment() throws InterruptedException {
while(number!=0) this.wait(); //等待-1操作,用while让线程不停的等待着
number++;
System.out.println(Thread.currentThread().getName()+"-->"+number);
this.notifyAll();//通知-1操作
}
//-1操作
public synchronized void decrement() throws InterruptedException {
while(number==0) this.wait();//这里看API为什么要while,线程可以唤醒,而不会被通知,中断或超时,即所谓的虚假唤醒。
number--;
System.out.println(Thread.currentThread().getName()+"-->"+number);
this.notifyAll();
}
Lock lock=new ReentrantLock();
//资源监视器,相当于synchronized中的监视,一个监视器监视一把锁
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
public void incrementLock(){
lock.lock();
try{
while(number==1) condition1.await();//需要先获取锁
number++;
System.out.println(Thread.currentThread().getName()+"-->"+number);
condition2.signal();//实现精准唤醒
}catch (Exception ex){
}finally {
lock.unlock();
}
}
public void decrementLock(){
lock.lock();
try{
while(number==0) condition2.await();
number--;
System.out.println(Thread.currentThread().getName()+"-->"+number);
condition1.signal();
}catch (Exception ex){
}finally {
lock.unlock();
}
}
需要注意的是一个资源监视器(Condition)监视一把锁,从而可以使用signal()实现精准唤醒。
最后,我们还需要了解一下Synchronized
和ReentrantLock
以及更加细粒度的ReentrantReadWriteLock
的区别这块就完善了。可参见面试官:谈谈synchronized与ReentrantLock的区别?
ReentrantReadWriteLock例子:
public class ReadWriteLockTest {
public static void main(String[] args) {
MyCache myCache=new MyCache();
for (int i = 1; i <= 5; i++) {
final Integer temp=i;
new Thread(()->{
myCache.put(temp.toString(),Thread.currentThread().getName());
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final Integer temp=i;
new Thread(()->{
myCache.get(temp.toString());
},String.valueOf(i)).start();
}
}
}
class MyCache{
/**
* ReadWriteLock
* 独占锁(写锁):一次只能被一个线程占有
* 共享锁(读锁):多个线程可以同时占有锁对象
* 读-读(可以共存)、读-写(不能共存)、写-写(不能共存)
*/
private volatile Map<String,String> maps=new HashMap<String,String>();
private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();//读写锁,相比ReentrantLock更加细粒度
public void put(String key,String value){ //写入操作
readWriteLock.writeLock().lock(); //加写锁
try{
System.out.println(Thread.currentThread().getName()+"--->开始写操作....");
maps.put(key,value);
System.out.println(Thread.currentThread().getName()+"--->写入完成!");
}catch (Exception ex){
ex.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();//解开写锁
}
}
public void get(String key){
readWriteLock.readLock().lock();//开启读锁,虽然所有线程都可以共享读,但开锁就是为了使得读的时候不能进行写
try{
System.out.println(Thread.currentThread().getName()+"--->正在读数据:"+key);
maps.get(key);
}catch (Exception ex){
ex.printStackTrace();
}finally {
readWriteLock.readLock().unlock();//解开读锁
}
}
}
当然,还可以借助阻塞队列完成。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Main{
static BlockingQueue blockingQueue=new LinkedBlockingQueue(10);
public static void producer(){
for(int i=0;i<10;i++){
blockingQueue.add(i);
System.out.println(Thread.currentThread().getName()+"----生产了--"+i);
}
}
public static void consumer() throws InterruptedException {
while(true){
if(blockingQueue.size()==0){
System.out.println("本次结束!");
break;
}
System.out.println(Thread.currentThread().getName()+"----消费了--"+blockingQueue.take());
}
}
public static void main(String[] args) {
new Thread(()->{
producer();
},"producer").start();
new Thread(()->{
try {
consumer();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"consumer").start();
}
}