撕开volatile的外衣一饱眼福

 

一、概述

由于疫情的影响,今天才回到长沙,估计各位道友跟我一样早就想发泄下内心的躁动,今天就满足下各位的要求,撕开volatile的外衣让大家一饱眼福。今天打算从下面三个方面来解剖

volatile 关键字是什什么?

volatile 关键字能解决什么问题?

使用场景是什么?

volatile关键字实现的原理理?

 

二、volatile 关键字是什什么?

Sun 的 JDK 官⽅方⽂文档是这样形容 volatile 的:

This means that changes to a volatile variable are always visible to other threads. What's more, it also means that when a thread reads a volatile variable, it sees not just the latest change to the volatile, but also the side effects of the code that led up the change.

翻译过来:如果一个变量加了 volatile 关键字,就会告诉编译器和 JVM 的内存模型:这 个变量是对所有线程共享的、可⻅的,每次 JVM 都会读取最新写⼊的值并使其最新值在 所有 CPU 可见。volatile 可以保证线程的可见性并且提供了一定的有序性,但是无法保 证原⼦子性。在 JVM 底层 volatile 是采⽤内存屏障来实现的。

 

通过这段话,我们可以知道 volatile 有两个特性:

保证可见性、不保证原⼦性

禁⽌指令重排序

 

三、可见性、原子性、有序性、重排序为何物

1、先说三性

可见性、有序性、原子性统称为并发三大核心问题。如果把并发编程的祖坟挖出来、你会发现一切的根源至:

 

1、硬件的发展中,一直存在⼀个⽭盾,CPU、内存、I/O设备的速度差异。

2、速度排序:CPU >> 内存 >> I/O设备

3、为了了平衡这三者的速度差异,做了了如下优化:

1) CPU 增加了了缓存,以均衡内存与CPU的速度差异

2)操作系统增加了了进程、线程,以分时复⽤用CPU,进⽽而均衡I/O设备与CPU的速度差异;

3) 编译程序优化指令执⾏行行次序,使得缓存能够得到更更加合理理地利利⽤用。

我经常说的一句话、看问题一定看到问题的本质,根源,你才能将这个设计或者概念深入骨髓。才能从灵魂上驾驭它!玩弄她

 

2、再说重排序

定义:在执行程序时为了提高性能,编译器和处理器常常会对执行做重排序。及代码的执行顺序重新排序执行。主要包括三个层面的重排序:

编译器重排序

指令级重排序

处理器重排序

说得俗气点就是为了提高代码执行的效率而做的优化,优化,优化,重要的事情说三遍。

 

四、原子性和可见性

原⼦性是指⼀个操作或多个操作要么全部执行并且执行的过程不会被任何因素打断,要么 都不执行。性质和数据库中事务⼀样,⼀组操作要么都成功,要么都失败。看下⾯⼏个简单例⼦来理解原子性:

 

1 i=0; //1 

2 j=i; //2 

3 i++; //3 

4 i=j+1; //4

在看答案之前,可以先思考⼀一下上⾯面四个操作,哪些是原⼦子操作?哪些是⾮非原⼦子操作? 答案揭晓:

1——是:在Java中,对基本数据类型的变量赋值操作都是原⼦性操作(Java 有八⼤基本数据类型) 

2——不是:包含两个动作:读取 i 值,将 i 值赋值给 j

3——不是:包含了三个动作:读取 i 值,i+1,将 i+1 结果赋值给 i

4——不是:包含了三个动作:读取 j 值,j+1,将 j+1 结果赋值给 i

 

也就是说,只有简单的读取、赋值(⽽且必须是将数字赋值给某个变量,变量之间的相互 赋值不是原⼦操作)才是原⼦操作。

:由于以前的操作系统是 32 位, 64 位数据(long 型,double 型)在 Java 中是 8 个字 节表示,一共占⽤用64 位,因此需要分成两次操作采⽤完成一个变量的赋值或者读取操作。随着 64 位操作系统越来越普及,在 64 位的 HotSpot JVM 实现中,对64 位数据 (long 型,double 型)做原⼦性处理(由于 JVM 规范没有明确规定,不排除别的 JVM 实现还是按照 32 位的方式处理)。

 

在单线程环境中我们可以认为上述步骤都是原子性操作,但是在多线程环境下,Java 只保证了了上述基本数据类型的赋值操作是原⼦性的,其他操作都有可能在运算过程中出现错误。为此在多线程环境下为了保证⼀些操作的原⼦性引⼊了锁和 synchronized 等关键字。

 

上⾯说到 volatile 关键字保证了变量的可⻅性,不保证原⼦性。原⼦性已经说了,下⾯说 下可见性。

 

可见性其实和 Java 内存模型的设定有关:Java 内存模型规定所有的变量都是存在主存 (线程共享区域)当中,每个线程都有自⼰的⼯工作内存(私有内存)。线程对变量的所有操作都必须在工作内存中进⾏,⽽不直接对主存进行操作。并且每个线程不能访问其他线 程的⼯作内存。

举个简单栗⼦:

1 ⽐如上面 i++ 操作,在 Java 中,执行 i++ 语句:

2 执行线程⾸先从主存中读取 i(原始值)到⼯作内存中,然后在⼯作内存中执⾏运算 +1

3

4 操作(主存的 i 值未变),最后将运算结果刷新到主存中。

 

数据运算是在执行线程的私有内存中进行的,线程执⾏完运算后,并不一定会⽴即将运算结果刷新到主存中(虽然最后一定会更新主存),刷新到主存动作是由 CPU ⾃⾏选择⼀一个合适的时间触发的。假设数值未更新到主存之前,当其他线程去读取时(而且优先读取的是工作内存中的数据⽽⾮主存),此时主存中可能还是原来的旧值,就有可能导致运算结果出错。

以下代码是测试代码:

1 public class VolatileDemo4 {

2     private boolean flag = false;

3     class ThreadOne implements Runnable {

4         @Override

5        public void run() {

6            while (!flag) {

7                 System.out.println("执⾏行行操作");

8                 try {

9                     Thread.sleep(1000L);

10                 } catch (InterruptedException e) {

11                     e.printStackTrace();

12                 }

13             }

14             System.out.println("任务停⽌止");

15         }

16     }

17     class ThreadTwo implements Runnable {

18         @Override

19         public void run() {

20             try {

21                 Thread.sleep(2000L);

22                 System.out.println("flag 状态改变");

23                 flag = true;

24             } catch (InterruptedException e) {

25                 e.printStackTrace();

26             }

27         }

28     }

29     public static void main(String[] args) {

30         VolatileDemo4 testVolatile = new VolatileDemo4();

31         Thread thread1 = new Thread(testVolatile.new ThreadOne());

32         Thread thread2 = new Thread(testVolatile.new ThreadTwo());

33         thread1.start();

34         thread2.start();

35     }

36 }

上述结果有可能在线程 2 执行完 flag = true 之后,并不能保证线程 1 中的 while 能立即停止循环,原因在于 flag 状态首先是在线程 2 的私有内存中改变的,刷新到主存的时机不固定,⽽且线程 1 读取 flag 的值也是在自⼰的私有内存中,而线程 1 的私有内存中 flag 仍为 false,这样就有可能导致线程仍然会继续 while 循环。运行结果如下:

执行操作

执行操作

执行操作

flag 状态改变

任务停⽌止

 

避免上述不可预知问题的发⽣就是⽤ volatile 关键字修饰 flag,volatile 修饰的共享变量 可以保证修改的值会在操作后立即更新到主存里面,当有其他线程需要操作该变量时,不是从私有内存中读取,⽽是强制从主存中读取新值。即一个线程修改了某个变量的值,这新值对其他线程来说是⽴即可见的。

 

由于篇幅原因,未完待续

 

posted @   xmx测试员  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示