Posted on 2007-07-18 13:13
Matthew Chen 阅读(202)
评论(0) 编辑 收藏 所属分类:
Java MultiThread
尽管其简单,开发者经常滥用Java的同步机制会导致程序由不同步变得死锁。这章将检查这些问题并提供一对避免它们的建议。 |
注意:一个与同步机制有关的线程问题是与锁的获得和释放有关的时间成本。换句话说,一个线程将花费时间去获得或释放一个锁。当在一个循环中获得/释放一个锁,单独的时间成本合计起来就会降低性能。对于旧的JVMs,锁的获得时间成本经常导致重大的性能损失。幸运地是, Sun微系统的HotSpot JVM (其装载在J2SE SDK上)提供快速的锁的获得和释放,大大减少了对这些程序的影响。 |
在一个线程自动或不自动(通过一个例外)退出一个关键代码部份时,它释放一个锁以便另一个线程能够得以进入。假设两个线程想进入同一个关键代码部份,为了阻止两个线程同时进入那个关键代码部份,每个线程必须努力获得同一个锁。如果每一个线程企图获得一个不同的锁并成功了,两个线程都进入了关键代码部份,则两个线程都不得不等待其它线程释放它的锁因为其它线程获得了一个不同的锁。最终结果是:没有同步。示范如列表4: |
列表4. NoSynchronizationDemo.java |
// NoSynchronizationDemo.java |
class NoSynchronizationDemo |
public static void main (String [] args) |
FinTrans ft = new FinTrans (); |
TransThread tt1 = new TransThread (ft, "Deposit Thread"); |
TransThread tt2 = new TransThread (ft, "Withdrawal Thread"); |
public static String transName; |
public static double amount; |
class TransThread extends Thread |
TransThread (FinTrans ft, String name) |
this.ft = ft; //保存对金融事务对象的引用 |
for (int i = 0; i < 100; i++) |
if (getName ().equals ("Deposit Thread")) |
ft.transName = "Deposit"; |
Thread.sleep ((int) (Math.random () * 1000)); |
catch (InterruptedException e) |
System.out.println (ft.transName + " " + ft.amount); |
ft.transName = "Withdrawal"; |
Thread.sleep ((int) (Math.random () * 1000)); |
catch (InterruptedException e) |
System.out.println (ft.transName + " " + ft.amount); |
当你运行NoSynchronizationDemo时,你将看到类似如下的输出: |
尽管使用了synchronized声明,但没有同步发生。为什么?检查synchronized (this)。因为关键字this指向当前对象,存款线程企图获得与初始化分配给tt1的TransThread对象引用有关的锁。 (在main()方法中)。类似的,取款线程企图获得与初始化分配给tt2的TransThread对象引用有关的锁。我们有两个不同的TransThread对象,并且每一个线程企图在进入它自己关键代码部份前获得与其各自TransThread对象相关的锁。因为线程获得不同的锁,两个线程都能在同一时间进入它们自己的关键代码部份。结果是没有同步。 |
技巧:为了避免一个没有同步的情形,选择一个对于所有相关线程都公有的对象。那样的话,这些线程竞相获得同一个对象的锁,并且同一时间仅有一个线程在能够进入相关的关键代码部份。 |
在有些程序中,下面的情形可能出现:在线程B能够进入B的关键代码部份前线程A获得一个线程B需要的锁。类似的,在线程A能够进入A的关键代码部份前线程B获得一个线程A需要的锁。因为两个线程都没有拥有它自己需要的锁,每个线程都必须等待获得它的锁。此外,因为没有线程能够执行,没有线程能够释放其它线程的锁,并且程序执行被冻结。这种行为叫作死锁(deadlock)。其示范列如表5: |
public static void main (String [] args) |
FinTrans ft = new FinTrans (); |
TransThread tt1 = new TransThread (ft, "Deposit Thread"); |
TransThread tt2 = new TransThread (ft, "Withdrawal Thread"); |
public static String transName; |
public static double amount; |
class TransThread extends Thread |
private static String anotherSharedLock = ""; |
TransThread (FinTrans ft, String name) |
this.ft = ft; //保存对金融事务对象的引用 |
for (int i = 0; i < 100; i++) |
if (getName ().equals ("Deposit Thread")) |
synchronized (anotherSharedLock) |
ft.transName = "Deposit"; |
Thread.sleep ((int) (Math.random () * 1000)); |
catch (InterruptedException e) |
System.out.println (ft.transName + " " + ft.amount); |
synchronized (anotherSharedLock) |
ft.transName = "Withdrawal"; |
Thread.sleep ((int) (Math.random () * 1000)); |
catch (InterruptedException e) |
System.out.println (ft.transName + " " + ft.amount); |
如果你运行DeadlockDemo,你将可能看到在应用程序冻结前仅一个单独输出行。要解冻DeadlockDemo,按Ctrl-C (假如你正在一个Windows命令提示符中使用Sun的SDK1.4)。 |
什么将引起死锁呢?仔细查看源代码。存款线程必须在它能够进入其内部关键代码部份前获得两个锁。与ft引用的FinTrans对象有关的外部锁和与anotherSharedLock引用的String对象有关的内部锁。类似的,取款线程必须在其能够进入它自己的内部关键代码部份前获得两个锁。与anotherSharedLock引用的String对象有关的外部锁和与ft引用的FinTrans对象有关的内部锁。假定两个线程的执行命令是每个线程获得它的外部锁。因此,存款线程获得它的FinTrans锁,以及取款线程获得它的String锁。现在两个线程都执行它们的外部锁,它们处在它们相应的外部关键代码部份。两个线程接下来企图获得内部锁,因此它们能够进入相应的内部关键代码部份。 |
存款线程企图获得与anotherSharedLock引用对象相关的锁。然而,因为取款线程控制着锁所以存款线程必须等待。类似的,取款线程企图获得与ft引用对象相关的锁。但是取款线程不能获得那个锁因为存款线程(它正在等待)控制着它。因此,取款线程也必须等待。两个线程都不能操作因为两个线程都不能释放它控制着的锁。两个线程不能释放它控制着的锁是因为每个线程都正在等待。每个线程都死锁,并且程序冻结。 |
技巧:为了避免死锁,仔细分析你的源代码看看当一个同步方法调用其它同步方法时什么地方可能出现线程互相企图获得彼此的锁。你必须这样做因为JVM不能探测并防止死锁。 |
为了使用线程达到优异性能,你将遇到你的多线程程序需要连载访问关键代码部份的情形。同步可以有效地阻止在奇怪程序行为中产生的不一致。你能够使用synchronized声明以保护一个方法的部份,或同步整个方法。但应仔细检查你的代码以防止可能造成同步失败或死锁的故障。 |