暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

正确认识volatile(一)

重塑之路 2020-05-17
267

volatile是Java中的一个关键词,我们在工作中很少会用到,但是这个是面试一定会问到的知识点,所以我们不得不去深入学习。要说volatile的使用场景,讲一个最好理解的:DCL。


关于volatile,一般面试官就一个问题:你从底层实现原理层面讲讲你对volatile的认识吧。各位要如何作答呢?


volatile很难学,为什么呢?因为想搞清楚它的实现原理,需要从以下几个方面去研究:

1、Java代码层面,即加volatile与不加有什么区别

2、Java字节码层面,即读写加了volatile修饰的变量的字节码长啥样子

3、openjdk层面,即jdk源码是如何处理加了volatile修改的变量的

4、c++层面,因为openjdk是由c/c++实现的,Java中的volatile底层用了c/c++的volatile

5、汇编层面,大家经常看到的内存屏障就是借助汇编指令lock实现的

6、编译器优化,即编译器优化到底是什么东西?优化了什么?加了volatile有什么区别……


这样讲下来大家是否对volatile有个整体上的认知了呢?为了讲明白volatile,讲得通俗易懂,目前来看至少需要3-4篇文章。本篇文章聚焦一个知识点:自上(Java代码层面)而下(c++代码)深入讲解JVM是如何处理加了volatile的读。


怎样算毕业


相信很多人都有这样的疑问:怎样才算学会了volatile?


如果你对Java中的volatile是如何实现多核多线程环境下这几个区域的数据一致性的,你就算学明白了:

1、当前CPU缓存,即当前执行写volatile修饰的变量的那个CPU,这个时候只有这个CPU缓存中的值是最新的

2、其他CPU缓存,即之前执行过volatile修饰的变量的所有CPU,这些CPU缓存中的值都是修改之前的

3、主存,即OS内存、Native Memory

4、工作内存,这是学习volatile时所有的书或文章都会提及的一个词。很多人的认知中:工作内存=虚拟机栈或者工作内存包含虚拟机栈。其实工作内存跟虚拟机栈根本就不存在任何父子关系或兄弟关系,所以你看过的很多书或文章对这块的讲解都是错误的。关于这个错误的正确认识我会在下篇讲解读volatile修饰的变量中给出答案。

5、虚拟机栈,即每个线程独有的一小块空间。


这里给出我看openjdk源码后得出的结论,具体的讲解,往后看:

  1. 读完成了工作内存与虚拟机栈的数据一致性,而且这个一致性是有延迟的,即在写之后的下一次读才能读到修改后的数值。

  2. 写完成了当前CPU缓存、其他CPU缓存、主存、工作内存这四个区域的数据一致性,那是怎么做到的呢?下篇讲。

上代码


1、Java代码

public class Test3 {
public static volatile int found = 0;


public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
System.out.println("等基友送笔来...");


while (0 == found) {
}


System.out.println("笔来了,开始写字...");
}
}, "我线程").start();


new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}


System.out.println("基友找到笔了,送过去...");


change();
}
}, "基友线程").start();
}


public static void change() {
found = 1;
}
}

稍微解释下这段代码:有两个线程:我线程、基友线程。『我线程』通过死循环阻塞在那里等待『基友线程』找到笔送过来,然后开始写字。『基友线程』等待一会就去找笔,找到了就送过去。


2、Java字节码(读)


这个是「我线程」run方法的字节码


3、Java字节码(写)


这是chang方法的字节码


4、openjdk源码(读)

CASE(_getstatic):
{
……
if (cache->is_volatile()) {
……
if (tos_type == atos) {

} else if (tos_type == itos) {
SET_STACK_INT(obj->int_field_acquire(field_offset), -1);
}
……
#define SET_STACK_INT(value, offset)   \ 
(*((jint *)&topOfStack[-(offset)]) = (value))


5、openjdk源码(写)

CASE(_putfield):
CASE(_putstatic):
{
……


//
// Now store the result
//
int field_offset = cache->f2_as_index();
if (cache->is_volatile()) {
if (tos_type == itos) {
obj->release_int_field_put(field_offset, STACK_INT(-1));
}

……

OrderAccess::storeload();


解密


关于读volatile修饰的共享变量,流传着一个错误的认知:读的时候会进行判断,判断虚拟机栈中是否存在这个变量,如果有就直接用,如果没有就去主存中取值,然后在虚拟机栈中创建一份拷贝,以备后续使用。看openjdk的源码后你就会发现,这个观点纯属瞎扯。


正确的认知是:读取volatile修饰的共享变量时,总是从主存中取数据,然后压入虚拟机栈中供程序运行使用。也就是说只要你修改的volatile修饰的变量同步到主存中了,那在读的时候一定是最新的值,只不过存在很微小的一点点延迟。所以学习volatile的核心在写。


结尾


这样就将读volatile修饰的变量的本质讲清楚了,看完有疑问的童鞋留言提问。


这里给大家两个与大家认知不太一样的结论,大家可以思考下看能否想明白:

工作内存与虚拟机栈没有任何关系
工作内存 = 方法区 + 堆区


关于这两个结论的正确认知及写volatile修饰的变量时是如何保证当前CPU缓存、其他CPU缓存、主存、工作内存四个区域的一致性的,下篇文章见。


文章转载自重塑之路,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论