Java之volatile关键字 volatile 关键字

Java的volatile关键字用于标记一个java变量“存储在主内存中”。更准确的说,被volatile标记的变量每次读取操作都将从计算机主内存(computer's main memory)读取,而不是从CPU缓存中读取,而且对volatile变量的每次写入都是写入到主内存,而不仅仅是写到CPU缓存。

实际上,从jdk5之后volatile关键字不仅仅保证了从内存中读写volatile变量。我会在下面对此进行介绍。

一:变量可见性问题

volatile关键字保证了同一变量跨线程更改的可见性问题。

在多线程应用程序中,线程对非volatile变量进行操作,出于性能考虑,每个线程在处理变量时,可以将他们从主内存中复制到CPU缓存中,如果你的计算机包含一个以上的CPU,每个线程可以在不同的CPU上运行。这意味着,每个线程可以将变量复制到不同CPU的CPU缓存上。如下图所示:

对于非volatile变量而言,不能保证Java虚拟机(JVM)何时将主内存中数据读取到CPU缓存中,或何时将CPU缓存中数据写入到主内存中。这就导致了几个问题。

假设一个场景,超过2个及其以上的线程访问一个声明包含计数器变量的共享对象:

public class ShareCounterObject{
 public int counter = 0;
}

假设仅线程1能修改counter变量并且每次递增+1,线程1和线程2都时时的访问counter变量。

如果counter变量不用volatile关键字声明,那么将不能保证counter变量每次更新的值从CPU缓存中写回主内存中。这意味着counter变量在CPU缓存中的值和主内存中不同。

如图所示:

因为一个线程更新没有立即回写到主内存,导致其他线程不能获取非volatile变量最新的值,这种被叫做“可见性”问题。一个线程更新了非volatile声明的共享变量对其他线程不可见

二:Java volatile保证可见性

volatile关键字目的就是解决可见性问题。声明counter是volatile变量,所有对counter的更改都会立即从CPU缓存写回到主内存中.同时,其他读取这个变量的操作也将是从主内存中读取。

声明volatile变量代码块儿:

 public class ShareCounterObject{
 public volatile int counter = 0;
}

这样的话,线程1对counter变量更改,线程2对这个变量读取(但是不修改这个变量),volatile修饰的变量counter完全能保证在线程2写counter变量时的可见性。

如果线程1和线程2都递增counter变量,那么仅仅将其声明volatile是不够的。

三:Full volatile Visibility Guarantee

实际上volatile变量可见性保证volatile声明变量的本身,与他操作相关的变量也会保证。

规则1.如果线程A写入一个volatile变量,线程B随后读取同一个变量,那么线程A在写入volatile变量之前的所有可见的变量,线程B读取volatile变量后也可以看到。

规则2.如果线程A读取一个volatile变量,那么线程A读取volatile变量之前看到的所有变量都将从主内存中读取。

public class MyObject{
 private int years;
 private int months;
 private volatile int days;
 
 public void update(int years ,int months ,int days){
 this.years = years;
 this.months = months;
 this.days = days;
 }
}

这个代码块中只有days是volatile变量。根据完整的可见性保证规则所示,一个线程执行update方法对这3个变量修改,最后修改volatile变量,因为在他前面的变量years,months对该线程是可见的,那么当days变量写回到主内存中时years,months变量也会被写入到主内存。

public class MyObject{
 private int years;
 private int months;
 private volatile int days;
 
 public void update(int years ,int months ,int days){
 this.years = years;
 this.months = months;
 this.days = days;
 }

 public int totalDays(){
 int total = this.days;
 total += months * 30;
 total += years * 365;
 return total;
 }
}

如上代码块儿所示,执行total方法时,首先读取days的值到total变量,在读取days变量值的时候,同时后面的months和years变量也是读入到主内存中。因此可以通过上面的读序列能够看到days,months,years的最新值。

四:指令重排序

在保证语义不变的情况下,JVM和CPU因为性能的原因可能会进行指令重排序。例如:

 int a = 1;
 int b = 2;
 a++;
 b++;

如上代码,a,b变量在逻辑上毫无关系,指令重排序后可能执行顺序就是:

 int a = 1;
 a++;
 int b = 2;
 b++;

指令重排序虽然很利于性能的优化,但是也会导致一些意外的问题,如上面的MyObject对象,如果对update方法进行重排序之后:

 public void update(int years ,int months ,int days){
 this.days = days;
 this.years = years;
 this.months = months;
 }

变成这样之后,修改days变量的时候months,years变量仍然写入主内存,但这次days新值写入主内存发生在months,years写入之前。因此months,years的新值对其他线程是不可见的。那么重新排序的语义就发生了变化。

Happens-Before规则就是Java推出的一个方案。后面我会针对这个在做一个详细介绍。

本文相关资料链接:http://tutorials.jenkov.com/java-concurrency/volatile.html

如有不对的地方请留言指正,互相学习!

相关文章

Java语言static关键字详解 java 中static

在Java语言中,static关键字是一个非常重要的修饰符,可以创建独立于具体对象的域变量或者方法。也就是实现即使没有创建对象,也能使用属性和调用方法。另一个比较关键的作用就是 用来形成静态代码块以优...

Java关键字:final,static,this,super

1. final 关键字:final 关键字,意思是最终的、不可改变的,初始化之后就不能再次修改 ,用来修饰类、方法和变量,具有以下特点:final 修饰的类不能被继承,final类中的所有成员方法都...

还没弄明白Java中的this关键字吗,那来看这篇就够了

今天在上课时,冉冉大妹纸拉着我问:小哥哥,小哥哥,听说你在学Java,那你知道this关键字吗?我:啊?this啊? (完了完了,学习的时候学的什么也不是,这下被问到了,还是个妹纸,答不上来岂不尴尬)...

深入理解 Java 中的 volatile 关键字

在 Java 编程的神秘领域中,volatile关键字犹如一把神奇的钥匙,为多线程编程带来关键的保障。现在,让我们更深入地理解这个神秘的关键字以及其背后的重要机制 —— 内存屏障,同时探讨如何保证并发...

Java 中你绝对没用过的一个关键字?

这节课给大家介绍一个 Java 中的一个关键字 Record,那 Record 关键字跟不可变类有什么关系呢?看完今天的文章你就知道了。友情提示 Record 关键字在 Java14 过后才支持的,所...

三十四、Java中的final关键字 java中final关键字的用途

Java中的final关键字是一种修饰符,它有着多种用途,主要应用在变量、方法和类上,以指示不可变性或不可覆盖性。final 关键字修饰不同元素的作用Java元素作用变量当final修饰基本类型变量时...