一、对可共享数据的同步访问
synchronized关键字可以保证在同一时刻,只有一个线程在执行一条语句,或者一段代码块。正确地使用同步可以保证其他任何方法都不会看到对象处于不一致的状态中,还能保证通过一系列看似顺序执行的状态转变序列,对象从一种一致的状态变迁到另一种一致的状态。
迟缓初始化(lazy initialization)的双重检查模式(double-check idiom):
//The double-check idiom for lazy initializaation -broken!
private static Foo foo = null;
public statc Foo getFoo(){
if(foo == null){
synchronized(Foo.class){
foo = new Foo();
}
}
return foo;
}
该模式的背后思想是,在域(foo)被初始化之后,再要访问该域时无需同步,从而避免多数情况下的同步开销。同步只是被用来避免多个线程对该域做初始化。问题出在:在缺少同步的情况下, 读入一个已“发布”的对象引用并不保证:一个线程会看到在对象引用发布之前所有保持在内存中的数据。一般情况下, 双重检查模式并不正确的工作。但是如果被共享的变量包含一个原语值,而不是一个对象引用,则它可以正确的工作。
按需初始化容器类(initialize-on-demand holder class)模式:
// The initialize-on-demand holder class idiom
private static class FooHolder{
static final Foo foo = new Foo();
}
public static Foo getFoo(){
return FooHolder.foo;
}
该模式充分利用了Java语言中“只有当一个类被用到的时候它才被初始化”。该模式的优美之处在于,getFoo方法并没有被同步,它只执行一次域访问,所以迟缓初始化并没有引入实际的访问开销。这种模式的缺点在于,它不能用于实例域,只能用于静态域。
简而言之,无论何时当多个线程共享可变数据的时候,每个读或者写数据的线程必须获得一把锁。
二、避免过多的同步
为了避免死锁的危险,在一个被同步的方法或者代码块中,永远不要放弃对客户的控制。换句话说,在一个被同的区域内部,不要调用一个可被改写的公有或受保护的方法(这样的方法是往往是抽象的,但偶尔它们也会有一个具体的默认实现).例如,在该方法中创建另一个线程,再回调到这个类中,然后,新建的线程试图获取原线程所拥有的那把锁,这样就导致新建线程被阻塞。如果方法还在等待这个线程完成任务,则死锁就形成了。
通常,在同步区域内你应该做尽可能少的工作。获得锁,检查共享数据,根据需要转换数据,然后放掉锁。如果你必须要执行某些很耗时的动作,则应该设法把这个动作移到同步区域的外面。
为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。
三、永远不要在循环的外面调用wait
Object.wait方法的作用是使一个线程等待某个条件,它一定是在一个同步区域中被调用的,而且该同步区域锁住了被调用的对象。下面是使用wait方法的标准模式:
synchronized(obj){
while(<condition does not hold>)
obj.wait();
//preform action appropriate to condition
}
总是使用wait循环模式来调用wait方法。永远不要在循环的外面调用wait。循环被用于在等待的前后测试条件。
作为一种优化,如果处于等待状态的所有线程都在等待同一个条件,而每次只有一个线程可以从这个条件中被唤醒,那么你可以选择调用notify而不是notifyAll。如果只有一个线程在一个特定的对象上等待,那么这两个条件很容易被满足。
关于使用notifyAll优先于notify的建议有一个告诫:虽然使用notifyAll不会影响正确性,但是会影响性能。当"特定状态"到来时,你唤醒每一个线程,那么你每次唤醒一个线程,总共n次唤醒。所以,选择的唤醒策略是非常重要的。
如果竞争某个特定状态的所有线程在逻辑上是等价的,那么,你必须谨慎地使用notify,而不是notifyAll。
四、不要依赖于线程调度器
当有多个线程可以运行时,线程调度器(thread scheduler)决定哪个线程将会运行,以及运行多长时间。任何一个合理的JVM实现在作出这样的决定的时候,都努力做到某种公正性,但是对于不同的JVM实现,其策略大相径庭。任何依赖于线程调度器而达到正确性或性能要求的程序,很可能是不可移植的。
编写健壮的、响应良好的、可移植的多线程应用程序的最好办法是,尽可能确保在任何给定时刻只有少量的可运行线程。这使得线程调度器没有更多的选择:它只需运行这些可运行的线程,直到它们不再可运行为止。保持可运行线程数量尽可能少的主要方法是,让每个线程做少量的工作,然后使用Object.wait等待某个条件发生,或者使用Thread.sleep睡眠一段时间。
如果一个程序因为某些线程无法像其他的线程那样获得足够的CPU时间,而不能工作,那么,不要企图通过Thread.yield来“修正”该程序。你可能会成功地让程序工作,但是从性能的角度来看,这样得到的程序是不可能移植的。
调整线程优先级(thread priorities),也可以算是一条建议。线程优先级是Java平台上最不可移植的特征了。
对于大多数程序员来说,Thread.yield的惟一用途是在测试期间人为地增加一个程序的并发性。通过探查一个程序更大部分的状态空间,可以发现一些隐藏错误(bug),从而对系统的正确性增强信心。这项技术已经被证明对于找出“微妙的并发性错误"非常有效。
五、线程安全性的文档化
在一个方法的声明中出现synchronized修饰符,这是一个实现细节,并不是导出的API的一部分。出现了synchronized修饰符并不一定表明这个方法是线程安全的,它有可能随着版本的不同而发生变化。
下面列表概括了一个类可能支持的线程安全性级别,虽然还没有被广泛接受:
非可变的(immutable)--这个类的实例对于其客户而言是不变的。所以,不需要外部的同步。这样的例子包括String、Integer和BigInteger。
线程安全的(thread-safe)--这个类的实例是可变的。但是所有的方法都包含足够的同步手段,所以,这些实例可以被并发使用,无需外部同步。例如,Random和java.util.Timer。
有条件的线程安全(conditionally thread-safe)--这个类(或者关联的类)包含有某些方法,它们必须被顺序调用,而不能收到其他线程的干扰,除此之外,这种线程安全级别与上一种情形(线程安全)相同。
线程兼容的(thread-compatiable)--在每个方法调用的外围使用同步,此时,这个类的实例可以被安全地并发使用。如ArrayList和HashMap。
线程对立的(thread-hostile)--这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外部同步包围。根源在于:这个类的方法要修改静态数据,而这些静态数据可能会影响其他的线程。幸运的是,在Java平台库中,线程对立的类或者方法非常少。System.runFinalizersOnExit方法是线程对立的,但是已经被废弃了。
六、避免使用线程组
线程组(thread group)的初衷是作为一个隔离(applet(小程序)的机制,当然是出于安全的考虑,它们并没有真正实现这个承诺,它们的安全重要性已经差到在Java 2平台安全模型的核心工作中不被提及的地步。