理解 JVM:Java 内存模型(二)——volatile

2017-11-15 11:37:27来源:oschina作者:alexpdh人点击

分享
概述

java 内存模型的核心是围绕着在并发过程中如何处理原子性、可见性、有序性这3个特性来展开的,它们是多线程编程的核心。



**原子性(Atomicity):**是指一个操作是不可中断的,即使是多个线程同时执行的情况下,一个操作一旦开始,就不会被其它线程干扰。对于基本类型的读写操作基本都具有原子性的(在32位操作系统中 long 和 double 类型数据的读写不是原子性的,因为它们有64位)。
**可见性(Visibility):**是指在多线程环境下,当一个线程修改了某一个共享变量的值,其它线程能够立刻知道这个修改。
**有序性(Ordering):**是指程序的执行顺序是按照代码的先后顺序执行的;对于这句话如果在单线程中所有的操作都是有序的,但是在多线程环境下,一个线程的操作相对于另外一个线程的操作是无序的。

关键字 volatile

volatile 修饰的变量保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。因为当对普通变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。而volatile修饰的变量,JVM保证了每次读变量都从内存中读,跳过CPU cache这一步。volatile修饰的变量禁止进行指令重排序,所以能在一定程度上保证有序性。只能保证该变量所在的语句还是原来的位置,并不能保证该语句之前或之后的语句是否被打乱。



volatile 的特性
当一个变量被 volatile 修饰之后,能保证此变量对所有线程的可见性,即当一个线程修改了这个变量的值,新值对其它线程是立即可见的。
被 volatile 修饰的变量通过查询内存屏障来禁止指令重排序,所以能在一定程度上保证有序性。
对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性。例如:package com.pdh.test;
/**
* volatile 复合操作测试
*
* @author pengdh
* @date 2017/11/12
*/
public class VolatileDemo {
// 申明 volatile 变量
private static volatile int i = 0;
// 计数
private static final int COUNT = 10;
/**
* 对 volatile 变量复合运算
*/
private static void increase() {
i++;
}
public static void main(String[] args) {
// 启动 10 个线程分别对 i 进行 10000 次计算,正常情况结果为 100000
for (int j = 0; j < COUNT; j++) {
new Thread(() -> {
for (int k = 0; k < 10000; k++) {
increase();
}
}).start();
}
// 等待所有累加线程全部执行结束,这里不同 ide 中线程存活数不一样,
// 该示例代码在 idea 中运行,会多出一个 Monitor Ctrl-Break 线程,故条件是 > 2,
// 如果在 Eclipse 中条件应为 > 1
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(i);
}
}

如上代码正常运行结果应该打印100000,但实际结果基本得不到正确结果。这说明了 volatile 变量的复合运算并不具有原子性,想要得到正确结果,需要对 volatile 变量运算操作加锁或者加上同步块。


package com.pdh.test;
/**
* volatile 复合操作测试
*
* @author pengdh
* @date 2017/11/12
*/
public class VolatileDemo {
// 申明 volatile 变量
private static volatile int i = 0;
// 计数
private static final int COUNT = 10;
/**
* 对 volatile 变量复合运算,使用 synchronized 同步
*/
private static synchronized void increase() {
i++;
}
public static void main(String[] args) {
// 启动 10 个线程分别对 i 进行 10000 次计算,正常情况结果为 100000
for (int j = 0; j < COUNT; j++) {
new Thread(() -> {
for (int k = 0; k < 10000; k++) {
increase();
}
}).start();
}
// 等待所有累加线程全部执行结束,这里不同 ide 中线程存活数不一样,
// 该示例代码在 idea 中运行,会多出一个 Monitor Ctrl-Break 线程,故条件是 > 2,
// 如果在 Eclipse 中条件应为 > 1
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(i);
}
}volatile 适合场景

volatile适用于不需要保证原子性,但却需要保证可见性的场景。一种典型的使用场景是用它修饰用于停止线程的状态标记,如:


package com.pdh.test;/**
* volatile 复合操作测试
*
* @author pengdh
* @date 2017/11/12
*/
public class VolatileDemo {
// 申明 volatile 变量
private volatile boolean flag = false;
// 计数
private static final int COUNT = 10;
/**
* 使用 volatile 变量作为线程结束标志
*/
private void start() {
new Thread(() -> {
while (!flag) {
System.out.println("Thread is running");
}
}).start();
}
private void shutdown() {
flag = true;
System.out.println("Thread is stop");
}
public static void main(String[] args) throws InterruptedException {
VolatileDemo demo = new VolatileDemo();
demo.start();
Thread.sleep(2000);
demo.shutdown();
}
}使用 volatile 的意义

在某些情况下,volatile 的同步机制性能要优于锁。




参考文献
深入理解 Java 虚拟机


欢迎扫一扫关注 程序猿pdh 公众号!

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台