一个对象能够有 synchronized 方法或其余形式的加锁机制来防止别的任务在互斥尚未释放的时候就访问这个对象。java
任务有可能变成阻塞状态,因此就可能发生这样的状况:某个任务在等待另外一个任务,然后者又在等待别的任务,这样一直下去,直到这个链条上的任务又在等待第一个任务释放锁。这就造成了一个相互等待的循环,没有那个线程可以继续。这被称之为死锁。编程
咱们真正须要解决的问题是程序看起来可能工做良好,可是具备潜在的死锁风险。这时,死锁可能发生,而事先却没有任何征兆,因此缺陷会潜伏在你的程序里,直到被人之外的发现了。所以,在编写并发编程的时候,进行仔细的程序设计以防止死锁是很是关键的。bash
下面引入一个问题,一共有 5 个哲学家。这些哲学家将花部分时间思考,花部分时间就餐。做为哲学家他们很穷,因此他们只能买 5 根筷子。他们围坐在桌子的周围,每人之间放一根筷子。当一个哲学家要就餐的时候,这个哲学家必须同时获得左边和右边的筷子。若是一个哲学家左边或者右边已经获得筷子,那么这个哲学家就必须等待,直至可获得必须的筷子。并发
public class Chopstick {
private boolean taken = false;
public synchronized void take() throws InterruptedException{
while (taken) {
wait();
}
taken = true;
}
public synchronized void drop() {
taken = false;
notifyAll();
}
}
复制代码
任何两个哲学家都不能使用同一根筷子。也就是不能同时 taken() 同一个筷子。另外若是一个 Chopstick 被一个哲学家得到,那么另外一个哲学家能够 wait(),直到当前的这根筷子的持有者调用 drop() 结束使用。dom
public class Philosopher implements Runnable{
private Chopstick left;
private Chopstick right;
private final int id;
private final int ponderFactor;
private Random random = new Random(47);
public void pause() throws InterruptedException{
if (ponderFactor ==0) {
return;
}
TimeUnit.MICROSECONDS.sleep(random.nextInt(ponderFactor * 250));
}
protected Philosopher(Chopstick left, Chopstick right, int id, int ponderFactor) {
super();
this.left = left;
this.right = right;
this.id = id;
this.ponderFactor = ponderFactor;
}
@Override
public void run() {
// TODO Auto-generated method stub
try {
while (!Thread.interrupted()) {
System.out.println(this+"开始思考");
pause();
System.out.println(this+"开始拿左边的筷子");
left.take();
System.out.println(this+"开始拿右边的筷子");
right.take();
System.out.println(this+"开始就餐");
pause();
left.drop();
right.drop();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
System.out.println("当前线程被中断了");
}
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "哲学家的编号:"+id;
}
}
复制代码
在哲学家的任务中,每一个哲学家都是不断的思考和吃饭。若是 ponderFactor 不为 0,则 pause() 就会休眠一会。经过这样的方法你会看到哲学家会思考一段时间。而后尝试着去获取左边和右边的筷子,随后再在吃饭上花掉一段随机的时间,以后重复此过程。ide
如今咱们来创建这个程序的死锁版本:ui
public class DeadLockingDiningPhilosophers {
public static void main(String[] args) {
// TODO Auto-generated method stub
int ponder = 5;
int size = 5;
ExecutorService service = Executors.newCachedThreadPool();
Chopstick[] sChopsticks = new Chopstick[size];
for (int i = 0; i < size; i++) {
sChopsticks[i] = new Chopstick();
}
for (int i = 0; i < size; i++) {
//每个哲学家都会持有他左边和右边的筷子对象
service.execute(new Philosopher(sChopsticks[i],sChopsticks[(i+1)%size],i,ponder));
}
System.out.println("执行结束");
service.shutdownNow();
}
}
复制代码
执行的结果:this
执行结束
哲学家的编号:2开始思考
哲学家的编号:4开始思考
哲学家的编号:1开始思考
哲学家的编号:0开始思考
哲学家的编号:3开始思考
当前线程被中断了
当前线程被中断了
当前线程被中断了
当前线程被中断了
当前线程被中断了
复制代码
这个程序表示每个哲学家都有可能要表示进餐,从而等待其临近的 Philosopher 放下他们的 Chopstick。这将会使得程序死锁。spa
要修正死锁必须明白,当如下四个条件同时知足时,就会发生死锁:线程
由于要发生死锁全部这些条件必须知足;因此要防止死锁的话只须要破坏其中一个就能够。在程序中防止死锁的最容易的办法就是破坏第四个循环条件。
public class FixedDiningPhilosophers {
public static void main(String[] args) throws Exception {
int ponder = 5;
if(args.length > 0)
ponder = Integer.parseInt(args[0]);
int size = 5;
if(args.length > 1)
size = Integer.parseInt(args[1]);
ExecutorService exec = Executors.newCachedThreadPool();
Chopstick[] sticks = new Chopstick[size];
for(int i = 0; i < size; i++)
sticks[i] = new Chopstick();
for(int i = 0; i < size; i++)
if(i < (size-1))
exec.execute(new Philosopher(sticks[i], sticks[i+1], i, ponder));
else
exec.execute(new Philosopher(sticks[0], sticks[i], i, ponder));
if(args.length == 3 && args[2].equals("timeout"))
TimeUnit.SECONDS.sleep(5);
else {
System.out.println("Press 'Enter' to quit");
System.in.read();
}
exec.shutdownNow();
}
}
复制代码
执行结果:
哲学家的编号:2开始拿左边的筷子
哲学家的编号:4开始思考
哲学家的编号:1开始思考
哲学家的编号:0开始拿左边的筷子
哲学家的编号:0开始拿右边的筷子
哲学家的编号:0开始就餐
哲学家的编号:3开始就餐
哲学家的编号:2开始拿右边的筷子
/.....
复制代码
经过确保最后一个哲学家先拿起和放下左边的筷子,咱们能够移除死锁,从而使得程序运行。Java 并无对死锁提供类库上的支持;可否经过仔细的程序设计避免死锁须要咱们本身努力。
扫码关注公众号便可当即获取全套的 Java 编程思想读书笔记。