第三章 对象的共享
3.1 可见性
重排序:
比如两步赋值操作,赋值的顺序可能会跟看到的顺序相反。在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。
Volatile变量:
用来确保将变量的更新操作通知到其它线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其它内存操作一起重排序。不会被缓存在寄存器或者对其他处理器不可见的地方,因此读取volatile类型的变量总会返回最新写入的值。
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
另外:volatile仅仅能保证内存可见性,而不能保证原子性
volatile变量正确使用方式:确保他们自身状态的可见性,确保他们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生(比如初始化或者关闭)
volatile boolean asleep;
...
while(!asleep)
countSomeSheep();
例如上面的程序,当没有睡着的时候进行数绵羊操作,为了能正确执行,asleep必须声明为volatile,否则,当asleep被另一个线程修改时,执行判断的线程却发现不了。
3.2 发布与逸出
发布一个对象的意思是,使对象能够在当前作用域之外的代码中使用。
逸出:当某个不应该发布的对象被发布时,这种情况就是逸出。
例如下面的程序:
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
当ThisEscape发布EvenListener时,也隐含的发布了ThisEscape实例本身,因为在这个内部类实例中包含了对ThisEscape实例的隐含引用。这个例子是隐式this逸出。
常见的this逸出:在构造函数中启动线程, 在构造方法中调用可覆盖的实例方法,在构造方法中创建内部类。
如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法。例子如下:
public class SafeListener {
private final EventListener listener;
private SafeListener(){
listener = new EventListener(){
public void onEvent(Event e){
doSomething(e);
}
);
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
3.3 线程封闭
当访问共享的可变数据时,通常需要使用同步。一种避免同步的方式就是不共享数据。
仅在单线程内访问数据,就不需要同步,这种技术被称为线程封闭。1.栈封闭 只能通过局部变量访问对象
2.ThreadLocal类:提供线程内部的局部变量,在本线程内随时可取,隔离其它线程
例如,因为JDBC的连接对象并非线程安全的,因此将JDBC的连接保存在ThreadLocal对象中,从而每个线程都会拥有属于自己的连接
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
public Connection init(){
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection(){
return connectionHolder.get();
}
3.4 不变性
满足同步需求的另一种方法是使用 不可变对象(Immutable Object)。不可变对象一定是线程安全的。
当满足以下这些条件的时候,对象才是不可变的:对象创建以后其状态就不能被修改;
对象的所有域都是 final 类型;
对象是正确创建的(在对象的创建期间,this引用没有逸出)。
两种很好的编程习惯:除非使用更高的可见性,否则应将所有的域都声明为私有的;除非需要某个域是可变的,否则都应该声明为final域;