写在开头的地方,本文是笔者的理解,不一定正确,但属于是自己较为深入的学习所得,在此进行分享学习。
话不多说,开搞, 一些volatile的基础知识我就不说了,我们先来看两类代码java和c++的
先上java的代码,非常简单
public class Test_1 {
private static int a = 0;
public static void main(String[] args) {
new Thread(() -> {
System.out.println("等待thread-2改变对a的数值");
while (0 == a) {
}
System.out.println("感受到数值的改变" + a);
}, "thread-1").start();
new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread-2改变对a的数值");
a = 1;
}, "thread-2").start();
}
}
因为静态字段a没有被volatile字段修饰,所以「感受到数值的改变」这句话不会被输出。
实际情况也的确如此。
那么我们再来看c++的代码,会涉及到一些unix线程编程,但不关键
int a = 0;
void* thread1_do(void* arg){
INFO_PRINT("等待thread-2改变对a的数值\n");
while (0 == a){}
INFO_PRINT("感受到数值的改变\n");
return NULL;
}
void* thread2_do(void* arg){
INFO_PRINT("thread-2改变对a的数值\n");
a = 1;
return NULL;
}
int main(){
pthread_t tid_1[1];
pthread_t tid_2[1];
pthread_attr_t attrs_1[1];
pthread_attr_t attrs_2[1];
pthread_attr_init(attrs_1);
pthread_attr_setdetachstate(attrs_1,PTHREAD_CREATE_DETACHED);
pthread_attr_init(attrs_2);
pthread_attr_setdetachstate(attrs_2,PTHREAD_CREATE_DETACHED);
pthread_create(tid_1, attrs_1, thread1_do, NULL);
pthread_create(tid_2, attrs_2, thread2_do, NULL);
pthread_attr_destroy(attrs_1);
pthread_attr_destroy(attrs_2);
sleep(1);
}
会发现,同样是局部变量的a,同样的线程逻辑。
但是却发现结果不同,这是为什么呢???
那么我们的第一个问题
问题一 为什么java线程修改全局变量是不可见的,而c++(unix)线程修改全局变量却是可见的。
我们先来回答后者,使用clion自带的lldb,我们debug可以看到反汇编
这句汇编很简单吧,简单的赋值操作,也就是说,是直接修改内存的值,所以c++线程修改全局变量是可见的。
那么我们再来看看,为什么java线程修改全局变量是不可见的,讲道理,java基于jvm运行,jvm是c++写的,也应该可见啊。其实用理论知识证明也很容易,
证明:因为java线程是运行在虚拟机栈中的,而虚拟机栈是不共享的。
因为java线程是面向对象的形式出现的,且java的线程实现了自己的一套JMM模型。
因为java虚拟机栈中存在栈帧,也就是在java语言进行a = 1时做的操作是如何的,通过jclasslib插件我们可以很清晰看到
icounst_1: 将常量1压入操作数栈
putstatic: 将操作数栈中顶端第一个值弹出栈对a进行赋值
可以发现,这两条指令都是在虚拟机栈中进行的,而虚拟机栈又不是共享的,所以a的改变并不会同步到方法区的引用中去。
问题二 为什么java中全局变量加了volatile关键字,字段就有了「可见性」
这个就留到之后再讲吧,其实网上资料也挺全的了,内存屏障啊,happens-before原则之类的,但是大多都还是理论,我看看之后有空把源码翻出来整理整理吧。本文到此结束。
后记:对于问题2,putstatic,getstatic这类字节码中在bytecodeparse解析中会有一个判断,is_volatile()的方法,有兴趣可以自己先看看。