李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
24.并发编程之可见性介绍
Leefs
2022-10-27 PM
1163℃
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
NLP
4
标签云
国产数据库改造
设计模式
Flink
人工智能
数据结构
Java
Spring
数据结构和算法
链表
DataX
Quartz
Nacos
CentOS
散列
Ubuntu
Spark RDD
JavaSE
ClickHouse
SpringCloudAlibaba
Elastisearch
并发线程
Linux
递归
正则表达式
SpringCloud
Scala
DataWarehouse
MyBatis-Plus
Flume
Tomcat
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
评论已关闭