225.死锁(进餐哲学家)

什么是死锁?网络上的一个著名笑话解释如下:

面试者:向我们解释死锁,我们会雇用你!

求职者:雇用我,我会向你解释......

一个简单的死锁可以解释为一个 A 线程持有 L 锁并试图获取 P 锁,同时,有一个 B 线程持有 P 锁并试图获取 L 锁。这种死锁被称为循环等待。Java 没有死锁检测和解决机制(如数据库那样),因此死锁对于应用程序来说可能非常尴尬。死锁可能完全或部分阻塞应用程序,可能导致严重的性能损失、奇怪的行为等。通常,死锁很难调试,解决死锁的唯一方法是重新启动应用程序并期待最好的结果。

哲学家进餐是一个用来说明死锁的著名问题。这个问题说五个哲学家围坐在一张桌子旁。他们每个人都交替思考和进食。为了吃饭,哲学家需要手里拿着两把叉子——左手的叉子和右手的叉子。困难在于只有五个叉子。吃完后,哲学家将两个叉子放回桌子上,然后可以由重复相同循环的另一个哲学家拿起。当哲学家 不吃饭时,他/她在思考。下图说明了这种情况:

 

主要任务是找到解决这个问题的方法,让哲学家们以这样的方式思考和进食,以避免被饿死。

在代码中,我们可以将每个哲学家视为一个Runnable实例。作为Runnable 实例,我们可以在单独的线程中执行它们。每个哲学家可以拿起放在他左右两侧的两个叉子。如果我们将叉表示为String,那么我们可以使用以下代码:

public class Philosopher implements Runnable {

  private final String leftFork;
  private final String rightFork;

  public Philosopher(String leftFork, String rightFork) {
    this.leftFork = leftFork;
    this.rightFork = rightFork;
  }

  @Override
  public void run() {
    // implemented below
  }
}

因此,哲学家可以选择 leftFork和rightFork。但是由于哲学家共享这些叉子,因此哲学家必须获得这两个叉子的排他锁。拥有一个排他锁 leftFork和一个排他锁rightFork, 就相当于手里拿着两把叉子。拥有独占的leftFork锁r和rightFork锁r等同于哲学家的饮食。释放两个排他锁,相当于哲学家不吃饭进行思考。

可以通过synchronized关键字实现锁定,如下run()方法所示:

@Override
public void run() {

  while (true) {
    logger.info(() -> Thread.currentThread().getName() 
      + ": thinking");
    doIt();

    synchronized(leftFork) {
      logger.info(() -> Thread.currentThread().getName() 
        + ": took the left fork (" + leftFork + ")");
      doIt();

      synchronized(rightFork) {
        logger.info(() -> Thread.currentThread().getName() 
          + ": took the right fork (" + rightFork + ") and eating");
        doIt();

        logger.info(() -> Thread.currentThread().getName() 
          + ": put the right fork ( " + rightFork 
          + ") on the table");
        doIt();
      }

      logger.info(() -> Thread.currentThread().getName() 
        + ": put the left fork (" + leftFork 
        + ") on the table and thinking");
      doIt();
    }
  }
}

哲学家从思考开始。过了一会儿,他饿了,所以他试图拿起左右叉。如果成功,他会吃一段时间。随后,他将叉子放在桌上,继续思考,直到又饿了。与此同时,另一位哲学家将吃饭。

该doIt()方法通过随机睡眠模拟所涉及的动作(思考、进食、拿起和放回叉子)。这可以在代码中看到如下:

private static void doIt() {
  try {
    Thread.sleep(rnd.nextInt(2000));
  } catch (InterruptedException ex) {
    Thread.currentThread().interrupt();
    logger.severe(() -> "Exception: " + ex);
  }
}

最后,我们需要叉子和哲学家,看下面的代码:

String[] forks = {
  "Fork-1", "Fork-2", "Fork-3", "Fork-4", "Fork-5"
};

Philosopher[] philosophers = {
  new Philosopher(forks[0], forks[1]),
  new Philosopher(forks[1], forks[2]),
  new Philosopher(forks[2], forks[3]),
  new Philosopher(forks[3], forks[4]),
  new Philosopher(forks[4], forks[0])
};

每个哲学家都会在一个线程中运行,如下所示:

Thread threadPhilosopher1 
  = new Thread(philosophers[0], "Philosopher-1");
...
Thread threadPhilosopher5 
  = new Thread(philosophers[4], "Philosopher-5");

threadPhilosopher1.start();
...
threadPhilosopher5.start();

这个实现似乎没问题,甚至可以正常工作一段时间。但是,这个实现迟早会阻塞,输出如下:

[17:29:21] [INFO] Philosopher-5: took the left fork (Fork-5)
...
// nothing happens

这是一个死锁!每个哲学家都拿着他的左叉子(上面有排他锁)并等待右叉子放在桌子上(锁要被释放)。显然,这种期望是无法满足的,因为叉子只有五个,每个哲学家手里都拿着一个。

为了避免这种死锁,有一个非常简单的解决方案。我们只是强迫其中一位哲学家先拿起右边的叉子。成功选择右边的叉子后,他可以尝试选择左边的叉子。在代码中,这是对以下行的快速修改:

// the original line
new Philosopher(forks[4], forks[0])

// the modified line that eliminates the deadlock
new Philosopher(forks[0], forks[4])

这一次我们可以无死锁地运行应用程序。

package modern.challenge;

public class Main {

    public static void main(String[] args) {

        System.setProperty("java.util.logging.SimpleFormatter.format",
                "[%1$tT] [%4$-7s] %5$s %n");

        String[] forks = {"Fork-1", "Fork-2", "Fork-3", "Fork-4", "Fork-5"};
        Philosopher[] philosophers = {
            new Philosopher(forks[0], forks[1]),
            new Philosopher(forks[1], forks[2]),
            new Philosopher(forks[2], forks[3]),
            new Philosopher(forks[3], forks[4]),
            new Philosopher(forks[0], forks[4])
        };

        Thread threadPhilosopher1 = new Thread(philosophers[0], "Philosopher-1");
        Thread threadPhilosopher2 = new Thread(philosophers[1], "Philosopher-2");
        Thread threadPhilosopher3 = new Thread(philosophers[2], "Philosopher-3");
        Thread threadPhilosopher4 = new Thread(philosophers[3], "Philosopher-4");
        Thread threadPhilosopher5 = new Thread(philosophers[4], "Philosopher-5");

        threadPhilosopher1.start();
        threadPhilosopher2.start();
        threadPhilosopher3.start();
        threadPhilosopher4.start();
        threadPhilosopher5.start();
    }

}
package modern.challenge;

import java.util.Random;
import java.util.logging.Logger;

public class Philosopher implements Runnable {

    private static final Logger logger = Logger.getLogger(Philosopher.class.getName());
    private static final Random rnd = new Random();

    private final String leftFork;
    private final String rightFork;

    public Philosopher(String leftFork, String rightFork) {
        this.leftFork = leftFork;
        this.rightFork = rightFork;
    }

    @Override
    public void run() {

        while (true) {
            logger.info(() -> Thread.currentThread().getName() + ": thinking");
            doIt();
            synchronized (leftFork) {
                logger.info(() -> Thread.currentThread().getName()
                        + ": took the left fork (" + leftFork + ")");
                doIt();
                synchronized (rightFork) {
                    logger.info(() -> Thread.currentThread().getName()
                            + ": took the right fork (" + rightFork + ") and eating");
                    doIt();
                    logger.info(() -> Thread.currentThread().getName()
                            + ": put the right fork ( " + rightFork + ") on the table");
                    doIt();
                }
                logger.info(() -> Thread.currentThread().getName()
                        + ": put the left fork (" + leftFork + ") on the table and thinking");
                doIt();
            }
        }
    }

    private static void doIt() {
        try {
            Thread.sleep(rnd.nextInt(2000));
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            logger.severe(() -> "Exception: " + ex);
        }
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值