【Java多线程】Volatile关键字详解

时间:2021-10-01 15:46:05

预备知识

1..内存模型

通过缓存一致性协议来保证一致性。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

图解:
【Java多线程】Volatile关键字详解

2.原子性、可见性与有序性

  • 原子性:在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

  • 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

  • 有序性:在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

3.happens-before原则(先行发生原则)

  • 程序顺序规则:一个线程中的每个操作,先行发生于该线程中的任意后序操作。

  • 监视器锁规则:对一个锁的解锁,先行发生于随后对这个锁的加锁

  • volatile变量规则:对一个volatile域的写,先行发生于任意后续对这个volatile域的读。

  • 传递性:如果A先行发生于B,而B又先行发生于C,那么A先行发生于C

  • start()规则:Thread对象的start()方法先行发生于此线程的每个一个动作

  • join()规则:如果线程A执行操作ThreadB.join()方法并成功返回,那么线程B中的任意操作先行发生于线程A从ThreadB.join()操作成功返回

引入

例1:没有Volatile修饰

public class VolatileTest {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
new Thread(td).start();

while(true){
if(td.isFlag()){
System.out.println("已跳出循环!");
break;
}
}
}
}

class ThreadDemo implements Runnable {

private boolean flag = false;

@Override
public void run() {

try {
Thread.sleep(200);
} catch (InterruptedException e) {
}

flag = true;

System.out.println("flag=" + isFlag());

}

public boolean isFlag() {
return flag;
}

}

输出结果(程序一直在运行):flag=true

分析:你会产生疑问,flag=true啊,为什么没有进入到if 方法里面呢!别急,我来小小的改造一下,你看看结果!

例2:加入Volatile关键字修饰

public class VolatileTest {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
new Thread(td).start();

while(true){
if(td.isFlag()){
System.out.println("已跳出循环!");
break;
}
}
}
}

class ThreadDemo implements Runnable {

//用volatile关键字修饰
private volatile boolean flag = false;

@Override
public void run() {

try {
Thread.sleep(200);
} catch (InterruptedException e) {
}

flag = true;

System.out.println("flag=" + isFlag());

}

public boolean isFlag() {
return flag;
}

}

运行结果(程序已结束):

flag=true
已跳出循环!

分析:你会发现结果正常了,仅仅是因为加了volatile关键字修饰吗,对,就是这样,我接下来就讲讲volatile关键字。

解析

1.特点

(1)禁止指令重排序,但不具备“互斥性”;

(2)当多个线程进行操作共享数据时,可以保证内存中的数据可见,但不能保证变量的“原子性”。

2.指令重排序的问题

(1)实例化一个对象其实可以分为三个步骤:

  • 分配内存空间
  • 初始化对象
  • 将内存空间的地址赋值给对应的引用

但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:

  • 分配内存空间。
  • 将内存空间的地址赋值给对应的引用。
  • 初始化对象

如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量。

3..内存可见性问题

(1)Lock前缀指令会引起处理器缓存写到内存,线程的本地内存失效,别的线程只能从主存中读取数据。而本地内存的值会立马刷新到主存中去。

(2)一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

4.synchronized和volatile比较

(1)关键字volatile是线程同步的轻量级实现,所以volatile性能比synchronized要好并且volatile只用来修饰变量,而synchronized可以修饰方法,以及代码块

(2)多线程访问volatile不会发生阻塞,而synchronized会出现阻塞

(3)volatile可以保证数据的可见性但不能保证原子性,synchronized可以保证原子性,也可以保证可见性。

5.注意

(1) 使用volatile强制的从公共内存中读取变量的值,

(2)volatile的非原子性,在i++或i=i+1;会出现不安全

(3)在JSR-133之前的旧内存模型中,一个64位long/double型变量的读写操作可以被拆分为两个32位的读写操作执行,但从JSR-133(JDK5)开始,只允许将一个64位long/double型变量的写操作可以被拆分为两个32位的写操作执行,任意读操作必须具有原子性(即必须要在单个读事务中执行)



本人才疏学浅,若有错误,还请指出
谢谢!