接上回!
避免同步
大部分显示的同步都可以避免。一般不操作对象状态信息(例如数据成员)的方法都不需要同步,例如:一些方法只访问本地变量(也就是说在方法内部声明的变量),而不操作类级别的数据成员,并且这些方法不会通过传入的引用参数来修改外部的对象。符合这些条件的方法都不需要使用synchronization这种重量级的操作。除此之外,还可以使用一些设计模式(Design Pattern)来避免同步(我将会在后面提到)。
你甚至可以通过适当的组织你的代码来避免同步。相对于同步的一个重要的概念就是原子性。一个原子性的操作事不能被其他线程中断的,通常的原子性操作是不需要同步的。
Java定义一些原子性的操作。一般的给变量付值的操作是原子的,除了long和double。看下面的代码:
class Unreliable
{
private long x;
public long get_x( ) {return x;}
public void set_x(long value) { x = value; }
}
线程1调用:
obj.set_x( 0 );
线程2调用:
obj.set_x( 0x123456789abcdef )
问题在于下面这行代码:
x = value;
JVM为了效率的问题,并没有把x当作一个64位的长整型数来使用,而是把它分为两个32-bit,分别付值:
x.high_word = value.high_word;
x.low_word = value.low_word;
因此,存在一个线程设置了高位之后被另一个线程切换出去,而改变了其高位或低位的值。所以,x的值最终可能为0x0123456789abcdef、0x01234567000000、0x00000000abcdef和0x00000000000000。你根本无法确定它的值,唯一的解决方法是,为set_x( )和get_x()方法加上synchronized这个关键字或者把这个付值操作封装在一个确保原子性的代码段里。
所以,在操作的long型数据的时候,千万不要想当然。强迫自己记住吧:只有直接付值操作是原子的(除了上面的例子)。其它,任何表达式,象x = ++y、x += y都是不安全的,不管x或y的数据类型是否是小于64位的。很可能在付值之前,自增之后,被其它线程抢先了(preempted)。
竞争条件
在术语中,对于前面我提到的多线程问题——两个线程同步操作同一个对象,使这个对象的最终状态不明——叫做竞争条件。竞争条件可以在任何应该由程序员保证原子操作的,而又忘记使用synchronized的地方。在这个意义上,可以把synchronized看作一种保证复杂的、顺序一定的操作具有原子性的工具,例如给一个boolean值变量付值,就是一个隐式的同步操作。
不变性
一种有效的语言级的避免同步的方法就是不变性(immutability)。一个自从产生那一刻起就无法再改变的对象就是不变性对象,例如一个String对象。但是要注意类似这样的表达式:string1 += string2;本质上等同于string1 = string1 + string2;其实第三个包含string1和string2的string对象被隐式的产生,最后,把string1的引用指向第三个string。这样的操作,并不是原子的。
由于不变对象的值无法发生改变,所以可以为多个线程安全的同步操作,不需要synchronized。
把一个类的所有数据成员都声明为final就可以创建一个不变类型了。那些被声明为final的数据成员并不是必须在声明的时候就写死,但必须在类的构造函数中,全部明确的初始化。例如:
Class I_am_immutable
{
private final int MAX_VALUE = 10;
private final int blank_final;
public I_am_immutable( int_initial_value )
{
blank_final = initial_value;
}
}
一个由构造函数进行初始化的final型变量叫做blank final。一般的,如果你频繁的只读访问一个对象,把它声明成一个不变对象是个保证同步的好办法,而且可以提高JVM的效率,因为HotSpot会把它放到堆栈里以供使用。
同步封装器(Synchronization Wrappers)
同步还是不同步,是问题的所在。让我们跳出这样的思维模式吧,世事无绝对。有什么办法可以使你的类灵活的在同步与不同步之间切换呢? 有一个非常好的现成例子,就是新近引入JAVA的Collection框架,它是用来取代原本散乱的、繁重的Vector等类型。Vector的任何方法都是同步的,这就是为什么说它繁重了。而对于collections对象,在需要保证同步的时候,一般会由访问它方法来保证同步,因此没有必要两次锁定(一次是锁定包含使用collection对象的方法的对象,一次是锁定collection对象自身)。Java的解决方案是使用同步封装器。其基本原理来自四人帮(Gang-of-Four)的Decorator模式,一个Decorator自身就实现某个接口,而且又包含了实现同样接口的数据成员,但是在通过外部类方法调用内部成员的相同方法的时候,控制或者修改传入的变量。java.io这个包里的所有类都是Decorator:一个BufferedInputStream既实现了虚类InputStream的所有方法,又包含了一个InputStream引用所指向的成员变量。程序员调用外部容器类的方法,实际上是变相的调用内部对象的方法。
我们可以利用这种设计模式。来实现灵活的同步方法。如下例:
Interface Some_interface
{
Object message( );
}
class Not_thread_safe implements Some_interface
{
public Object message( )
{
//实现该方法的代码,省~~~~~~~~~~~
return null;
}
}
class Thread_safe_wrapper implements Some_interface
{
Some_interface not_thread_safe;
public Thread_safe_wrapper(Some_interface not_thread_safe)
{
this.not_thread_safe = not_thread_safe;
}
public Some_interface extract( )
{
return not_thread_safe;
}
public synchronized Object message( )
{
return not_thread_safe.message( );
}
}
当不存在线程安全的时候,你可以直接使用Not_thread_safe类对象。当需要考虑线程安全的时候,只需要把它包装一下:
Some_interface object = new Not_thread_safe( );
……………
object = new Thread_safe_wrapper(object); //object现在变成线程安全了
当你不需要考虑线程安全的时候,你可以还原object对象:
object = ((Thread_safe_Wrapper)object).extract( );
下一回,我们又要深入底层机制了。呵呵!千万不要闷着大家呀!下回见!