Java 内存模型
Java 内存模型
1、JMM:Java Memory Model
2、定义主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等
3、JMM 体现
(1)原子性:保证指令不会受到线程上下文切换的影响
(2)可见性:保证指令不会受 CPU 缓存的影响
(3)有序性:保证指令不会受 CPU 指令并行优化的影响
可见性
1、volatile
(1)修饰成员变量、静态成员变量
(2)作用:避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
(3)保证可见性:在多个线程之间,一个线程对 volatile 变量的修改,对另一个线程可见
(4)不能保证原子性,仅用在一个写线程,多个读线程的情况
2、synchronized
(1)既可以保证代码块的原子性,也同时保证代码块内变量的可见性
(2)缺点:属于重量级操作,性能相对更低
两阶段终止模式
1、使用 isInterrupted、interrupt 实现
(1)缺点:可能出现 InterruptedException,打断标记容易遗漏、出错
(2)使用 volatile 改进
2、volatile 实现示例
(1)缺点:在 start() 中,没有限制创建监控线程,因为监控线程只需一个
class Volatile {
//监控线程
private Thread thread;
//停止标记
private volatile boolean stop = false;
//启动监控线程
public void start(){
thread = new Thread(() -> {
while(true) {
Thread current = Thread.currentThread();
if(stop) {
//线程被打断,则执行退出的前置操作
break;
}
try {
//监控间隔
Thread.sleep(1000);
} catch (InterruptedException e) {
}
// 执行监控操作
}
},"monitor");
thread.start();
}
//停止监控线程
public void stop() {
stop = true;
//若在sleep期间,stop设置为true,该方法可以在避免等待剩余sleep时间
thread.interrupt();
}
}
同步模式:Balking
1、Balking(犹豫)模式使用在一个线程,发现另一个线程或本线程,已经执行某一件相同的事,则本线程就无需再执行,直接结束返回
2、改进两阶段终止模式
class Volatile {
//监控线程
private Thread thread;
//停止标记
private volatile boolean stop = false;
//表示是否有线程在执行启动
private volatile boolean starting = false;
//启动监控线程
public void start(){
//尝试启动监控线程,synchronized避免多线程进入
synchronized (this) {
if (starting) {
return;
}
starting = true;
}
thread = new Thread(() -> {
while(true) {
Thread current = Thread.currentThread();
if(stop) {
//线程被打断,则执行退出的前置操作
break;
}
try {
//监控间隔
Thread.sleep(1000);
} catch (InterruptedException e) {
}
// 执行监控操作
}
},"monitor");
thread.start();
}
//停止监控线程
public void stop() {
stop = true;
starting = false;
//若在sleep期间,stop设置为true,该方法可以在避免等待剩余sleep时间
thread.interrupt();
}
}
3、应用:实现线程安全的单例
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;
}
}
指令级并行概念
1、Clock Cycle Time
(1)时钟周期时间
(2)等于主频的倒数,CPU 能够识别的最小时间单位
(3)如:4G 主频的 CPU 的 Clock Cycle Time 为 0.25 ns
2、CPI
(1)Cycles Per Instruction
(2)有的指令需要更多的时钟周期时间,所以引出指令平均时钟周期数
3、IPC
(1)Instruction Per Clock Cycle, 即 CPI 的倒数
(2)表示每个时钟周期能够运行的指令数
4、程序的 CPU 执行时间
(1)user + system 时间
(2)程序 CPU 执行时间 = 指令数 * CPI * Clock Cycle Time
指令重排序优化
1、现代 CPU 会设计为一个时钟周期,完成一条执行时间最长的 CPU 指令
2、指令可再划分成更小的阶段,如:取指令 -> 指令译码 -> 执行指令 -> 内存访问 -> 数据写回
(1)instruction fetch (IF)
(2)instruction decode (ID)
(3)execute (EX)
(4)memory access (MEM)
(5)register write back (WB)
3、在不改变程序结果的前提下,这些指令的各个阶段,可以通过重排序、组合,来实现指令级并行
4、现代 CPU 支持多级指令流水线
(1)如:支持同时执行:取指令 -> 指令译码 -> 执行指令 -> 内存访问 -> 数据写回,为五级指令流水线
(2)CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC = 1
(3)本质上,流水线技术并不能缩短单条指令的执行时间,但提高指令的吞吐率
5、SuperScalar 处理器
(1)大多数处理器包含多个执行单元,并不是所有计算功能都集中在一起,可以再细分为整数运算单元、浮点数运算单元等
(2)可以把多条指令做到并行获取、译码等
(3)CPU 可以在一个时钟周期内,执行多于一条指令,IPC > 1
6、禁止指令重排:volatile
volatile
1、底层实现原理是内存屏障,Memory Barrier(Memory Fence)
(1)对 volatile 变量的写指令后会加入写屏障
(2)对 volatile 变量的读指令前会加入读屏障
2、保证可见性
(1)写屏障(sfence)保证在该屏障之前,对共享变量的改动,都同步到主存当中
(2)读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
3、保证有序性
(1)写屏障确保指令重排序时,不会将写屏障之前的代码,排在写屏障之后
(2)读屏障确保指令重排序时,不会将读屏障之后的代码,排在读屏障之前
(3)注意:volatile 不能解决指令交错
(4)写屏障只保证之后的读,能够读到最新的结果,但不能保证其它线程的读在本线程前
(5)有序性的保证只保证本线程内相关代码不被重排序
4、更底层是读写变量时,使用 lock 指令,保证多核 CPU 之间的可见性、有序性
5、synchronized 不能禁止代码块内的指令重排
(1)但可以保证其有序性
(2)原因:synchronized 保证单线程执行代码块,不会出现多线程下的指令重排问题
(3)即有序性建立在原子性基础上
happens-before
1、规定哪些写操作对其它线程的读操作可见,是可见性与有序性的一套规则总结
2、抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见
3、以下变量都是指成员变量 / 静态成员变量
(1)t1 线程释放锁对象之前,对变量的写,对于接下来对 m 加锁的 t2 线程,对该变量的读可见
static int x;
static Object m = new Object();
new Thread(()->{
synchronized(m) {
x = 10;
}
},"t1").start();
new Thread(()->{
synchronized(m) {
System.out.println(x);
}
},"t2").start();
(2)t1 线程对 volatile 变量的写,对接下来 t2 线程,对该变量的读可见
volatile static int x;
new Thread(()->{
x = 10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
(3)t1 线程 start 前,对变量的写,对 t1 线程开始后,对该变量的读可见
static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},"t1").start();
(4)t1 线程结束前,对变量的写,对主线程得知 t1 结束后的读可见(如:主线程调用 t1 的 isAlive() 或 join(),等待它结束)
static int x;
Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
(5)线程 t1 打断(interrupt)t2 线程前,对变量的写,对于 t3 线程得知 t2 被打断后,对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted)
static int x;
public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
}
}
},"t2");
t2.start();
new Thread(()->{
sleep(1);
x = 10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);
}
(6)对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
volatile static int x;
static int y;
new Thread(()->{
y = 10;
x = 20;
},"t1").start();
new Thread(()->{
System.out.println(x);
System.out.println(y);
},"t2").start();
(7)具有传递性,如果 x happens-before -> y 并且 y happens-before -> z,那么有 x happens-before -> z
单例模式的线程安全
1、饿汉式(静态常量)
//final,防止被继承,子类方法破坏单例
public final class Singleton implements Serializable {
//private,防止直接创建,破坏单例
//不能防止反射创建新的实例
private Singleton() {}
//静态常量,JVM在类加载阶段,保证其创建时的线程安全
private static final Singleton INSTANCE = new Singleton();
/*
提供静态方法,而不是直接将 INSTANCE 设置为 public
1、更好的封装性
2、可以改进为懒汉式
3、对单例更好地控制
4、支持泛型
*/
public static Singleton getInstance() {
return INSTANCE;
}
//防止反序列化破坏单例
public Object readResolve() {
return INSTANCE;
}
}
2、枚举类
(1)枚举单例,底层通过静态常量,限制实例个数
(2)枚举单例为静态常量,JVM 保证在创建时的线程安全
(3)枚举单例不能被反射破坏单例
(4)枚举单例不能被反序列化破坏单例
(5)枚举单例属于饿汉式
(6)枚举单例可以加入构造方法,实现在创建时的初始化逻辑
enum Singleton {
INSTANCE;
}
3、懒汉式(静态内部类)
(1)类初始化是懒加载的
(2)初始化时机:首次访问类的静态变量 / 静态方法时
(3)使用 LazyHolder,才加载
(4)static final 修饰的引用类型不会在准备阶段赋值,而是在初始化阶段赋值
(5)类的初始化方法:<clinit>()
(6)虚拟机会保证一个类的 <clinit>() 在多线程环境中被正确地加锁、同步
(7)如果多个线程同时去初始化一个类,则只会有一个线程去执行这个类的 <clinit>(),其他线程都需要阻塞等待,直到活动线程执行 <clinit>() 完毕
(8)如果之前的线程成功加载了类,则等在队列中的线程,就没有机会再执行 <clinit>(),当需要使用这个类时,虚拟机会直接返回给它已经准备好的信息
public final class Singleton {
private Singleton() { }
// 问题1:属于懒汉式还是饿汉式
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 问题2:在创建时是否有并发问题
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战