前言
volatile是java的一个关键字,我们在很多地方都有看到。接下来我们一起探讨下这个关键字的作用和使用场景
一、volatile概述
volatile作为java的关键字,用来修饰变量,在多线程的场景下,能够保证程序的有序性和可见性。不用sychonized关键字就能实现线程安全。
二、问题引入
1.例子
代码如下(示例):
public class VolatileTest {
private static volatile int INIT_VALUE = 0;
private final static int MAX_LIMIT = 500;
public static void main(String[] args) {
new Thread(() -> {
int localValue = INIT_VALUE;
while (localValue < MAX_LIMIT) {
if (localValue != INIT_VALUE) {
System.out.printf("The value updated to [%d]\n", INIT_VALUE);
localValue = INIT_VALUE;
}
}
}, "READER").start();
new Thread(() -> {
int localValue = INIT_VALUE;
while (INIT_VALUE < MAX_LIMIT) {
System.out.printf("Update the value to [%d]\n", ++localValue);
INIT_VALUE = localValue;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "UPDATER").start();
}
}
2.执行结果分析
这段程序执行的结果为
update the value to [1]
The value updated to [1]
update the value to [2]
The value updated to [2]
update the value to [3]
The value updated to [3]
update the value to [4]
The value updated to [4]
update the value to [5]
The value updated to [5]
我们把INIT_VALUE 的volatile关键字去掉,执行结果为
update the value to [1]
update the value to [2]
update the value to [3]
update the value to [4]
update the value to [5]
READER线程陷入死循环。为什么会出现这种问题呢?接下来我们先看看java内存模型
3.java内存模型
在讲java内存模型之前,我们先看看cpu是如何执行我们的java程序的,大致有以下两个步骤:
首先、java编译器会将我们的代码编译成汇编指令
然后、汇编指令会在jvm处理之后转成cpu指令,发送给cpu执行
我们都知道汇编指令是操作数据的过程,而cpu执行程序的过程中从哪里拿数据呢?说到这,我们想到了我们工作常见的一种情况:application、缓存、数据库。为了减轻数据库的压力,我们会加缓存,比如redis等,application在执行的过程中访问数据,可能会先到缓存中去拿,当我们给数据库插入数据后,缓存也会做相应的更新,保证数据的一致性。
而cpu拿数据也是差不多的,cpu和内存RAM之间会有一个类似缓存的角色chacheline。如下图
而java的内存模型也是差不多的:有一个主内存,每个线程都有一份主内存的缓存。当我们执行程序的时候为了提高程序的执行效率,线程会从自己的缓存中去取数据,而不会主动到主内存中去拿。这就导致了一个问题,线程之间的的缓存数据不一致。jmm-java内存模型如下图:
看完java内存模型之后,线程READER线程陷入死循环的问题就很好解释了
WRIDTER线程和READER线程都有自己的一份缓存,当线程运行的过程中会到自己的缓存中去拿数据。所以当没有volatile关键字修饰INIT_VALUE的时候,WRITER线程虽然改变了INIT_VALUE的值,但是由于READER线程只到自己缓存中的数据不会到主内存去拿数据,导致READER中的INIT_VALUE的数据一直为0。所以程序就陷入了死循环。而当INIT_VALUE有volatile关键字修饰之后。READER线程会到主内存中去拿数据。这样就保证了数据可见性,也就是WRITER线程在更改数据之后READER线程对INIT_VALUE的数据可见。保证了数据的一致性和线程安全。让程序能够正常执行。
扩展问题,当有volatile关键字修饰的时候,线程会到住内存中去拿数据。那当没有volatile关键字修饰的时候,就一直不会到主内存中去拿数据吗?答案分两种情况。
第一种:如下截图:
这里的话是永远都不会到主内存中拿数据的
第二种:如果上面的代码对INITE_VALUE有写操作,则还是会到主内存拿数据的,但是并不能够保证线程安全。
为什么出现这两种情况呢?是因为java的优化机制,没有写操作,java认为数据没有改变,故永远不会到主内存拿数据。如果有写操作,线程在执行的过程中还是会到主内存中去拿数据的。