当前位置: 首页 > news >正文

JUC-JAVA内存模型

概念

JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。

JMM的意义

计算机硬件底层的内存结构过于复杂,JMM的意义在于避免程序员直接管理计算机底层内存,用一些关键字synchronized、volatile等可以方便的管理内存。

JMM 体现在以下几个方面

  • 原子性 - 保证指令不会受到线程上下文切换的影响

  • 可见性 - 保证指令不会受 cpu 缓存的影响

  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

 可见性

退不出的循环

先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

static boolean run = true;
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run){// ....}});t.start();sleep(1);run = false; // 线程t不会如预想的停下来
}

分析

  • 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。

  • 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中, 减少对主存中 run 的访问,提高效率  

  • 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量 的值,结果永远是旧值

解决方法

volatile(易变关键字)

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取 它的值,线程操作 volatile 变量都是直接操作主存

可见性 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 

注意: 

ynchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低 。

JMM关于synchronized的两条规定:

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值

   (注意:加锁与解锁需要是同一把锁)

模式之两阶段终止

在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。例如释放T2所持有的锁,关闭一些资源流

错误思路

  • 使用线程对象的 stop() 方法停止线程

    • stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁, 其它线程将永远无法获取锁

  • 使用 System.exit(int) 方法停止线程

    • 目的仅是停止一个线程,但这种做法会让整个程序都停止

两阶段终止模式

利用 isInterrupted  

 interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行,但在打断之后,打断标记会清零

class TPTInterrupt {private Thread thread;public void start(){thread = new Thread(() -> {while(true) {Thread current = Thread.currentThread();if(current.isInterrupted()) {log.debug("料理后事");break;}try {Thread.sleep(1000);log.debug("将结果保存");} catch (InterruptedException e) {//打断sleep线程会清除打断标记,所以要添加标记current.interrupt();}// 执行监控操作 }},"监控线程");thread.start();}public void stop() {thread.interrupt();}
}

利用volatile修饰的停止标记

停止标记用 volatile 是为了保证该变量在多个线程之间的可见性, 我们的例子中,即主线程把它修改为 true 对 t1 线程可见,所以t1线程会正常退出。

class TPTVolatile {private Thread thread;private volatile boolean stop = false;public void start(){thread = new Thread(() -> {while(true) {Thread current = Thread.currentThread();if(stop) {log.debug("料理后事");break;}try {Thread.sleep(1000);log.debug("将结果保存");} catch (InterruptedException e) {}// 执行监控操作}},"监控线程");thread.start();}public void stop() {stop = true;//让线程立即停止而不是等待sleep结束thread.interrupt();}
}

模式之 Balking 

定义

Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做 了,直接结束返回

实现

public class MonitorService {// 用来表示是否已经有线程已经在执行启动了private volatile boolean starting;public void start() {log.info("尝试启动监控线程...");synchronized (this) {if (starting) {return;}starting = true;}//其实synchronized外面还可以再套一层if,或者改为if(!starting),if框后直接return// 真正启动监控线程...}
}

当前端页面多次点击按钮调用 start 时

[http-nio-8080-exec-1] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(false)
[http-nio-8080-exec-1] cn.itcast.monitor.service.MonitorService - 监控线程已启动...
[http-nio-8080-exec-2] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true)
[http-nio-8080-exec-3] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true)
[http-nio-8080-exec-4] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true)

它还经常用来实现线程安全的单例

public final class Singleton {private Singleton() {}private static Singleton INSTANCE = null;public static synchronized Singleton getInstance() {if (INSTANCE != null) {return INSTANCE;}INSTANCE = new Singleton();return INSTANCE;}
}


http://www.mrgr.cn/news/16390.html

相关文章:

  • Java面试题真题·技术面试题部分总结
  • 平台介绍-机构、岗位、人员基础数据
  • 有宠物用哪个牌子的宠物空气净化器,希喂、IAM哪个更值得推荐
  • 【3D目标检测】MMdetection3d——nuScenes数据集训练BEVFusion
  • ubuntu24下python3.9安装pytorch
  • 和星辰为伴,与代码共舞
  • 【Centos】yum 安装软件失败时,切换 Aliyun 镜像源
  • 【Kubernetes部署篇】二进制搭建K8s高可用集群1.26.15版本
  • 5.11 飞行控制——定点飞行
  • 深入详解 C# 中的 Task.Run 与 Thread 的区别与联系
  • sicp每日一题[1.39]
  • 自建一款开源音乐服务-Navidrome
  • [YM]课设-C#-WebApi-Vue-员工管理系统 (五)登录
  • 二叉树的相关oj题目 — java实现
  • 最短路算法详解(Dijkstra 算法,Bellman-Ford 算法,Floyd-Warshall 算法)
  • 黄力医生谈健康:掌握这几个秘诀,帮你远离冠心病困扰!
  • Java技术栈 —— Spark入门(三)之实时视频流
  • 算法训练第28天|509. 斐波那契数|70. 爬楼梯|746. 使用最小花费爬楼梯
  • 蜂鸣器奏乐
  • 代码随想录算法训练营第五十九天 | 图论part09