NO.48 对共享可变数据的同步访问
同步,不仅可以阻止一个线程看到对象处于不一致的状态中,它还可以保证通过一系列看似顺序执行的状态转变序列,对象从一种一致的状态变迁到另一种一致的状态。
synchronized关键字可以保证在同一时刻,只有一个线程在执行一条语句,或者一段代码块。java语言保证读或写一个变量是原子的,除非这个变量的类型是long或double.
java的内存模型决定,为了在线程之间可靠地通信,以及为了互斥访问,对原子数据的读写进行同步是需要的。看一个可怕的例子:
- //Broken - require synchronization!
- private static int nextSerialNumber=0;
- public static int generateSerialNumber(){
- return nextSerialNumber++;
- }
对其改进,只需要在generateSerialNumber的声明中增加synchronized修饰符即可。
为了终止一个线程,一种推荐的做法是让线程轮询某个域,该域的值如果发生变化,就表明此线程就应该终止自己。下面的例子就是这个思路,但在同步出了问题。
- //Broken - requires synchronization
- public class StoppableThread extends Thread{
- private boolean stopRequested=false;
- public void run(){
- boolean done=false;
- while(!stopRequested && !done){
- ...//do what needs to be done in the thread
- }
- }
- public void requestStop(){
- stopRequested=true;
- }
- }
对其改进如下:
- //Properly synchronized cooperative thread temination
- public class StoppableThread extends Thread{
- private boolean stopRequested=false;
- public void run(){
- boolean done=false;
- while(!stopRequested() && !done){
- ...//do what needs to be done in the thread
- }
- }
- public synchronized void requestStop(){
- stopRequested=true;
- }
- private synchronized boolean stopRequested(){
- return stopRequested;
- }
- }
另一种改进是,将stopRequested声明为volatile,则同步可以省略。
再来看迟缓初始化(lazy initialization)问题,双重访问模式并不一定都能正常工作,除非被共享的变量包含一个原语值。看例子:
- //The double-check idion fro lazy initialization - broken!
- private static Foo foo=null;
- public static Foo getFoo(){
- if (foo==null){
- synchronized(Foo.class){
- if(foo==null)foo=new Foo();
- }
- }
- return foo;
- }
最容易的修改是省去迟缓初始化:
- //normal static initialization (not lazy)
- private static finall Foo foo=new Foo();
- public static Foo getFoo(){
- return foo;
- }
或者使用正确的同步方法,但可能增加少许的同步开销:
- //properly synchronized lazy initialization
- private static Foo foo=null;
- public static synchronized Foo getFoo(){
- if(foo==null)foo=new Foo();
- return foo;
- }
按需初始化容器模式也不错,但是它只能用于静态域,不能用于实例域。
- //The initialize-on-demand holder class idiom
- private static class FooHolder(){
- static final Foo foo=new Foo();
- }
- public static Foo getFoo(){ return FooHolder.foo;}
简而言之,无论何时当多个线程共享可变数据的时候,每个读或写数据的线程必须获得一把锁。如果没有同步,则一个线程所做的修改就无法保证被另一个线程所观察到。
NO.51 不要依赖于线程调度器
不能让应用程序的正确性依赖于线程调度器。否则,结果得到的应用程序既不健壮也不具有可移植性。作为一个推论,不要依赖Thread.yield或者线程优先级。这些设施都只是影响到调度器,它们可以被用来提高一个已经能够正常工作的系统的服务质量,但永远不应用来“修正”一个原本并不能工作的程序。
编写健壮的、响应良好的、可移植的多线程应用程序的最好办法是,尽可能确保在任何给定时刻只有少量的可运行线程。这种办法采用的主要技术是,让每个线程做少量的工作,然后使用Object.Wait等待某个条件发生,或者使用Thread.sleep睡眠一段时间。
NO.52 线程安全性的文档化
每个类都应该清楚地在文档中说明它的线程安全属性。在一个方法的声明中出现synchronized修饰符,这是一个实现细节,并不是导出的API文档的一部分。
一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全性级别。
非可变性(immutable)-这个类的实例对于其它客户而言是不变的,不需要外部的同步。参见13条。
线程程安全的(thread-safe)-这个类的实例是可变的,但是所有的地方都包含足够的同步手段,这些实例可以被并发使用无需外部同步。
有条件的线程安全(conditionally thread-safe)-这个类(或关联的类)包含有某些方法,它们必须被顺序调用,而不能受到其它线程的干扰,除此之外,这种线程安全级别与上一种情形相同。为了消除被其他线程干扰的可能性,客户在执行此方法序列期间,必须获得一把适当的锁。如HashTable或Vector,它们的迭代器要求外部同步。如: