接上回!
避免同步
大部分显示的同步都可以避免。一般不操作对象状态信息(例如数据成员)的方法都不需要同步,例如:一些方法只访问本地变量(也就是说在方法内部声明的变量),而不操作类级别的数据成员,并且这些方法不会通过传入的引用参数来修改外部的对象。符合这些条件的方法都不需要使用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的效率,因为






