Java之volatile关键字 volatile 关键字

createh54个月前 (12-29)技术教程46

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 关键字之 native 详解 java native access

本篇博客我们将介绍Java中的一个关键字——native。native 关键字在 JDK 源码中很多类中都有,在 Object.java类中,其 getClass() 方法、hashCode()方法、...

小高分享(54)Java中的super关键字和final关键字

分享兴趣,传播快乐,增长见闻,留下美好!亲爱的您,这里是LearningYard新学苑。今天小编为你带来小高分享(54)Java中的super关键字和final关键字欢迎您的访问!Share inte...

《Java语言程序设计》期末考试模拟试题——判断题和问答题

一、是非选择题1、构造方法(Constructor)是否可被重写(override)?2、启动一个线程是用run()方法吗?3、是否可以继承String类?4、Java语言代码中能否应用goto语句?...

小伙子,你真的搞懂 transient 关键字了吗?

先解释下什么是序列化我们的对象并不只是存在内存中,还需要传输网络,或者保存起来下次再加载出来用,所以需要Java序列化技术。Java序列化技术正是将对象转变成一串由二进制字节组成的数组,可以通过将二进...

《Java基础知识》Java断言 - - 关键字 assert

背景断言:也就是所谓的assertion,是jdk1.4后加入的新功能。 它主要使用在代码开发和测试时期,用于对某些关键数据的判断,如果这个关键数据不是你程序所预期的数据,程序就提出警告或退出。 当软...

深入理解Java中的 Static 关键字 java里面static

#大有学问# #java#在Java编程语言中,static关键字是一个重要的概念。本文将详细介绍static关键字的用途、特点及使用场景,帮助您更深入地理解Java中的static关键字。1. s...