Java 之 volatile 解析

来源:互联网 发布:mac dock栏finder恢复 编辑:程序博客网 时间:2024/05/20 02:56
本文参考周志明先生的《深入理解Java虚拟机(第2版)》和《Java 理论与实践: 正确使用 Volatile 变量》写成,如有错误之处,请不吝指出。

volatile 和 synchronized 是java 的两种同步机制。介绍volatile前,先介绍几个概念。

Java内存模型

Java内存模型定义了程序中各个变量的访问规则。所有的变量(如图中主内存中的Counter变量)都存储在主内存(Main Memory)中,如下图蓝色部分所示,每条线程有自己的工作内存(Working Memory),工作内存中保存了该线程使用的该变量的主内存副本拷贝(如图中工作内存中的Counter),线程对变量所有的操作如读取、赋值等,都必须在各自的工作内存中进行,而不能直接操作主内存中的变量。不同的线程之间也不能直接访问对方工作内存中的变量,线程之间的变量的传递需要通过主内存完成,具体来说是通过lock, unlock, read, load, use, assign, store, write等8中原子操作来完成的。

lock(锁定):锁定主内存中变量,使其只能被一个线程占用;

unlock(解锁):解锁主内存变量,使其可以被其他线程锁定;

read(读取):读取主内存变量到工作内存中,以被load使用;

load(载入):将read到的值赋给工作内存中的变量副本;

use(使用):某个线程的执行引擎使用该线程的工作内存中的变量副本;

assign(赋值):某个线程的执行引擎对该变量副本赋值;

store(存储):将工作内存中的变量副本传递给主内存,供write操作调用;

write(写入):将store传回的值赋给主内存中的变量;


volatile特性

volatile具有可见性,有序性(指令重排)和原子性。 

可见性(Visibility)

当一个线程修改了共享变量的值,其他线程能立即获得新值;举例说明volatile的可见性。当线程2执行assign操作,及更改Counter副本的值,会触发store, write操作,及时更新住内存中Counter的值,当线程1执行use操作时,会触发read, load操作,其工作内存中的Counter立即得到更新,线程1使用的Counter值就是最新的。volatile关键字就是通过此种机制保证共享变量的最新值对所有线程可见的。

有序性(Ordering)

有序性是指禁止指令重排序优化,对于普通变量,仅能保证所有依赖赋值(如 i = 2; … j = i + 2;)的操作都能得到正确的结果,而无法保证赋值操作的顺序与代码执行的顺序一致;volatile修饰的变量,jvm编译时会在机器指令中插入一些代码,防止重排序现象的发生。

原子性(Atomicity)

某种操作是不可再分的,即具有原子性;


public class VolatileDemo {public static final int THREADS_COUNT = 10000;public volatile int counter = 0;/** * 累加操作  * @Title:       increase  * @Description: TODO  * @param          * @return       void  * @throws */public void increase() {counter++;}/** * 创建并启动累加线程  * @Title:       m  * @Description: TODO  * @param          * @return       void  * @throws */public void m() {Thread threads[] = new Thread[THREADS_COUNT];for (int i = 0; i < THREADS_COUNT; i++) {threads[i] = new Thread(new Runnable() {@Overridepublic void run() {increase();}});threads[i].start();}// 等待所有累加线程结束for (int i = 0; i < THREADS_COUNT; i++) {try {threads[i].join();} catch (InterruptedException e) {e.printStackTrace();}}// 待所有线程结束,输出累加结果System.out.println("counter: " + counter);}public static void main(String[] args) {VolatileDemo vd = new VolatileDemo();vd.m();}}

运行50次的输出结果是:9999, 10000, 9999, 10000, 9999, 9999, 10000, 9999, 10000, 10000, 10000, 9999, 10000, 10000, 9998, 10000, 10000, 10000, 9998, 9999, 9999, 10000, 10000, 10000, 10000, 10000, 9999, 10000, 9999, 9999, 9999, 10000, 9999, 9999, 10000, 10000, 9998, 10000, 10000, 10000, 10000, 9999, 9998, 9996, 10000, 10000, 9999, 9999, 9999, 9999

在increase()方法前增加synchronized关键字,运行50次的输出结果是:10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000

现象解释:由于counter++操作是非原子操作,多个线程可以同时进行此操作,如某2个线程的副本变量均为10,同时进行counter++操作,操作完成后更新主内存的counter,2次更新的结果本应该为12,实际却是2次重复更新为11,最终结果是11,无法完成计数器的功能。

synchronized()方法将increase()变成原子操作,每次只能有一个线程进入increase(),下一个线程再进入该方法时,使用的上个线程更新后的counter值,故计数结果是正确的。


volatile的应用场景

至于volatile的应用场景,本文只举一例,其余场景请参见《Java 理论与实践: 正确使用 Volatile 变量》和我的另一篇博文《单例模式》中的“双重检查锁”。

volatile 可修饰状态标识变量,一个线程负责更改标识的状态,其他线程据此做相应的动作,为了保证该标识为的可见性,可以将其修饰为volatile。代码如下:

volatile boolean shutdownRequested;...public void shutdown() { shutdownRequested = true; }public void doWork() {     while (!shutdownRequested) {         // do stuff    }}

1 0
原创粉丝点击