Picses' sky

Picses' sky
posts - 43, comments - 29, trackbacks - 0, articles - 24

用Java线程获取优异性能(II)2

Posted on 2007-07-18 13:12 Matthew Chen 阅读(210) 评论(0)  编辑  收藏 所属分类: Java MultiThread
Java的同步机制
Java提供一个同步机制以阻止多于一个的线程在时间的任意一点在一个或多个关键代码部份执行代码。这种机制将自己建立在监视器和锁的概念基础上。一个监视器被作为包在关键代码部份周围的保护,一个锁被作为监视器用来防止多重线程进入监视器的一个软件实体。其想法是:当一个线程想进入一个监视器监视着的关键代码部份时,那个线程必须获得一个与监视器相关的对象的锁。(每个对象都有它自己的锁)如果一些其它线程保存着这个锁, JVM会强迫请求线程在一个与监视器/锁有关的等待区域等待。当监视器中的线程释放锁时, JVM从监视器的等待区域中移出等待线程并允许那个线程获得锁且处理监视器的关键代码部份。
要和监视器/锁一起工作, JVM提供了monitorenter和monitorexit 指令。幸运地是,你不需要在如此低级别地工作。取而代之,你能够在synchronized声明和同步方法中使用Java的synchronized关键字。
同步声明
一些关键代码部份占了它们封装方法的一小部份。为了防止多重线程访问这们的关键代码部份,你可使用synchronized声明。这个声明有如下的语法:
'synchronized' '(' objectidentifier ')'
'{'
//关键代码部份
'}'
synchronized声明用关键字synchronized开始及用一个objectidentifier,这出现在一对圆括弧之间。objectidentifier 引用一个与synchronized 声明描述的监视器相关的锁对象。最后,Java声明的关键代码部份出现在一对花括弧之间。你怎样解释synchronized声明呢?看看如下代码片断:
synchronized ("sync object")
{
//访问共享变量及其它共享资源
}
从一个源代码观点看,一个线程企图进入synchronized声明保护的关键代码部份。在内部, JVM 检查是否一些其它线程控制着与"sync object"对象相关的锁。如果没有其它线程控制着锁, JVM将锁给请求线程并允许那个线程进入花括弧之间的关键代码部份。然而,如果有其它线程控制着锁, JVM会强迫请求线程在一个私有等待区域等待直到在关键代码部份内的当前线程完成执行最后声明及经过最后的花括弧。
你能够使用synchronized声明去消除NeedForSynchronizationDemo的竞态条件。如何消除,请看练习列表2:
列表2. SynchronizationDemo1.java
// SynchronizationDemo1.java
class SynchronizationDemo1
{
public static void main (String [] args)
{
FinTrans ft = new FinTrans ();
TransThread tt1 = new TransThread (ft, "Deposit Thread");
TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
tt1.start ();
tt2.start ();
}
}
class FinTrans
{
public static String transName;
public static double amount;
}
class TransThread extends Thread
{
private FinTrans ft;
TransThread (FinTrans ft, String name)
{
super (name); //保存线程的名称 Save thread's name
this.ft = ft; //保存对金融事务对象的引用
}
public void run ()
{
for (int i = 0; i < 100; i++)
{
if (getName ().equals ("Deposit Thread"))
{
synchronized (ft)
{
ft.transName = "Deposit";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
else
{
synchronized (ft)
{
ft.transName = "Withdrawal";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
}
}
}
仔细看看SynchronizationDemo1,run()方法包含两个夹在synchronized (ft) { and }间的关键代码部份。每个存款和取款线程必须在任一线程进入它的关键代码部份前获得与ft引用的FinTrans对象相关的锁。假如如果存款线程在它的关键代码部份且取款线程想进入它自己的关键代码部份,取款线程就应努力获得锁。因为当存款线程在它的关键代码部份执行时控制着锁, JVM 便强迫取款线程等待直到存款线程执行完关键代码部份并释放锁。(当执行离开关键代码部份时,锁自动释放)
技巧:当你需要决定是否一个线程控制与一个给定对象相关的锁时,调用Thread的静态布尔holdsLock(Object o)方法。如果线程调用控制着与对象相关的锁的方法,这个方法便返回一个布尔真值。否则,返回一个假值。例如,如果你打算将System.out.println (Thread.holdsLock (ft))放置在SynchronizationDemo1的main()方法末尾, holdsLock()将返回假值。返回 假值是因为执行main()方法的主线程没有使用同步机制获得任何锁。可是,如果你打算将System.out.println (Thread.holdsLock (ft))放在run()的synchronized (ft)声明中, holdsLock()将返回真值因为无论是存款线程或是取款线程都不得不在那些线程能够进入它的关键代码部份前获得与ft引用的FinTrans对象相关的锁。
Synchronized方法
你能够通过你的程序的源代码使用synchronized声明。然而,你也可能陷入过多使用这样的声明而导致代码效率低。例如,假设你的程序包含一个带两个连续synchronized声明的方法,每一个声明都企图获得同一公共对象的锁。因为获得和翻译对象的锁要消耗时间,重复调用(在一个循环中)那个方法会降低程序的性能。每次对那个方法的一个调用都必须获得和释放两个锁。程序花费大量的时间获得和释放锁。要消除这个问题,你应考虑使用同步方法。
一个同步方法不是一个实例就是一个其头包含synchronized关键字的类方法。例如: synchronized void print (String s)。当你同步一个完整实例方法时,一个线程必须获得与那个方法调用出现的对象相关的锁。例如,给一个ft.update("Deposit", 2000.0)实例方法调用,并且假定update()是同步的,一个方法必须获得与ft引用的对象相关的锁。要看一个SynchronizationDemo1版本的同步方法的源代码,请查看列表3:
列表3. SynchronizationDemo2.java
// SynchronizationDemo2.java
class SynchronizationDemo2
{
public static void main (String [] args)
{
FinTrans ft = new FinTrans ();
TransThread tt1 = new TransThread (ft, "Deposit Thread");
TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
tt1.start ();
tt2.start ();
}
}
class FinTrans
{
private String transName;
private double amount;
synchronized void update (String transName, double amount)
{
this.transName = transName;
this.amount = amount;
System.out.println (this.transName + " " + this.amount);
}
}
class TransThread extends Thread
{
private FinTrans ft;
TransThread (FinTrans ft, String name)
{
super (name); //保存线程名称
this.ft = ft; //保存对金融事务对象的引用
}
public void run ()
{
for (int i = 0; i < 100; i++)
if (getName ().equals ("Deposit Thread"))
ft.update ("Deposit", 2000.0);
else
ft.update ("Withdrawal", 250.0);
}
}
虽然比列表2稍微更简洁,表3达到的是同一目的。如果存款线程调用update()方法, JVM检查看是否取款线程已经获得与ft引用的对象相关的锁。如果是这样,存款线程就等待。否则,那个线程就进入关键代码部份。
SynchronizationDemo2示范了一个同步实例方法。然而,你也能够同步class 方法。例如, java.util.Calendar类声明了一个public static synchronized Locale [] getAvailableLocales() 方法。因为类方法没有一个this引用的概念,那么类方法从哪里获得它的锁呢?类方法从类对象获得它们的锁——每一个与Class对象相关的载入的类,从那些载入的类的类方法得到它们的锁。我称这样的锁为class locks。
一些程序混淆同步实例方法和同步类方法。为帮助你理解在同步类方法调用同步实例方法的程序中到底发生了什么,应在头脑里保持如下两个观点:
1. 对象锁和类锁互相没有关系。它们是不同的实体。你独立地获得和释放每一个锁。一个调用同步类方法的同步实例方法获得两个锁。首先,同步实例方法获得它的对象的对象锁。其次,那个方法获得同步类方法的类锁。
2. 同步类方法能够调用一个对象的同步方法或使用对象去锁住一个同步块。在那种情形下,一个线程最初获得同步类方法的类锁并且接下来获得对象的对象锁。因此,调用同步实例方法的一个同步类方法也获得两个锁。
下面的代码片断描述了这两个观点:
class LockTypes
{
//刚好在执行进入instanceMethod()前获得对象锁
synchronized void instanceMethod ()
{
//当线程离开instanceMethod()时释放对象锁
}
//刚好在执行进入classMethod()前获得类锁
synchronized static void classMethod (LockTypes lt)
{
lt.instanceMethod ();
//刚好在关键代码部份执行前获得对象锁
synchronized (lt)
{
//关键代码部份
//当线程离开关键代码部份时释放对象锁
}
//当线程离开classMethod()时释放类锁
}
}
代码段示范了调用同步实例方法instanceMethod()的同步类方法classMethod()。通过阅读注解,你看到classMethod()首先获得它的类锁接下来获得与lt引用的LockTypes对象相关的对象锁。
警告:不要同步一个线程对象的run()方法因为多线程需要执行run()。因为那些线程企图对同一个对象同步,所以在一个时间里只有一个线程能够执行run()。结果,在每一个线程能访问run()前必须等待前一线程结束。

只有注册用户登录后才能发表评论。


网站导航: