问题描述
五位哲学家围绕一个圆桌就坐,桌上摆着五支筷子。哲学家的状态可能是“思考”或者“饥饿”。如果“饥饿”。哲学家将拿起他两边的筷子并进行进餐一段时间,进餐结束放回筷子。
错误解法造成死锁
代码如下:
class Philosopher extends Thread {
private Chopstick left,right;
private Random random;
public Philosopher(Chopstick left,Chopstick right) {
this.left = left;
this.right = right;
random = new Random();
}
public void run() {
try {
while(true) {
Thread.sleep(random.nextInt(1000)); //思考一段时间
synchronized(left) { //拿起左手边筷子
synchronized(right) { //拿起右手边筷子
Thread.sleep(random.nextInt(1000));//进餐一段时间
}
}
}
}catch (InterruptedException e) {
}
}
}
//主函数略
错误解法的思路是,首先获取左手边的筷子,锁定左手边筷子后再拿起右手边筷子,两边筷子同时锁定后开始进餐。很明显,当所有哲学家同时决定进餐时,都拿起了左手边的筷子,那么就无法进行下去,所有人都持有一只筷子并等待右手边的人放下筷子,这时产生死锁问题。
锁全局排序正确解法
代码如下:
class Philosopher extends Thread {
private Chopstick first,second;
private Random random;
public Philosopher(Chopstick left,Chopstick right) {
if(left.getId() < right.getId()){
first = left;
second = right;
}else {
first = right;
second = left;
}
random = new Random();
}
public void run() {
try {
while(true) {
Thread.sleep(random.nextInt(1000)); //思考一段时间
synchronized(first) { //拿起左手边筷子
synchronized(second) { //拿起右手边筷子
Thread.sleep(random.nextInt(1000));//进餐一段时间
}
}
}
}catch (InterruptedException e) {
}
}
}
//主函数略
该正确解法的思路是,将五只筷子抽象为5把锁,进行全局排序,每次哲学家首先拿起小序号的锁,锁定后拿起大序号的锁进行进餐,这样就不会产生死锁。
该方法适合获取锁的代码写的比较集中的情况,有利于维护这个全局顺序;若规模较大的程序,使用锁的地方比较零散,各处都遵守这个顺序就变得不太实际。
使用超时锁的正确解法
代码如下:
class Philosopher extends Thread {
private ReentrantLock leftChopstick,rightChoppstick;
private Random random;
public Philosopher(ReentrantLock leftChopstick,ReentrantLock rightChoppstick) {
this.leftChopstick = leftChopstick;
this.rightChoppstick = rightChoppstick;
random = new Random();
}
public void run() {
try {
while(true) {
Thread.sleep(random.nextInt(1000));
leftChopstick.lock();
try {
if(rightChoppstick.tryLock(1000,TimeUnit.MILLISECONDS)) {
try {
Thread.sleep(random.nextInt(1000));
} finally{
rightChoppstick.unlock();
}
}else {
// 没有获取到右手边的筷子,放弃并继续思考
}
}finally {
leftChopstick.unlock()
}
}
}catch(InterruptedException e) {
}
}
}
//主函数略
该正确解法的思路是,哲学家首先拿起左手边筷子上锁,然后尝试拿起右手边的筷子,若超时没有拿到右手边筷子,则放弃进餐,释放左手筷子。
该方法避免了无尽地死锁,但也不是很好的方案,因为该方案并不能避免死锁,它只是提供了从死锁中恢复的手段,并且受到活锁现象的影响,如果所有死锁线程同时超时,它们极有可能再次陷入死锁,虽然死锁没有永远持续下去,但对资源的争夺状态却没有得到任何改善(为每个线程设置不同的超时时间可以稍好的处理这种情况)。
使用条件变量的正确解法
代码如下:
class Philosopher extends Thread {
private boolean eating;
private Philosopher left;
private Philosopher right;
private ReentrantLock table;
private Condition condition;
private Random random;
public Philosopher(ReentrantLock table) {
eating = false;
this.table = table;
condition = table.newCondition();
random = new Random();
}
public void setLeft(Philosopher left) {
this.left = left;
}
public void setRight(Philosopher right) {
this.right = right;
}
public void run() {
try {
while(true) {
think();
eat();
}
}catch(InterruptedException e) {
}
}
public void think() throws InterruptedException {
table.lock();
try {
eating = false;
left.condition.signal();
right.condition.signal();
}finally {
table.unlock();
}
Thread.sleep(1000);
}
public void eat() throws InterruptedException {
table.lock();
try {
while(left.eating || right.eating)
condition.await();
eating = true;
}finally{
table.unlock();
}
Thread.sleep(1000);
}
}
//主函数略
该正确解法的思路是只使用一把锁,将竞争从对筷子的争夺转换成了对状态的判断,仅当哲学家的左右邻座都没有进餐时,才可以进餐。当一个哲学家饥饿时,首先锁住餐桌,这样其他哲学家无法改变状态,然后查看左右邻居是否正在进餐,如果没有,那么该哲学家开始进餐并解锁餐桌,否则调用await()以解锁餐桌;当一个哲学家进餐结束并开始思考时,首先锁住餐桌将eating改为false,然后通知左右邻座可以进餐,最后解锁餐桌。如果左右邻居目前正在等待,那么他们将被唤醒,重新锁住餐桌,并判断是否开始进餐。
在这个解决方法中,当一个哲学家理论上可以进餐时,肯定就可以进餐,并发度显著提升。
总结
通过这一经典的问题,学习多线程并发模型的三种解决方案:
-
多把锁时,对锁设置全局唯一的顺序,按序使用锁;
-
设置线程获取锁的超时时间,防止无限制的死锁;
-
使用条件变量
ReentrantLock lock = new ReentrantLock();
Condition Condition = lock.newCondition();
lock.lock();
try {
while(!《条件为真》)
condition.await();
《使用共享资源》
} finally {
lock.unlock();
}
一个条件变量需要与一把锁关联,线程在开始等待条件之前必须获取这把锁,获取锁后,线程检查所等待的条件是否已经为真,如果为真,线程将继续执行并解锁。条件变量的方法针对哲学家的问题,会使并发度显著提升。