生产者与消费者模型是Java多线程中的常见经典模型。为此特写一篇博客记录该模式中线程不安全问题的产生以及解决方案。
一、单消费者与单生产者模式
所谓生产者消费者模式,就是指一个/多个线程生产东西(往资源中赋值等),另一个/多个线程消费资源中的东西(输出资源中的内容等)。下面这个例子为一个线程赋值名字和性别,另一个线程负责输出名字和性别。
class Resource{
String name;
String sex;
boolean flag = true;
}
class Input implements Runnable{
Resource r;
Input(Resource r){
this.r = r;
}
public void run() {
while(true) {
synchronized (r) {
if(r.flag) {
r.name = "john";
r.sex = "------->male";
r.flag = false;
} else {
r.name = "lilly";
r.sex = "----------->female";
r.flag = true;
}
}
}
}
}
class Output implements Runnable{
Resource r;
Output(Resource r){
this.r = r;
}
public void run() {
while(true) {
synchronized (r) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(r.name+r.sex);
}
}
}
}
public class Demo {
public static void main(String[] args) {
Resource r = new Resource();
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();t2.start();
}
}
注意这里同步的使用。如果不加同步的话,在修改完姓名后输出线程可能会得到执行权并执行输出语句。此时性别并没有修改,所以可能会出现如john------->female性别不统一的线程问题。同步的锁必须是同一个(这里为资源r),每一个锁都会管理属于自己的一组线程。如果将Ouput中同步代码块中的锁换成别的锁(比如new Object())或者不加锁,都不会使得Ouput被r锁管理,进而导致线程安全问题的出现。
可以看到,Input、Output中的锁都为r。所以可以把Input、Output变为Resource中的同步函数,他们俩的锁也都为调用他们的同一个r对象(this)。
class Resource{
private String name;
private String sex;
private boolean flag = true;
public synchronized void set() {
if(this.flag) {
this.name = "john";
this.sex = "------->male";
this.flag = false;
} else {
this.name = "lilly";
this.sex = "----------->female";
this.flag = true;
}
}
public synchronized void show() {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.name+this.sex);
}
}
class Input implements Runnable{
Resource r;
Input(Resource r){
this.r = r;
}
public void run() {
while(true) {
r.set();
}
}
}
class Output implements Runnable{
Resource r;
Output(Resource r){
this.r = r;
}
public void run() {
while(true) {
r.show();
}
}
}
public class Demo {
public static void main(String[] args) {
Resource r = new Resource();
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();t2.start();
}
}
二、多消费者与多生产者模式
首先介绍一下wait()、notify()、notifyAll()的使用:
wait():将当前线程释放资源进入等待阻塞状态。
notify():随机唤醒所属于当前锁的线程池中的任意一个线程,使其可以被调度。
notifyAll():唤醒所属于当前锁的线程池中的所有线程。
由于任何的对象都可以作为锁,所以这三个方法定义在Object类中。因此这三个对象必须在同步代码块中才可以使用,且必须由所属的锁调用(锁.wait()/notify()/notifyAll())。下面这段代码为生产一只烤鸭,消费一只烤鸭。
class Resource{
private String name ;
private int num = 1;
boolean flag = true;
public synchronized void set() {
//若t1调度完后下一次仍是t1,则判断flag让t1等待不继续生产,在t2消费完改变标记后继续生产
if(!this.flag)
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = "duck"+"------>"+num;
System.out.println("生产"+this.name);
num++;
this.flag = false;
this.notify();
}
public synchronized void show() {
if(this.flag)
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费"+this.name);
this.flag = true;
this.notify();
}
}
class Input implements Runnable{
Resource r;
Input(Resource r){
this.r = r;
}
public void run() {
while(true) {
r.set();
}
}
}
class Output implements Runnable{
Resource r;
Output(Resource r){
this.r = r;
}
public void run() {
while(true) {
r.show();
}
}
}
public class Demo {
public static void main(String[] args) {
Resource r = new Resource();
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();t2.start();
}
}
在只有一个线程生产,一个线程消费的时候,程序是和谐的。但在多生产者多消费者模式时,会有以下安全性问题产生:
class Resource{
private String name ;
private int num = 1;
boolean flag = true;
public synchronized void set() {
if(!this.flag)
try {
this.wait(); //如果原先wait再被唤醒继续执行的话从此处开始
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = "duck"+"------>"+num;
System.out.println("生产"+this.name);
num++;
this.flag = false;
this.notify();
}
public synchronized void show() {
if(this.flag)
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费"+this.name);
this.flag = true;
this.notify();
}
}
class Input implements Runnable{
Resource r;
Input(Resource r){
this.r = r;
}
public void run() {
while(true) {
r.set();
}
}
}
class Output implements Runnable{
Resource r;
Output(Resource r){
this.r = r;
}
public void run() {
while(true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
r.show();
}
}
}
public class Demo {
public static void main(String[] args) {
Resource r = new Resource();
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(in);
Thread t3 = new Thread(out);
Thread t4 = new Thread(out);
t1.start();t2.start();
t3.start();t4.start();
}
}
输出:
生产duck------>1
消费duck------>1
消费duck------>1
消费duck------>1
消费duck------>1
这是因为,在t1、t2、t4wait时,当t3消费duck------>1后,t3notify()了t4线程(t4A处于wait状态),t4拿到了CPU执行权,多消费了duck------>1,产生了线程不安全问题。
这个问题的原因在于if判断上。当t4拿到执行权后,不会重新判断flag(会继续从wait出开始执行),即使t3已经把flag改为了true。因此貌似把if改为while让t4回头重新判断一下标记即可。可真的是这样吗?
class Resource{
private String name ;
private int num = 1;
boolean flag = true;
public synchronized void set() {
while(!this.flag)
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = "duck"+"------>"+num;
System.out.println("生产"+this.name);
num++;
this.flag = false;
this.notify();
}
public synchronized void show() {
while(this.flag)
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费"+this.name);
this.flag = true;
this.notify();
}
}
class Input implements Runnable{
Resource r;
Input(Resource r){
this.r = r;
}
public void run() {
while(true) {
r.set();
}
}
}
class Output implements Runnable{
Resource r;
Output(Resource r){
this.r = r;
}
public void run() {
while(true) {
r.show();
}
}
}
public class Demo {
public static void main(String[] args) {
Resource r = new Resource();
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(in);
Thread t3 = new Thread(out);
Thread t4 = new Thread(out);
t1.start();t2.start();
t3.start();t4.start();
}
}
如果将此段程序多运行几次,会发现死锁的产生。这是因为notify()只会随即唤醒一个线程。当t2、t3、t4处于wait状态,t1处于运行状态时,t1在唤醒t2并改变flag后再次获得执行权,t1处于wait状态。此时t2获得执行权,回头再判断flag,也进入wait状态。此时四个线程全部wait,造成了死锁。
那么如何解决?很简单,只需要使用notifyAll()将所有线程全部唤醒即可。这样即使己方的线程全部处于wait状态,也会有对方的两个线程具有执行资格(其中一个具有执行权),永远不会造成死锁。
class Resource{
private String name ;
private int num = 1;
boolean flag = true;
public synchronized void set() {
while(!this.flag)
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = "duck"+"------>"+num;
System.out.println("生产"+this.name);
num++;
this.flag = false;
this.notifyAll();
}
public synchronized void show() {
while(this.flag)
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费"+this.name);
this.flag = true;
this.notifyAll();
}
}
class Input implements Runnable{
Resource r;
Input(Resource r){
this.r = r;
}
public void run() {
while(true) {
r.set();
}
}
}
class Output implements Runnable{
Resource r;
Output(Resource r){
this.r = r;
}
public void run() {
while(true) {
r.show();
}
}
}
public class Demo {
public static void main(String[] args) {
Resource r = new Resource();
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(in);
Thread t3 = new Thread(out);
Thread t4 = new Thread(out);
t1.start();t2.start();
t3.start();t4.start();
}
}
三、使用ReentrantLock优化解决
如果使用notifyAll()的话,己方线程也会被唤醒,但是并不需要。这会导致效率的降低。可以使用ReentrantLock与Condition配合解决这个问题,使得唤醒更加精确。
Lock接口:
synchronized中的锁为隐式操作,使用Lock接口后可以把锁转变显式操作。同事一个Lock还可以挂多个Condition监视器对象。
lock():获取锁。
unlock():释放锁。
ReentrantLock:
属于Lock的实现类,是一个互斥锁,也就是如果一个线程拿到了改锁,别的线程无法拿到。
Condition接口:
由于synchronized中只能使用一组监视器方法(wait、notify、notifyAll),所以产生了Condition对象使得一个锁可以挂载多个监视器对象来使用多组监视器方法。Condition封装了以上三个方法:
wait()-------->await()
notify()-------->signal()
notifyAll()-------->signalAll()
import java.util.concurrent.locks.*;;
class Resource{
private String name ;
private int num = 1;
boolean flag = true;
Lock lock = new ReentrantLock();
Condition produer_con = lock.newCondition();
Condition Consumer_con = lock.newCondition();
public void set() {
lock.lock();
while(!this.flag)
try {
produer_con.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = "duck"+"------>"+num;
System.out.println("生产"+this.name);
num++;
this.flag = false;
Consumer_con.signal();
lock.unlock();
}
public void show() {
lock.lock();
while(this.flag)
try {
Consumer_con.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费"+this.name);
this.flag = true;
produer_con.signal();
lock.unlock();
}
}
class Input implements Runnable{
Resource r;
Input(Resource r){
this.r = r;
}
public void run() {
while(true) {
r.set();
}
}
}
class Output implements Runnable{
Resource r;
Output(Resource r){
this.r = r;
}
public void run() {
while(true) {
r.show();
}
}
}
public class Demo {
public static void main(String[] args) {
Resource r = new Resource();
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(in);
Thread t3 = new Thread(out);
Thread t4 = new Thread(out);
t1.start();t2.start();
t3.start();t4.start();
}
}