第三章:共享对象
编写正确的并发程序关键在于对共享的可变状态进行管理
1. 可见性
没用同步的情况下共享变量的例子,不能保证主线程写入的值对于读线程是可见的,读线程可能会一致等待,也可能读出0(因为主线程在写入number之前已经写入ready,重排序现象)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class NoVisibility {
public static int number = 0 ;
public static boolean ready = false ;
/**
* 负责读的线程
*/
private static class ReadThread implements Runnable {
@Override
public void run () {
while (! ready ){
System . out . println ( "Waiting..." );
Thread . yield ();
}
System . out . println ( "Number = " + number );
}
}
/**
* 主线程, 写
* @param args
*/
public static void main ( String [] args ){
new Thread ( new ReadThread ()). start ();
number = 100 ;
ready = true ;
}
}
结论:只要数据被跨线程共享,就需要进行恰当的同步.
上面例子的一种情况是,读线程读取的是一个过期数据.但更坏的情况是:过期即不是发生在全部变量上,也不是不过期,而是部分变量过期.
最低限的安全性(out-of-thin-air safety):当线程在没有同步的情况下读取变量,它可能会的得到过期值.但是它至少看到的是某个线程设置的真实值,而不是凭空而来的值.
最低限安全性应用于所有的变量,除了:没有声明为volatile的64位的数值变量(long,double),JVM允许将64位的读或写划分为两个32位的操作.
为了保证所有的线程能够看到共享的可变变量的最新值,读取和写入都必须使用公共的锁进行同步.
Volatile提供了同步的一种弱形式.
当一个变量声明为volatile类型后,编译与运行时候会见识这个变量,而且对它的操作不会被重排序.
volatile变量不会缓存在缓存器或者缓存在对其他处理器隐藏的地方.所以,读一个volatile类型变量时候总会返回由某一个线程写入的最新值.
访问volatile变量的操作不会加锁,也就不会引起执行线程的阻塞,这使得volatile变量相对于sychronized而言,只是轻量级的同步机制.通常被当作标记完成,中断或某个状态的标记使用.
注意:加锁可以保证可见性和原子性,volatile变量只能保证可见性.比如volatile不足以保证自增操作(count++)原子化.
2. 发布和逸出
发布(publish)一个对象的意思是使它能够被当前范围之外的代码锁使用.
逸出(Escape):一个对象尚未准备号时候就将它发布.
下面是一个发布的例子,init()声明了一个HashSet,并将它存储到公共的静态域中.
1
2
3
4
5
6
7
public class Publish {
public static Set < Secret > knownSecret ;
public void init (){
knownSecret = new HashSet < Secret >();
}
}
类似的,非私有的方法中返回引用,也能发布返回的对象,如:
1
2
3
4
5
6
7
public class Publish {
private String [] country = { "USA" , "China" };
public String [] getCountry () {
return country ;
}
}
这种发布会有问题,任何一个调用者都能修改它的内容.
3. 线程封闭
实现线程安全最简单的方式之一就是线程封闭.
一种维护线程限制的方式是使用ThradLocal,它提供了get和set访问器,为每个使用它的线程维护了一份单独的拷贝,所有get总是返回当前执行线程通过set方法设置的最新值.简单示例如下:
1
2
3
4
5
6
7
8
9
10
private static ThreadLocal < Connection > connectionHolder = new ThreadLocal < Connection >(){
@Override
protected Connection initialValue () {
return super . initialValue ();
}
};
public static Connection getConnection (){
return connectionHolder . get ();
}
4. 不可变性
为例满足同步的需要,另外一种方法是使用不可变对象.不可变对象即创建后状态不能被修改的对象.
不可变对象并不适简单的等于将对象的所有域都声明为final类型,所有域都是final类型的对象仍然可以是可变的,因为final域可以获得一个可变对象的引用.
5. 安全发布
在并发程序中,使用和共享对象的一些最有效的策略如下:
线程限制:一个线程限制对象,通过限制在线程中,而被线程独占,且只能被占有它的线程修改.
共享只读:一个共享的只读对象,在没有额外同步的情况下啊,可以被多个线程并发的访问,但是任何线程都不能修改它.
共享线程安全:一个线程安全的对象在内部进行同步,所以其他线程无需额外的同步,就可以通过公共接口随意访问它.
被守护的:一个被守护的对象只能通过特定的锁来访问.