Java 多线程 死锁 Java编程思想读书笔记

来源:互联网 发布:微店 淘宝客 编辑:程序博客网 时间:2024/05/20 06:54

一个对象可以有synchronized方法或其他形式的加锁机制来防止别的任务在互斥还没有释放的时候就访问这个对象,任务就可以变成阻塞状态,所以有可能出现这种情况:某个任务在等待另一个任务,而后者又等待别的任务,这样一直下去,直到这个链条上的任务又在第一个任务释放锁。这就得到了一个任务之间相互等待的连续循环,没有哪个线程能继续。这被称为死锁。

如果你运行一个程序,而它马上就死锁了,你可以立即跟踪下去。真正的问题是,程序可能看起来工作良好,但是具有潜在的死锁危险。这时,死锁可能发生,而事先却没有任何征兆,所以缺陷会潜伏在你的程序里,直到客户发现它出乎意料地发生(以一种很难重现的方式发生)。因此,在编写并发的时候,进行仔细的程序设计以防止死锁是关键部分。

由Edsger Dijkstra提出的哲学家就餐问题是一个经典的死锁例证。假定五个哲学家(可以任意数目)。这些哲学家部分时间思考,部分时间就餐。当一个哲学家就餐时需要两根筷子。但他们只有五根筷子(一般筷子和哲学家数量相同)。他们围坐在桌子周围,每人之间放一根筷子。当一个哲学家要就餐时,他必须同时得到左边和右边的筷子。如果一个哲学家左边或右边已经有人在使用筷子了,那么这个哲学家就必须等待,直到可得到必需的筷子。

定义一个筷子类:

public class Chopstick {      private boolean taken = false;      public synchronized void tack() throws InterruptedException {          while(taken)  <span style="white-space:pre"></span>//当这根<span style="font-family: Arial;">筷子被别人拿着时,只好等待</span>            wait();          taken = true;      }      public synchronized void drop(){          taken = false;          notifyAll();  //当放下筷子时,激活其他人来拿这根筷子    }  } 

任何两个科学家都不可能成功take()同一根筷子Chopstick。另外,如果一根筷子已经被某个科学家获得,另外一个科学家可以wait(),直到这根筷子的当前持有者调用drop()使其可用并notifyAll()为止。

再定义一个科学家类:

import java.util.Random;  import java.util.concurrent.TimeUnit;    public class Philosopher implements Runnable{      private Chopstick left;      private Chopstick right;      private final int id;      private final int ponderFactor;      private Random rand = new Random(47);            //暂停    随机思考或吃饭一段时间      private void pause() throws InterruptedException {          if(ponderFactor == 0)              return ;          TimeUnit.MICROSECONDS.sleep(rand.nextInt(ponderFactor * 250));      }            public Philosopher(Chopstick left,Chopstick right,int ident, int ponder) {          this.left = left;          this.right = right;          id = ident;          ponderFactor = ponder;      }            public void run() {          try{              while(!Thread.interrupted()) {                  System.out.println(this + "  thinking");                  pause();                                    // 科学家饿了,要吃饭了                 System.out.println(this + "  grabbing right");                  right.tack();  //拿起右边的筷子                System.out.println(this + "  grabbing left");                  left.tack();  <span style="font-family: Arial;">//拿起左边的筷子</span>                System.out.println(this + "  eating");                  pause();  //吃饭                right.drop();  //放下右边的筷子                left.drop();   //放下左边的筷子            }          } catch(InterruptedException e) {              System.out.println(this + " exiting via interrupt");          }      }            public String toString(){          return "Philosopher  " + id;      }  } 

在Philosopher.run()中,每个Philosopher只是不断地思考和吃饭。如果ponderFactor不为0,则pause()方法会休眠(sleeps())一段随机时间。通过使用这种方式,可以看到Philosopher会在思考一段随机时间,然后尝试获取(take())右边和左边的Chopstick,随后在吃饭上再花掉一段随机时间,之后重复此过程。


现在我们可以建立这个程序的将会产生死锁的版本了:

import java.io.IOException;  import java.util.concurrent.*;    public class DeadlockingDiningPhilosophers {      public static void main(String[] args) throws IOException {          int ponder = 5;  //5根筷子        int size = 5;  //5个科学家                  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++) {              exec.execute(new Philosopher(sticks[i], sticks[(i+1)%size], i, ponder));          }                    System.out.println("Press 'Enter' to quit");          System.in.read(); //提供后台输入中止程序         exec.shutdownNow();      }  }

可以看到不用多久,程序就出现死锁。

注意,如果Philosopher花费更多的时间去思考而不是就餐(使用非0的ponder值,或者大量的Philosopher),那么他们请求共享资源(Chopstick)的可能性会小很多。这样也许你会确信程序不会死锁,但是它们并非如此。它起来正确运行,但实际上会死锁。

当以下四个条件同时满足时,就会发生死锁:

1.互斥条件。任务使用的资源中至少有一个是不能共享的。这时,一根筷子一次只能被一个科学家使用。

2.至少有一个任务它必须持有一个资源且正在等待获取一个当前被别的任务持有的资源。也就是说,科学家必须拿着一根筷子并且等待另一根。

3.资源不能被任务抢占,任务必须把资源释放当作普通事件。科学家很有礼貌,他们不会从其他科学家那里抢筷子。

4.必须有循环等待。这里,一个任务等待其他任务所持有的资源,后者又在等待另一个任务所持有的资源,这样一直下去,直到有一个任务在等待第一个任务所持有的资源,使得大家都被锁住。在DeadlockingDiningPhilosophers.java中,因为每个Philosopher都试图先得到右边的Chopstick,然后得到左边的Chopstick,所以发生了循环等待。

要防止死锁,只需破坏其中一个条件即可。在程序中,防止死锁最容易的方法就是破坏第4个条件。程序满足这个条件的原因是每个Philosopher都试图用特定的顺序拿筷子:先右后左。正因为如此,就可能发生“每个人都拿着右边的Chopstick,并等待左边的Chopstick”的情况。然而,如果最后一个Philosopher被初始化为先拿左边的Chopstick,再拿右边的Chopstick,那么这个Philosopher就永远不会阻止其右边的Chopstick,这可以防止循环等待。这是解决死锁的方法之一,也可以通过破坏其他条件来防止死锁(具体细节请参考更高级的讨论线程的书籍):

import java.io.IOException;  import java.util.concurrent.*;    public class FixedDiningPhilosophers {            public static void main(String[] args) throws IOException {          int ponder = 5;          int size = 5;          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)); //最后一个科学家先拿左边的筷子,再拿右边的筷子         }            System.out.println("Press 'Enter' to quit");          System.in.read();          exec.shutdownNow();      }
通过确保最后一个Philosopher先拿起和放下左边的Chopstick,我们可以移除死锁,从而使这个程序平滑地运行。

Java对死锁并没有提供语言层面上的支持;能否通过仔细地设计程序来避免死锁,这取决于你自己。


0 0