1. volatile的使用
public class VolatileUsedClass {
private static int sharedVar = 10;
public static void main(String[] args) throws Exception {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
// modify the sharedVar,write first
TimeUnit.MICROSECONDS.sleep(500L);
sharedVar = 20;
System.out.printf("%s modify the shared var to %s ...\n", "thread-1", sharedVar);
} catch (Exception e) {
System.out.println(e);
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
// read the value,read for the last
TimeUnit.MICROSECONDS.sleep(505L);
System.out.printf("%s read the shared var %s \n", "thread-2", sharedVar);
} catch (Exception e) {
System.out.println(e);
}
}
});
thread2.start();
thread1.start();
thread1.join();
thread2.join();
System.out.println("finish the thread task...");
}
}
不加volatile的执行结果(多次执行)
加volatile的执行结果(多次)
结果分析
在源代码中是添加时间休眠主要是保证先写后读的逻辑
从运行结果可以看出,虽然时间片很短,读线程的数据仍然是本地缓存的数据,并没有从主内存中读取值
添加volatile关键字之后,可以看到读线程的数据正是写线程之后的数据,也就是写读数据是一致的
无volatile修饰的执行结果(多次)
有volatile修饰的执行结果(多次)
结果分析
不带volatile修饰与客户端执行效果一致
但是使用server模式带有volatile的方式却出现了数据不一致的情况,为什么?
原因在于-server模式会在编译成字节码的时候进行代码重排序导致的,主要用于优化程序提升执行效率
clinet模式是jdk执行的默认配置,可用于测试环境或者本地开发
server模式一般用于生产环境,目的是开启server模式的时候编译器会针对OS系统情况做一些优化操作
服务端与客户端运行模式参考:
参考JVM Server vs Client Mode:
https://javapapers.com/core-java/jvm-server-vs-client-mode/
2. 原子性问题
说明: 以下运行环境是使用-client模式进行,排除重排序的干扰
jdk文档对于原子性的说明如下:
除了long和double类型之外,引用变量与大多数的原始数据类型都具备读写操作的原子性
所有使用volatile修饰的变量都具备读写操作的原子性
分析
针对64bit的数据类型,主要与处理器(32bit/64bit)有关,在32bit处理器上,JVM会将64bit的long/double划分为两个32bit的写操作,并不具备原子性(数据的读写主要是通过处理器总线与主内存进行传递)
基于Happen-Before原则,对于volatile的变量读取总是可以“看到”任何一个线程对该volatile变量的最后写入,因此在临界区代码的执行是具备原子性,即使是long或是double类型
代码
// 部分代码,在上述的写线程进行修改, 前提: volatile修饰变量sharedVar
Thread t1 = new Thread(){
@Override
public void run() {
try {
// modify the sharedVar
TimeUnit.MICROSECONDS.sleep(500L);
sharedVar ++;
System.out.printf("%s modify the shared var with atomic %s ...\n", "thread-1", sharedVar);
} catch (Exception e) {
System.out.println(e);
}
}
};
运行结果
sharedVar = 20L显示的字节码如下:
L3
LINENUMBER 102 L3
BIPUSH 20 // 压入线程的操作数栈
INVOKESTATIC com/xiaokunliu/blogs/thread/volatile2code/VolatileUsedClass.access$002 (I)I //实例化并加载sharedVar并压入操作数栈,说明完成赋值操作
POP // 弹出数据
L4
sharedVar ++ 显示的字节码如下:
L3
LINENUMBER 27 L3
INVOKESTATIC com/xiaokunliu/blogs/thread/volatile2code/VolatileUsedClass.access$000 ()I // 实例化并加载sharedVar并压入操作数栈
ICONST_1 // 将常量 1 压入线程操作数栈总
IADD // 执行sharedVar+1
INVOKESTATIC com/xiaokunliu/blogs/thread/volatile2code/VolatileUsedClass.access$002 (I)I // 重新实力化加载sharedVar,说明完成赋值操作
POP
L4
运行分析:
通过字节码可知,sharedVar ++;相比单纯赋值操作增加了一个添加的动作
也就整体代码块存在在并发多线程下交替执行的两个操作,不具备原子性,volatile在这里是保证代码刷新到主内存,对于sharedVar = const 是具备原子性的
对变量进行写操作的时候可以通过volatile来实现对其他线程的可见,同样在单步指令操作中是具备原子性,针对long或者double也起到具备原子性的作用
对于需要用volatile修饰的变量来完成一系列的非单步操作运算是无法保证原子性,必须借助lock的方式来实现代码块的原子性
JDK关于原子性问题说明参考:
参考JDK关于原子性文档:
https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html
感谢花时间阅读,如有用请转发或者点个好看,谢谢!
本文分享自微信公众号 - 疾风先生(Gale2Writing)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。