Java 经典多线程问题(一)生产者消费者问题
Java 经典多线程问题(二)死锁问题
一、线程的死锁
线程死锁的必要条件:(1) 互斥条件: 资源只能被一个或有限个线程使用。
(2) 请求与保持条件: 当进程在申请资源被阻塞时,不释放自己拥有的资源。
(3) 不剥夺条件: 进程已拥有的资源不能强行剥夺。
(4) 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系。
要出现死锁,以上四个必要条件缺一不可。所以,解决死锁问题,就是破坏以上四个条件的任何一个,实际解决时应根据业务需求适当选取。
注意:发生死锁时一定具备上述四个条件,但具备上述四个条件时,不一定会发生死锁。
二、哲学家就餐问题
哲学家就餐问题是死锁的一个经典问题。
情境模拟:在一个圆桌上,有n个哲学家,n只筷子,每个哲学家左右两边各返一只筷子。
哲学家可以进行思考和吃饭,思考时,不获取筷子。
吃饭时,必须同时获得左右两只筷子才能吃饭
三、死锁时的哲学家就餐代码:
/**
* 哲学家类
*/
public class Philosopher implements Runnable{
// 左边的筷子
private Chopstick leftChopstick;
// 右边的筷子
private Chopstick rightChopstick;
// 哲学家名字
private String name;
public Philosopher( Chopstick leftChopstick, Chopstick rightChopstick, String name) {
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
this.name = name;
}
@Override
public void run() {
eat();
think();
}
public void eat(){
System.out.println(name + "饥饿");
leftChopstick.requestUse(name);
rightChopstick.requestUse(name);
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();;
}
leftChopstick.giveupUse(name);
rightChopstick.giveupUse(name);
}
public void think() {
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();;
}
}
}
/**
* 筷子类
*/
public class Chopstick {
// 筷子的状态
private boolean isUsing = false;
// 筷子名
private String name;
public Chopstick(String name){
this.name = name;
}
// 请求使用筷子
public synchronized void requestUse(String usrName) {
try {
if(isUsing){
System.out.println(usrName + "申请" + name + ",被阻塞");
wait();
}
isUsing = true;
System.out.println(usrName + "申请" + name + ",成功!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 放弃筷子
public synchronized void giveupUse(String usrName) {
isUsing = false;
System.out.println(usrName + "吃完,放弃" + name + ",并唤醒所有等待的哲学家");
notifyAll();
}
}
/**
* 可能死锁的情况
*/
public class DeadlockTest {
public static void main(String[] args) {
List<Chopstick> chopsticks = new ArrayList<>();
for (int i = 0; i < 3; i++) {
chopsticks.add(new Chopstick("筷子" + (i + 1)));
}
Philosopher philosopher1 = new Philosopher(chopsticks.get(0), chopsticks.get(1), "哲学家1");
Philosopher philosopher2 = new Philosopher(chopsticks.get(1), chopsticks.get(2), "哲学家2");
Philosopher philosopher3 = new Philosopher(chopsticks.get(2), chopsticks.get(0), "哲学家3");
new Thread(philosopher2).start();
new Thread(philosopher1).start();
new Thread(philosopher3).start();
}
}
四、测试的结果:
没有发生死锁的情况:
哲学家2饥饿
哲学家3饥饿
哲学家3申请筷子3,成功!
哲学家3申请筷子1,成功!
哲学家1饥饿
哲学家1申请筷子1,被阻塞
哲学家2申请筷子2,成功!
哲学家2申请筷子3,被阻塞
哲学家3吃完,放弃筷子3,并唤醒所有等待的哲学家
哲学家3吃完,放弃筷子1,并唤醒所有等待的哲学家
哲学家2申请筷子3,成功!
哲学家1申请筷子1,成功!
哲学家1申请筷子2,被阻塞
哲学家2吃完,放弃筷子2,并唤醒所有等待的哲学家
哲学家2吃完,放弃筷子3,并唤醒所有等待的哲学家
哲学家1申请筷子2,成功!
哲学家1吃完,放弃筷子1,并唤醒所有等待的哲学家
哲学家1吃完,放弃筷子2,并唤醒所有等待的哲学家
发生死锁的情况:
哲学家2饥饿
哲学家3饥饿
哲学家1饥饿
哲学家3申请筷子3,成功!
哲学家2申请筷子2,成功!
哲学家1申请筷子1,成功!
哲学家2申请筷子3,被阻塞
哲学家3申请筷子1,被阻塞
哲学家1申请筷子2,被阻塞
五、解决问题:
破坏必要条件,
思路一:可以在线程被阻塞时释放已经申请到的筷子,这样破坏了请求与保持条件。
思路二:可以改变某个哲学家的申请顺序,破坏循环等待条件。
下面根据思路二,对上面的代码做修改:
/**
* 哲学家类
*/
public class Philosopher implements Runnable{
// 左边的筷子
private Chopstick leftChopstick;
// 右边的筷子
private Chopstick rightChopstick;
// 哲学家名字
private String name;
// 淘气的一个:不按顺序申请
private boolean taoQi;
public Philosopher( Chopstick leftChopstick, Chopstick rightChopstick, String name, boolean taoQi) {
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
this.name = name;
this.taoQi = taoQi;
}
@Override
public void run() {
eat();
think();
}
public void eat(){
System.out.println(name + "饥饿");
if (taoQi) {
System.out.println(name + "很淘气");
rightChopstick.requestUse(name);
leftChopstick.requestUse(name);
} else {
leftChopstick.requestUse(name);
rightChopstick.requestUse(name);
}
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();;
}
leftChopstick.giveupUse(name);
rightChopstick.giveupUse(name);
}
public void think() {
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();;
}
}
}
/**
* 可能死锁的情况
*/
public class DeadlockTest {
public static void main(String[] args) {
List<Chopstick> chopsticks = new ArrayList<>();
for (int i = 0; i < 3; i++) {
chopsticks.add(new Chopstick("筷子" + (i + 1)));
}
int taoQi = (int) (Math.random() * 3 + 1);
Philosopher philosopher1 = new Philosopher(chopsticks.get(0), chopsticks.get(1), "哲学家1", 1 == taoQi);
Philosopher philosopher2 = new Philosopher(chopsticks.get(1), chopsticks.get(2), "哲学家2", 2 == taoQi);
Philosopher philosopher3 = new Philosopher(chopsticks.get(2), chopsticks.get(0), "哲学家3", 3 == taoQi);
new Thread(philosopher2).start();
new Thread(philosopher1).start();
new Thread(philosopher3).start();
}
}
六、修改后的结果
哲学家2饥饿
哲学家2很淘气
哲学家3饥饿
哲学家1饥饿
哲学家3申请筷子3,成功!
哲学家2申请筷子3,被阻塞
哲学家1申请筷子1,成功!
哲学家1申请筷子2,成功!
哲学家3申请筷子1,被阻塞
哲学家1吃完,放弃筷子1,并唤醒所有等待的哲学家
哲学家1吃完,放弃筷子2,并唤醒所有等待的哲学家
哲学家3申请筷子1,成功!
哲学家3吃完,放弃筷子3,并唤醒所有等待的哲学家
哲学家3吃完,放弃筷子1,并唤醒所有等待的哲学家
哲学家2申请筷子3,成功!
哲学家2申请筷子2,成功!
哲学家2吃完,放弃筷子2,并唤醒所有等待的哲学家
哲学家2吃完,放弃筷子3,并唤醒所有等待的哲学家