我们已经知道同步代码块和同步方法可以确保以原子的方式执行操作,但一种常见的误解是synchronized只能用于实现原子性或者确定临界区。同步还有另一个重要的方面:内存可见性(Memory visibility)。我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。如果没有同步,那么这种情况就无法实现,
可见性
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) throws InterruptedException {
new ReaderThread().start();
number = 42;
ready = true;
}
}
NoVisibility可能会持续循坏下去,因为读线程可能永远都看不到ready的值,一种更奇怪的现象是Novisibility可能会输出0,因为读线程可能看到了写入ready的值,但却没有看到之后写入number的值,这种现象被称之为重排序。当主线程首先写入number,然后在没有同步的情况下写入ready,那么读线程看到的顺序可能与写入的顺序完全相反。
在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的的调整。在缺乏足够同步的多线程程序中,想要对内存操作的执行顺序进行判断,几乎无法得出正确的结论。
加锁与可见性
内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果,
当线程b执行由锁保护的同步代码块时, 可以看到线程A之前在同一个同步代码块中的所有操作。如果没有同步,就无法实现上述保证。
volatile变量
java语言提供了一种稍弱的同步机制,volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与处理器都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作重排序。volatile变量不会被缓存在寄存器或者其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。它是一种比synchronized关键字更轻量级的同步机制。
public class CountingSheep {
volatile boolean asleep;
void tryToSleep() {
while (!asleep)
countSomeSheep();
}
void countSomeSheep() {
// One, two, three...
}
}
虽然使用volatile变量很方便,但也存在一些局限性。volatile变量通常用做某个操作完成、发生中断的标志。但volatile的语义不足以确保递增操作count++的原子性。
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性
发布与逸出
发布一个对象是指,使对象能够在当前作用域于之外的代码中使用。当某个不应该发布的对象被发布时,这种情况称为逸出。 发布对象的最简单方法是将对象的引用保存到一个公有的静态变量中,以便任何类和线程都能看见该对象。
不要在构造过程中使this引用逸出
线程封闭
当某个对象封闭在一个线程中,这种用法将自动实现线程安全性。维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get、set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本。比如在单线程应用程序中可能会维持一个全局的数据块连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时要传递一个Connection对象。通过将jdbc的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接。从概念上看,你可以将ThreadLocal<T>视为包含了Map<Thread,T>对象,其中保存了特定于该线程的值
Final域
我们都知道final类型的域是不能修改的,然而在java内存模型中,final域还有着特殊的语义,final域能确保初始化过程的安全性