java多线程-初探(三)
本文阐述经典的多线程生产者、消费者模型。
涉及线程等待、唤醒、死锁以及常用的synchronized跟JDK5的Lock接口两种方式的知识点。
生产者、消费者模型初步理解
生产者:负责给资源中保存数据(资源),当资源中的数据被放满了,这时生产者就应该停止继续保存数据。
消费者:负责从资源中取出数据进行消费。当资源中的数据被取完,这时就应该停止取出数据的动作。
生产者在给资源中保存数据的时候,应该需要判断资源中的数据是否已经保存满了。
消费者在从资源中取出数据的时候,应该判断资源中是否有数据。
单生产,单消费多线程初步实现
class Main
package com.thread.four;
public class Main {
public static void main(String[] args) {
//创建资源类
Resource resource = new Resource() ;
//创建生产者
Producer producer = new Producer(resource) ;
//创建消费者
Consumer consumer = new Consumer(resource) ;
//创建线程类
Thread producerThread = new Thread(producer) ;//生产者的线程
Thread consumerThread = new Thread(consumer) ;//消费者的线程
//启动线程
producerThread.start();
consumerThread.start();
}
}
class Producer
package com.thread.four;
/**
* 生产者
* @author w954
*
*/
public class Producer implements Runnable{
private Resource resource ;
public Producer(Resource resource){
this.resource = resource;
}
@Override
public void run(){
for(int i = 0 ; i <= 100 ; i++){
resource.setResource("一大碗白米饭");
}
}
}
class Consumer
package com.thread.four;
/**
* 消费者
* @author w954
*
*/
public class Consumer implements Runnable{
private Resource resource ;
public Consumer(Resource resource){
this.resource = resource;
}
public void run(){
for(int i = 0 ; i <= 100 ; i++){
resource.getResource();
}
}
}
class Resource
package com.thread.four;
/**
* 资源类
* @author w954
*
*/
public class Resource{
// 定义资源
private String resource ;
// 定义锁
private static final Object lock = new Object();
int count = 1;
/**
* 生产者调用的生产资源的方法
* @param resourceName 资源名
*/
public void setResource(String resourceName){
synchronized (lock) {
if(resource == null) {
resource = resourceName + count++;
System.out.println(Thread.currentThread().getName() + "----生产者----已生产:" + resource);
}
}
}
/**
* 消费者调用的消费资源的方法
*/
public void getResource(){
synchronized (lock) {
if(resource != null) {
System.out.println(Thread.currentThread().getName() + "--消费者--已消费:" + resource);
resource = null;
}
}
}
}
控制台输出
我们想要的结果
一生产一消费,生产者生产完一个资源,消费者就消费一个资源。
并且预计是生产100碗饭,消费也是消费100碗饭。
上述代码可能导致的现象
生产者一直在生产、消费者一直在消费。
导致原因
生产者的cpu一直没有切换到消费者,导致生产者一直在生产饭。
消费者的cpu一直没有切换到生产者,导致消费者一直在消费饭。但是代码判断非空了,所以没有打印。
解决思路
生产者生产的时候判断现在有没有饭,如果有就唤醒消费者先去消费,消费者消费完成之后唤醒生产者去生产饭,以此达到和谐。
关键性代码
class Resource
/**
* 生产者调用的生产资源的方法
* @param resourceName 资源名
*/
public void setResource(String resourceName){
synchronized (lock) {
if(resource != null) {
// 资源不为空,自己等待
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
resource = resourceName + count++;
System.out.println(Thread.currentThread().getName() + "----生产者----已生产:" + resource);
// 生产完成,现在有资源了,唤醒消费者去消费
lock.notify();
}
}
/**
* 消费者调用的消费资源的方法
*/
public void getResource(){
synchronized (lock) {
if(resource == null) {
// 没资源了,自己等待
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "--消费者--已消费:" + resource);
resource = null;
// 消费完成,现在没有资源了,唤醒生产者去生产
lock.notify();
}
}
以此和谐得不得了。接下来解释一下上述代码。
wait
当线程执行wait方法时,会把当前的锁释放,然后让出CPU,进入等待状态。
notify
当执行notify方法时,会唤醒一个处于等待该对象锁的线程,然后继续往下执行。
解释完上述两个等待唤醒啥意思后,我们来简单读一下代码。
首次当消费者来调用getResource函数消费的时候,发现resource==null。于是消费者调用wait函数,自己等待,把lock这个对象锁释放了。cpu此时就会跑到其他空闲线程上,发现有一个生产者线程,就执行生产者,生产者获取锁进入同步代码块(消费者等待的时候已经把锁释放了),判断资源为空,就去生产资源了,生产完成就调用notify函数,唤醒刚刚等待这个lock对象锁的消费者线程,从刚刚消费者等待那里接着往下执行,消费者就去消费了,消费完又唤醒等待这个对象锁的线程,此时发现没有等待的线程,cpu就随意切换了。切换到哪个都能正常执行。
多生产,多消费实现
所谓多生产多消费就是把生产者弄成多个,消费者也多个。
我们刚刚的代码可以看到生产者就一个,消费者也就一个。
我们先直接更改代码运行看看会出现什么问题。
class Main
package com.thread.four;
public class Main {
public static void main(String[] args) {
//创建资源类
Resource resource = new Resource() ;
//创建生产者
Producer producer = new Producer(resource) ;
//创建消费者
Consumer consumer = new Consumer(resource) ;
//创建线程类
Thread producerThread = new Thread(producer) ;//生产者的线程
Thread producerThread1 = new Thread(producer) ;//生产者的线程
Thread consumerThread = new Thread(consumer) ;//消费者的线程
Thread consumerThread1 = new Thread(consumer) ;//消费者的线程
//启动线程
producerThread.start();
producerThread1.start();
consumerThread.start();
consumerThread1.start();
}
}
出现了消费者消费null的情况。为啥嘞????
导致原因
最根本的原因是因为消费者唤醒了上一个在等待的消费者。
我们来理解一下这句话,假设上一个消费者执行到判断资源的时候,发现没有资源,于是处于等待。
这时cpu该死没切到我们想要的生产者线程上,它跑到了另一个消费者线程上,那这个消费者又来了。发现没资源,又等待。
现在的状态就是两个处于等待的消费者线程,两个空闲的生产者线程。
这时cpu终于切换到了生产者线程上,这时候生产者生产完资源,执行完成该唤醒处于等待这个lock锁的对象的线程(也就是那两个处于等待的消费者其中一个),唤醒某一个消费者之后,这个消费者消费了资源,又接下去又执行唤醒处于等待这个lock锁对象的线程(也就是另一个处于等待的消费者),这时这个消费者醒了,当他去消费时候,这时资源已经被上一个消费者消费了。所以这个消费者就输出null。
初步解决思路
消费者或者生产者唤醒之后,再判断一次是否有商品。这样是否就解决了?
class Resource
/**
* 生产者调用的生产资源的方法
* @param resourceName 资源名
*/
public void setResource(String resourceName){
synchronized (lock) {
while (resource != null) {
// 资源不为空,自己等待
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
resource = resourceName + count++;
System.out.println(Thread.currentThread().getName() + "----生产者----已生产:" + resource);
// 生产完成,现在有资源了,唤醒消费者去消费
lock.notify();
}
}
/**
* 消费者调用的消费资源的方法
*/
public void getResource(){
synchronized (lock) {
while (resource == null) {
// 没资源了,自己等待,让生产者先去生产
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "--消费者--已消费:" + resource);
resource = null;
// 消费完成,现在没有资源了,唤醒生产者去生产
lock.notify();
}
}
把if的判断改成while,这样确实每次都会判断了。
但是代码出现bug了,执行到一半,程序不动了。这种现象称为死锁现象
上述代码错误原因分析
生产者1生产完资源进入等待的栈内存中,cpu切换到生产者2上,生产者2判断是否有资源,此时有资源,生产者2调用wait函数也进入栈内存等待了。此时两个生产者都在栈内存中等待。
这时cpu切换到消费者1身上,消费者1把资源消费完成之后进入栈内存等待,同时调用notify函数把两个生产者其中一个唤醒了,假设生产者1被唤醒了,但是此时cpu却切换到消费者2身上,消费者2判断没有资源也进入等待的栈内存中。
此时栈内存中等待的分别是生产者2、消费者1、消费者2
这是cpu执行到生产者1,生产者1判断没有资源则生产完资源后也进入栈内存等待,这时调用notify函数唤醒一个等待的线程,这时如果唤醒的是生产者2,那生产者2判断是否有资源,有资源也进入栈内存等待了。
结果就是四个线程全在等待。导致程序无法继续执行。
在JDK1.5之前的解决上述这个死锁的方案
将notify函数改成notifyAll函数。
notity:唤醒一个等待该锁对象的等待线程。
notify:唤醒所有在等待该锁对象的等待线程。
如果不管是生产者还是消费者,唤醒的时候把栈内存中所有的线程都唤醒是不是就能解决?
我们来试试看!
class Main
/**
* 生产者调用的生产资源的方法
* @param resourceName 资源名
*/
public void setResource(String resourceName){
synchronized (lock) {
while (resource != null) {
// 资源不为空,自己等待
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
resource = resourceName + count++;
System.out.println(Thread.currentThread().getName() + "----生产者----已生产:" + resource);
// 生产完成,现在有资源了,唤醒消费者去消费
lock.notifyAll();
}
}
/**
* 消费者调用的消费资源的方法
*/
public void getResource(){
synchronized (lock) {
while (resource == null) {
// 没资源了,自己等待,让生产者先去生产
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "--消费者--已消费:" + resource);
resource = null;
// 消费完成,现在没有资源了,唤醒生产者去生产
lock.notifyAll();
}
}
至此和谐得不得了!但是如果这么好用,也不会出现后来JDK5中的Lock接口了。
上述代码的弊端
唤醒notifyAll函数,必然会唤醒跟自己一样的线程,唤醒没问题,没bug,但是效率低啊!
如生产者把生产者也给唤醒了,消费者把消费者也给唤醒了。
如果最终能实现生产者只唤醒消费者,而消费者只唤醒生产者,那岂不是万物和谐了?
JDK5中的Lock接口跟Condition接口实现多生产多消费方式
Lock
在synchronized上的锁是开发人员自己定义的任意锁对象,而在jdk5中专门提供Lock接口来表示同步中的锁对象。
它代替了同步和同步中的锁,将synchronized代码块获取跟释放锁的操作指定到具体的方法中。
如:使用Lock接口获取锁就要手动调用lock()函数获取锁,释放锁则调用unlock()函数释放。
Condition
代替原有的wait、notify、notifyAll等待唤醒机制。
通常获取condition对象采用lock.newCondition;获得。
每new一个condition可以绑定一个等待唤醒线程。如题就是两个condition。一个生产者一个消费者。
lock跟condition关系图
以此来达到生产者唤醒消费者,消费者唤醒生产者。(调用对方的唤醒函数)
class Resource
package com.thread.four;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 采用JDK1.5的Lock接口实现方式
*/
public class JdkLockResource {
// 定义资源
private String resource ;
// 定义锁
private static final Lock lock = new ReentrantLock();
// 定义生产者线程
private static final Condition producer = lock.newCondition();
// 定义消费者线程
private static final Condition consumer = lock.newCondition();
int count = 0;
/**
* 生产者调用的生产资源的方法
* @param resourceName 资源名
*/
public void setResource(String resourceName){
// 获取锁
lock.lock();
try {
while (resource != null){
// 生产者等待消费者消费
try {
producer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
resource = resourceName + count++;
System.out.println(Thread.currentThread().getName() + "--生产者--已生产:" + resource);
// 唤醒消费者消费
consumer.signal();
} finally {
// 释放锁
lock.unlock();
}
}
/**
* 消费者调用的消费资源的方法
*/
public void getResource(){
// 获取锁
lock.lock();
try {
while (resource != null){
// 消费者等待生产者生产
try {
consumer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "--消费者--已消费:" + resource);
// 模拟消费,此时应将资源置空
resource = null;
// 消费完成。唤醒生产者线程进行生产资源 唤醒一条生产者线程
producer.signal();
} finally {
// 释放锁
lock.unlock();
}
}
}
简要说明
释放锁因为要手动调用unlock函数,如抛异常,则锁无法释放,所以必须要在finally处添加释放锁逻辑。
Condition对象是从lock锁对象中获取监视具体的线程的,所以可以用他来管理生产者跟消费者的等待唤醒。
那业务开发中是应该都用jdk5的Lock接口吗?
不一定!
个人建议:如果不需要等待唤醒机制,只是操作同一个数据,则用synchronized更为简洁方便。
但如果涉及到多个线程操作同一组数据,如买票跟添加票的业务场景,或者商品上架数量跟抢购的业务场景,则用jdk5的Lock接口实现等待唤醒机制效率更高