Java之volatile关键字 volatile 关键字

createh55个月前 (12-29)技术教程51

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 的标识符和关键字 java 的标识符和关键字一样吗

Java 的标识符和关键字标识符的概念:标识符就是我们在编程中使用的“名字”。给类、接口、方法、变量、常量、包名起的相对唯一的名字。在编程语言中,不管是什么样的编程语言都会有标识符。并且都有差不多的规...

Java 详细剖析关键字 static,深入全面了解

1. 概述static 是一种修饰符static 是Java中表静态的关键字它可以修饰成员变量、成员方法、代码块被static修饰的成员变量或成员方法,将不再依赖于对象的创建而去使用,而是依赖类的存在...

带你入门Java之每日3分钟Java——流程控制关键字

接着我们上期的带你入门Java之每日3分钟Java——数据运算符和显隐数据类型转换,我们来学习Java中的流程控制语句。判断语句选择判断语句指的是if else语句,其代码书写格式是if(statem...

你知道 Java 中关键字 enum 是一个语法糖吗?反编译枚举类

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情写下这篇文章,也纯属于是一个机缘巧合,我一个非常要好的朋友程,也是刚刚踏上工作岗位。这个问题也是他踏上岗位...

java基础之作用域关键字 作用域java四种权限

我是个木得感情的更新机器作用域关键字 public,private,protected用于标记类与方法,变量的作用域作用域 当前类 同一package 子孙类 其他类 public √ √ √ √pr...

Log4j 爆“核弹级”漏洞,Flink、Kafka等至少十多个项目受影响

作者 | 褚杏娟昨晚,你熬夜应急了吗?昨晚,对很多程序员来说可能是一个不眠之夜。12 月 10 日凌晨,Apache 开源项目 Log4j 的远程代码执行漏洞细节被公开,由于 Log4j 的广泛使用,...