李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
24.并发编程之可见性介绍
Leefs
2022-10-27 PM
530℃
0条
[TOC] ### 一、Java内存模型 JMM即Java Memory Model,它定义了**主存、工作内存抽象概念,**底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。 **JMM 体现在以下几个方面** - **原子性**:保证指令不会受到线程上下文切换的影响 - **可见性**:保证指令不会受 cpu 缓存的影响 - **有序性**:保证指令不会受 cpu 指令并行优化的影响 简单的说,**JMM** 定义了一套**在多线程读写共享数据时**(成员变量、数组)时,对数据的可见性、有序性和原子性的规则和保障。 ### 二、不可见性导致的问题 #### 示例 main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止 ```java import lombok.extern.slf4j.Slf4j; import static com.lilinchao.concurrent.utils.Sleeper.sleep; /** * Created by lilinchao * Date 2022/10/27 * Description 退不出的循环 */ @Slf4j(topic = "c.Test06") public class Test06 { static boolean run = true; public static void main(String[] args) { Thread t = new Thread(() -> { while (run){ } }); t.start(); sleep(1); log.debug("停止t线程..."); run = false; // 线程t不会如预想的停下来 } } ``` **运行结果** ![24.并发编程之可见性介绍01.jpg](https://lilinchao.com/usr/uploads/2022/10/3649050250.jpg) 从结果可以看出,当run变量为false后,t线程并未退出循环。 **问题分析** 1、初始状态,t线程刚开始从主内存读取了run 的值到工作内存。 ![24.并发编程之可见性介绍02.png](https://lilinchao.com/usr/uploads/2022/10/2159324095.png) 2、因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存 中,减少对主存中run的访问,提高效率 ![24.并发编程之可见性介绍03.png](https://lilinchao.com/usr/uploads/2022/10/4288564373.png) 3、1秒之后, main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变 量的值,结果永远是旧值 ![24.并发编程之可见性介绍04.png](https://lilinchao.com/usr/uploads/2022/10/498051245.png) ### 三、可见性 #### 3.1 volatile(易变关键字) 它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。 ```java import lombok.extern.slf4j.Slf4j; import static com.lilinchao.concurrent.utils.Sleeper.sleep; /** * Created by lilinchao * Date 2022/10/27 * Description 退不出的循环 -- volatile解决办法 */ @Slf4j(topic = "c.Test06") public class Test06 { volatile static boolean run = true; public static void main(String[] args) { Thread t = new Thread(() -> { while (run){ } }); t.start(); sleep(1); log.debug("停止t线程..."); run = false; } } ``` **运行结果** ``` 21:49:15.174 c.Test06 [main] - 停止t线程... ``` 线程t在1s后正常退出了循环。 **分析** 当主线程修改主存中的run变量的时候,t线程一直访问的是自己缓存的run值,所以不认为run已经改为false,顾不会退出循环。 当为主存(成员变量)进行volatile修饰,增加变量的可见性, 当主线程修改run为false, t线程对run的值可见,这样就可以正常退出循环。 #### 3.2 synchronized的可见性 synchronized的执行内部代码的过程分为五步,分别是: + 1、获得同步锁; + 2、清空工作内存; + 3、在主内存中拷贝最新变量的副本到工作内存; + 4、执行代码(计算或者输出等); + 5、将更改后的共享变量的值刷新到主内存中; + 6、 释放同步锁。 ```java import lombok.extern.slf4j.Slf4j; import static com.lilinchao.concurrent.utils.Sleeper.sleep; /** * Created by lilinchao * Date 2022/10/27 * Description 退不出的循环 -- synchronized解决办法 */ @Slf4j(topic = "c.Test07") public class Test07 { static boolean run = true; private final static Object lock = new Object(); public static void main(String[] args) { Thread t = new Thread(() -> { while (run) { synchronized (lock) { if (!run){ break; } } } }); t.start(); sleep(1); log.debug("停止t线程..."); synchronized (lock){ run = false; } } } ``` **运行结果** ``` 22:09:18.867 c.Test07 [main] - 停止t线程... ``` #### 3.3 print打印输出 当在while循环代码中加入print打印输出时,t线程会退出循环。 ``` import lombok.extern.slf4j.Slf4j; import static com.lilinchao.concurrent.utils.Sleeper.sleep; /** * Created by lilinchao * Date 2022/10/27 * Description 退不出的循环 -- print打印输出 */ @Slf4j(topic = "c.Test06") public class Test06 { static boolean run = true; public static void main(String[] args) { Thread t = new Thread(() -> { while (run){ System.out.print(""); } }); t.start(); sleep(1); log.debug("停止t线程..."); run = false; // 线程t不会如预想的停下来 } } ``` **运行结果** ``` 22:10:28.651 c.Test06 [main] - 停止t线程... ``` **分析** 从print的源码中可以看出,print方法使用到了`synchronized`,synchronized可以保证原子性、可见性、有序性。 ### 四、可见性 vs 原子性 前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线 程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况。 上例从字节码理解是这样的: ``` getstatic run // 线程 t 获取 run true getstatic run // 线程 t 获取 run true getstatic run // 线程 t 获取 run true getstatic run // 线程 t 获取 run true putstatic run // 线程 main 修改 run 为 false, 仅此一次 getstatic run // 线程 t 获取 run false ``` 比较一下之前讲线程安全时举的例子:两个线程一个 `i++` 一个 `i- -`,只能保证看到最新值,不能解决指令交错。 ``` // 假设i的初始值为0 getstatic i // 线程2-获取静态变量i的值 线程内i=0 getstatic i // 线程1-获取静态变量i的值 线程内i=0 iconst_1 // 线程1-准备常量1 iadd // 线程1-自增 线程内i=1 putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1 iconst_1 // 线程2-准备常量1 isub // 线程2-自减 线程内i=-1 putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1 ``` synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低。 *附参考原文地址* *《黑马程序员之并发编程》*
标签:
并发编程
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/2529.html
上一篇
23.同步模式之顺序控制
下一篇
25.并发编程之Balking模式
取消回复
评论啦~
提交评论
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
标签云
Yarn
JavaWeb
Java工具类
Ubuntu
Java
Spring
Redis
Sentinel
递归
GET和POST
Spark SQL
链表
容器深入研究
SpringCloudAlibaba
HDFS
SpringBoot
VUE
排序
Scala
数据结构和算法
DataX
稀疏数组
正则表达式
Beego
Shiro
查找
并发线程
Jquery
JVM
Spark
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞