同步的重要性有两个方面:
- 实现原子性:防止某个线程正在使用对象状态而另一个线程同时在修改改状态
- 内存可见性:确保一个线程修改了对象状态后,其他线程能够看到发生的状态变化
失效数据
- 缺乏同步的程序可能会产生的一种错误情况就是——失效数据
- 失效数据举例
//在没有同步的情况下共享数据
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) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
引发的问题有
- NoVisibility 可能会持续循环下去,因为ReaderThread可能永远看不到ready写入的值
- NoVisibility 可能会输出0,因为ReaderThread可能看到了ready写入的值,但是没有看到写入的number的值(这种现象叫重排序)
非原子的64位操作
- 非volatile类型的64位数值变量(double和long)在多线程程序中使用也是不安全的
- 如果对该变量的读操作和写操作在不同的线程中执行,那么可能会读取某个值的高32位和另一个值的低32位
- 用关键字volatile来声明,或者用锁保护
加锁与可见性
- 为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步
volatile变量
- 稍弱的同步机制
- 编译器和runtime都会注意到这个变量是共享的,所以不会将该变量重排序,不会被缓存在寄存器或者其他处理器不可见的地方
- 行为类似于以下程序清单,但是使用volatile时不会执行加锁操作,因此也不会阻塞线程,比synchronized更轻量级
@ThreadSafe
public class SynchronizedInteger {
@GuardedBy("this") private int value;
public synchronized int get() { return value; }
public synchronized void set(int value) { this.value = value; }
}
- 仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们,不建议过度依赖volatile变量提供的可见性
- 通常用作某个操作完成、发生中断或者状态的标志
- 典型用法:检查某个状态标记以判断是否退出循环。以下示例中asleep必须为volatile变量,否则当asleep被另一个线程修改时,执行判断的线程却发现不了。
- 加锁机制可以确保可见性和原子性,而volatile变量只能确保可见性
//数绵羊
volatile boolean asleep;
...
while (!asleep)
countSomeSheep();