前言
本篇文章是多线程系列的第三篇(第二篇可参考多线程(二)),主要讲解:死锁、等待-唤醒机制、Lock和Condition。文章讲解的思路是:先通过一个例子来演示死锁的现象,再通过分析引出一系列的解决方案。同样,重点部分我都会用红色字体标识。
正文
死锁现象?
前一篇文章讲过:通过"synchronized"实现的同步是带有锁的。我们不免联想到生活中的一个场景:出门忘带钥匙被锁在了门外。其实这种情况在多线程程序中也可能会出现,并且它还有一个专业名称叫"死锁"。比如,下面这个程序就说明了可能会发生死锁的一个场景:
public class Test {
// 创建资源
private static Object resourceA = new Object();
private static Object resourceB = new Object();
public static void main(String[] args) {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread() + "get ResourceA");
try {
Thread.sleep(1000); // 休眠1s的目的是让线程B抢占到CPU资源从而获取到resourceB上的锁
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get ResourceB");
synchronized (resourceB) {
System.out.println(Thread.currentThread() + "get ResourceB");
}
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceB) {
System.out.println(Thread.currentThread() + " get ResourceB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get ResourceA");
synchronized (resourceA) {
System.out.println(Thread.currentThread() + "get ResourceA");
}
}
}
});
threadA.start();
threadB.start();
}
}
上面的代码就是可能会发生"死锁"的一个场景:同步的嵌套。线程A获取到了resourceA的监视器锁,然后调用sleep方法休眠了1s,在线程A休眠期间,线程B获取到了resourceB的监视器锁,也休眠了1s,当线程A休眠结束后会企图获取resourceB的的监视器锁,然而由于该资源被线程B所持有,所以线程A就会被阻塞并等待,而同理当线程B休眠结束后也会被阻塞并等待,最终线程A和线程B就陷入了相互等待的状态,也就产生了"死锁"。于是我们就可以用专业术语来总结什么是死锁:死锁就是指多个线程在执行的过程中,因争夺资源而造成的互相等待现象,并且在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
我们可以通过使用资源申请的有序性原则去避免死锁。那么什么是资源申请的有序性原则呢?它是指假如线程A和线程B都需要资源1,2,3,...,n时,对资源进行排序,线程A和线程B只有在获取了资源n-1时才能去获取资源n。就像下面这样:
public class Test {
private static Object resourceA = new Object();
private static Object resourceB = new Object();
public static void main(String[] args) {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread() + "get ResourceA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get ResourceB");
synchronized (resourceB) {
System.out.println(Thread.currentThread() + "get ResourceB");
}
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) { // 先获取resourceA的监视器锁
System.out.println(Thread.currentThread() + " get ResourceA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get ResourceA");
synchronized (resourceB) {
System.out.println(Thread.currentThread() + "get ResourceB");
}
}
}
});
threadA.start();
threadB.start();
}
}
等待唤醒机制?
线程间通信?
我们在第二篇文章中讲过的"卖票功能"其实是多个线程处理同一资源(即num),任务也相同(都是卖票),那么线程间通信就是指:多个线程处理同一资源,但是任务却不同。就像下面这样:
那么在这种情况下又会出现怎样的问题呢?现在考虑这样一个场景:有两个任务,一个输入负责为资源赋值,另外一个输出负责读取资源的值并打印。代码如下:
// 资源
class Resource
{
String name;
String sex;
}
// 输入
class Input implements Runnable
{
Resource r ;
Input(Resource r)
{
this.r = r;
}
public void run()
{
int x = 0;
while(true)
{
synchronized(r) {
if (x == 0) {
r.name = "mike";
r.sex = "nan";
} else {
r.name = "丽丽";
r.sex = "女女女女女女";
}
x = (x + 1) % 2;
}
}
}
}
// 输出
class Output implements Runnable
{
Resource r;
Output(Resource r)
{
this.r = r;
}
public void run()
{
while(true)
{
synchronized(r) {
System.out.println(r.name + "... ..." + r.sex);
}
}
}
}
class ResourceDemo
{
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();
}
}
由于使用了同步代码块,上面的代码就没有线程安全问题了。但是输出结果似乎有点不尽如人意:mike和丽丽都是成片输出,而我们希望的是输入一个就输出一个。在这种需求下,我们就需要使用到另一种技术:等待-唤醒机制,它其实就是wait()-notify()。我们的解决思路就是:输入线程为资源赋完值之后就去唤醒另一个输出线程去打印,并且输入线程在唤醒输出线程之后就进入等待状态。同理输出线程打印完之后就去唤醒另一个输入线程去赋值,并且输出线程在唤醒输入线程之后就进入等待状态。就像下面这样:
// 资源
class Resource
{
private String name;
private String sex;
private boolean flag = false; // 标记,false代表资源现在没有值
public synchronized void set(String name, String sex)
{
if(flag) // 如果现在资源有值
try{this.wait();}catch(InterruptedException e){}
this.name = name;
this.sex = sex;
flag = true;
this.notify();
}
public synchronized void out()
{
if(!flag) // 如果资源现在没有值
try{this.wait();}catch(InterruptedException e){}
System.out.println(name +"... ..." + sex);
flag = false;
notify();
}
}
// 输入
class Input implements Runnable
{
Resource r ;
Input(Resource r)
{
this.r = r;
}
public void run()
{
int x = 0;
while(true)
{
if(x == 0)
{
r.set("mike", "nan");
}
else
{
r.set("丽丽", "女女女女女女");
}
x = (x+1)%2;
}
}
}
// 输出
class Output implements Runnable
{
Resource r;
Output(Resource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.out();
}
}
}
class ResourceDemo
{
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()和sleep()的区别:
-
wait()可以指定时间也可以不指定;sleep()必须指定时间。
-
在同步中时,对CPU的执行权和锁的处理不同:wait()会释放执行权也会释放锁;sleep会释放执行权但不会不释放锁。
多生产者-多消费者问题?
"多生产者-多消费者问题"是学习"等待-唤醒机制"最经典的案例。顾名思义,这个案例其实就是:有多个生产者在生产资源,同时有多个消费者在消费资源。考虑如下情景:现在有多个人在生产烤鸭,同时有多个人在消费烤鸭。代码如下:
// 资源
class Resource
{
private String name;
private int count = 1;
private boolean flag = false;
public synchronized void set(String name)
{
while(flag)
try{this.wait();}catch(InterruptedException e){}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
flag = true;
notify();
}
public synchronized void out()
{
while(!flag)
try{this.wait();}catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName() + "...消费者........." + this.name);
flag = false;
notify();
}
}
// 生产者
class Producer implements Runnable
{
private Resource r;
Producer(Resource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.set("烤鸭");
}
}
}
// 消费者
class Consumer implements Runnable
{
private Resource r;
Consumer(Resource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.out();
}
}
}
class ProducerConsumerDemo
{
public static void main(String[] args)
{
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
我们通过执行上面的代码发现:出现了"死锁"。这其实是由于:生产者(或消费者)线程调用notify()不仅可以唤醒消费者(或生产者)线程,也可以唤醒生产者(或消费者)线程。从而导致所有线程都进入了休眠状态,也就出现了"死锁"。那我们如何解决这个问题呢?
notifyAll解决?
我们知道之所以出现上面"死锁"的情况是由于notify()唤醒了本方线程(即是生产者唤醒了生产者,消费者唤醒了消费者),这就导致对方线程由于没有线程notify它们而一直等待下去。于是我们可以通过notifyAll()唤醒所有线程,这样对方线程就能够被唤醒从而解决了死锁的问题。
// 资源
class Resource
{
private String name;
private int count = 1;
private boolean flag = false;
public synchronized void set(String name)
{
while(flag)
try{this.wait();}catch(InterruptedException e){}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
flag = true;
notifyAll(); // 唤醒所有线程
}
public synchronized void out()
{
while(!flag)
try{this.wait();}catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName() + "...消费者........." + this.name);
flag = false;
notifyAll(); // 唤醒所有线程
}
}
Lock解决?
通过notify()确实能够解决"多生产者-多消费者问题"的死锁情况,但是我们只是想唤醒对方线程,唤醒本方线程是没有意义的并且会多消耗资源。于是我们可以通过另一种方法来解决这个问题:Lock接口。
import java.util.concurrent.locks.*;
class Resource
{
private String name;
private int count = 1;
private boolean flag = false;
// 创建一个锁对象。
Lock lock = new ReentrantLock();
// 通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者。
Condition producer_con = lock.newCondition();
Condition consumer_con = lock.newCondition();
public void set(String name)
{
lock.lock();
try
{
while(flag)
try{producer_con.await();}catch(InterruptedException e){} // 生产者等待
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
flag = true;
consumer_con.signal(); // 唤醒消费者
}
finally
{
lock.unlock();
}
}
public void out()
{
lock.lock();
try
{
while(!flag)
try{consumer_con.await();}catch(InterruptedException e){} // 消费者等待
System.out.println(Thread.currentThread().getName() + "...消费者........." + this.name);
flag = false;
producer_con.signal(); // 唤醒生产者
}
finally
{
lock.unlock();
}
}
}
我们需要注意:在jdk1.5之前,同步的解决方案synchronized对锁的操作是隐式的;而在jdk1.5之后提供的Lock接口将锁和对锁的操作封装到对象中,将隐式变成了显式。同时它更为灵活,因为我们可以在一个锁上加上多组监视器(即Condition)。Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用。