可见性
在一个单线程程序中,如果向一个变量先写入值,然后在没有写干涉的情况下读取这个变量,会得到相同的返回值。但是当读和写发生在不同的线程中时,就不能保证读线程及时地读取其他线程写入的值。在JAVA中所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享,局部变量,方法定义参数和异常处理器参数不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。为了确保跨线程写入的内存可见性,必须使用同步机制
下例是主线程和读线程两个线程访问共享变量ready和number。主线程启动读线程,然后把number的值设为42,ready的值赋为true。读线程进行循环,直到发现ready的值变为true,然后打印出number的值。虽然看起来会输出42,但事实上,它很有可能打印出0,或者根本不会终止。这是因为它没有使用恰当的同步机制,没能保证主线程写入ready和number的值对读线程是可见的
package com.henrysun.javaSE.bfbc;/** * 在没有同步的情况下共享变量(不要这样做) * 重排序现象 * @author Sam Flynn * */public class GongXiangBianLiangReordering { 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); } } /** * @param args */ public static void main(String[] args) { new ReaderThread().start(); number=42; ready=true; }}
重排序现象
上面的例子中,可能会打印0,因为早在对number赋值之前,主线程就已经写入ready并使之对读线程可见,这叫做。
这里处理器A和处理器B可以同时把共享变量写入自己的写缓冲区(A1,B1),然后从内存中读取另一个共享变量(A2,B2),最后才把自己写缓存区中保存的脏数据刷新到内存中(A3,B3)。当以这种时序执行时,程序就可以得到x = y = 0的结果
锁和可见性
锁不仅仅是关于同步互斥的,也是关于内存可见的。当线程A执行一个同步块时,线程B也随后进入了被同一个锁监视的同步块中,这时可以保证,在锁释放之前对A的可见的变量的值,B获得锁之后同样是可见的。换句话说,当B执行到A相同的锁监视的同步块时,A在同步块之中或之前所做的每件事,对B都是可见的。为了保证所有线程都能够看到共享的,可变变量的最新值,读取和写入线程必须使用公共的锁进行同步
Volatile变量
当一个域声明为volatile类型后,编译器与运行时会监视这个变量:它是共享的,而且对它的操作不会与其他的内存操作一起被重排序。volatile变量不会缓存在寄存器或者缓存在其他对处理器隐藏的地方。所以,读一个volatile类型的变量时,总会返回由某一线程写入的最新值
但是volatile变量的操作不会加锁,也就不会引起执行线程的阻塞,所以它只是轻量级的同步机制,正确使用volatile变量的方式包括:用于确保它们所引用的对象状态的可见性,或者用于标识重要的生命周期事件(比如初始化或关闭)的发生,即通常被当作标识完成、中断、状态的标记使用
尽管volatile也可以用来标识其他类型的状态信息,但是决定这样做之前请格外小心。比如,volatile的语义不足以使自增操作(count++)原子化,除非你能保证只有一个线程对变量执行写操作。加锁可以保证可见性与原子性,而volatile变量只能保证可见性
只有满足了下面的标准,才能使用volatile变量
- 写入变量时并不依赖变量的当前值;或者能够确保只有单一的线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
- 而且,访问变量时,没有其他的原因需要加锁
ThreadLocal
线程本地变量,详见