10.5 线程死锁和协作
多线程同步,解决的是多线程安全性的问题,避免获取错误的数据,但同步也同时会带来性能损耗和线程死锁的问题。本节通过案例演示什么是线程死锁,并简单介绍解决线程死锁的方法。解决了多线程之间的问题后,本节还会介绍线程之间相互协作,通过多线程间的协作完成系统的功能。
10.5.1 线程死锁
多线程同步的好处是避免了线程获取错误数据,但多线程同步也带来了性能问题。多线程同步采用了同步代码块和同步方法的方式,依靠的是锁机制实现了互斥访问。因为是互斥的访问,所以不能并行处理,存在性能问题。
多线程同步的性能问题还只是快和慢的问题,但如果出现了线程死锁,那可能直接导致程序众多的线程都处于阻塞状态,无法继续运行。
如果线程A只有等待另一个线程B的完成才能继续,而在线程B中又要等待线程A的资源,那么这两个线程相互等待对方释放锁时就会发生死锁。出现死锁后,不会出现异常,不会出现提示,只是相关线程都处于阻塞状态,无法继续运行。
下面仍然通过一个案例来演示线程的死锁,具体代码如下:
public class DeadLockThread{
//创建两个线程之间竞争使用的对象
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args){
new Thread(new ShareThread1()).start();
new Thread(new ShareThread2()).start();
}
private static class ShareThread1 implements Runnable
{
public void run(){
synchronized(lock1){
try{
Thread.sleep(50);
}catch(InterruptedException e)
{
e.printStackTrace();
}
synchronized(lock2){
System.out.println("ShareThread1");
}
}
}
}
private static class ShareThread2 implements Runnable
{
public void run(){
synchronized(lock2){
try{
Thread.sleep(50);
}catch(InterruptedException e)
{
e.printStackTrace();
}
synchronized(lock1){
System.out.println("ShareThread2");
}
}
}
}
}
上面的代码中,创建了两个线程之间竞争使用的对象lock1和lock2,内部类ShareThread1在run()方法中先对lock1上锁,然后对lock2上锁,并且只有lock2代码块运行结束解锁之后,lock1才能运行结束解锁。类似的内部类ShareThread2在run()方法中先对lock2上锁,然后对lock1上锁,并且只有lock1代码块运行结束解锁之后,lock2才能运行结束解锁。当这两个线程启动以后,分别都握着第一个锁,等待第二个锁,程序死锁!
当多个线程竞争多个排他性锁的时候,可能出现死锁。解决的方式为多个线程以同样的顺序获取锁,不出现交叉也就不会出现死锁的问题。
10.5.2 产生死锁的原因及条件
为什么会产生死锁?什么情况下可能会导致死锁?下面,我们就一起来探讨死锁产生的原因及必要条件。
死锁产生的原因有以下三个方面。
(1)系统资源不足。如果系统的资源充足,所有进程的资源请求都能够得到满足,自然就不会发生死锁。
(2)进程运行推进的顺序不合适。
(3)资源分配不当等。
产生死锁的必要条件有以下四个。
(1)互斥条件:一个资源每次只能被一个进程使用。
(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
只要系统发生死锁,这四个条件就必然成立;反之,只要破坏四个条件中的任意一个,就可以避免死锁的产生。
10.5.3 线程协作
通过之前的学习,已经了解并初步解决了多线程之间可能出现的问题,下一步学习的重点是如何让线程之间进行有效协作。线程协作的一个典型案例就是生产者和消费者问题,生产者和消费者的这种协作是通过线程之间的握手来实现的,而这种握手又是通过Object类的wait()和notify()方法来实现的。下面具体来了解生产者和消费者问题。
有一家餐厅举办吃热狗活动,活动时有5个顾客来吃,3个厨师来做。为了避免浪费,制作好的热狗被放进一个能装10个热狗的长条状容器中,并且按照先进先出的原则取热狗。如果长条容器被装满,则厨师已经做完的热狗不再往长条容器里放,同时停止做热狗;如果顾客发现长条容器内的热狗吃完了,则提醒厨师再做热狗。这里的厨师就是生产者,顾客就是消费者。
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。对于生产者,当生产的产品装满了仓库,则需要停止生产,等待消费者消费后提醒生产者继续生产。对于消费者,当发现仓库中已没有产品时,则不能消费,等待生产者生产出产品以后通知消费者可以消费。
之前学习的synchronized关键字可实现对共享资源的互斥操作,但无法实现不同线程之间消息的传递。Java提供了wait()、notify()、notifyAll()三个方法,解决线程之间协作的问题。这三个方法均是java.lang.Object类的方法,但都只能在同步方法或者同步代码块中使用,否则会抛出异常。下面是这三个方法的简单介绍。
- void wait()
当前线程等待,等待其他线程调用此对象的notify()方法或notifyAll()方法将其唤醒。
- void notify()
唤醒在此对象锁上等待的单个线程。
- void notifyAll()
唤醒在此对象锁上等待的所有线程。
图10.10所示的是线程等待与唤醒的示意图。
完成吃热狗活动的需求有一定的难度,现整理思路如下。
(1)定义一个集合模拟长条容器存放热狗,集合里实际存放Integer对象,其数值代表热狗的编号(热狗编号规则举例:300002代表编号为3的厨师做的第2个热狗),这样能通过集合添加和删除操作实现长条容器内热狗的先进先出。
(2)以热狗集合作为对象锁,所有对热狗集合的操作(在长条容器中添加或取走热狗)互斥,这样保证不会出现多个顾客同时取最后剩下的一个热狗的情况,也不会出现多个厨师同时添加热狗造成长条容器里热狗数大于10个的情况。
图10.10 线程等待与唤醒
(3)当厨师希望往长条容器中添加热狗时,如果发现长条容器中已有10个热狗,则停止做热狗,等待顾客从长条容器中取走热狗的事件发生,以唤醒厨师可以重新进行判断,是否需要做热狗。
(4)当顾客希望从长条容器中取走热狗时,如果发现长条容器中已没有热狗,则停止吃热狗,等待厨师往长条容器中添加热狗的事件发生,以唤醒顾客可以重新进行判断,是否可以取走热狗吃。
实现此功能的代码如下:
import java.util.*;
public class TestProdCons {
//定义一个存放热狗的集合,里面存放的是整数,代表热狗编号
private static final List<Integer> hotDogs = new ArrayList<Integer>();
public static void main(String[] args){
for(int i = 1;i <= 3;i++){
new Producer(i).start();
}
for(int i = 1;i <= 5;i++){
new Consumer(i).start();
}
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.exit(0);
}
//生产者线程,以热狗集合作为对象锁,所有对热狗集合的操作互斥
private static class Producer extends Thread{
int i = 1;
int pid = -1;
public Producer(int id){
this.pid = id;
}
public void run(){
while(true){
try{
//模拟消耗的时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(hotDogs){
if(hotDogs.size() < 10){
//热狗编号,300002代表编号为3的生产者生产的第2个热狗
hotDogs.add(pid*10000 + i);
System.out.println("生产者" + pid + "生产热狗,编号为:" + pid*10000 + i);
i++;
//唤醒hotDogs对象锁上所有调用wait()方法的线程
hotDogs.notifyAll();
}else{
try{
System.out.println("热狗数已到10个,等待消费!");
hotDogs.wait();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
//消费者线程,以热狗集合作为对象锁,所有对热狗集合的操作互斥
private static class Consumer extends Thread {
int cid = -1;
public Consumer(int id){
this.cid = id;
}
public void run(){
while(true){
synchronized (hotDogs) {
try{
//模拟消耗的时间
Thread.sleep(200);
}catch(InterruptedException e) {
e.printStackTrace();
}
if(hotDogs.size() > 0) {
System.out.println("消费者" + this.cid + "正在消费一个热狗,其编号为:
" + hotDogs.remove(0));
hotDogs.notifyAll();
}else{
try{
System.out.println("已没有热狗,等待生产!");
hotDogs.wait();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
编译、运行程序,运行结果如图10.11所示。通过调整生产者和消费者模拟消耗的时间,重新编译、运行程序,程序运行结果会显示出符合需求的不同情况,大家可以尝试一下。
图10.11 生产者消费者问题