JUC编程04

JUC编程

15、JMM

Java虚拟机提供的轻量级的同步机制

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

JMM:Java内存模型

关于JMM的一些同步的约定:

  1. 线程解锁前,必须把共享变量立刻刷回
  2. 线程加锁前,必须读取主存中的最新值到工作内存中
  3. 必须保证加锁和解锁是同一把锁

一、JMM是什么

JMM = Java Memory Model(Java内存模型)

它不是一块真实存在的内存,也不是 JVM 的内存结构图,而是一套"并发读写内存时必须遵守的规则"。

一句话理解:JMM规定了,多个线程如何"看见"内存/如何读写共享变量、以及在什么条件下一个线程的修改对另一个线程可见。

二、为什么需要JMM

因为多线程 + CPU 缓存 + 指令重排序会带来三大问题:

  1. 可见性问题:一个线程改了变量,另一个线程看不到。
  2. 原子性问题:一个操作被拆成多步,中途被别的线程打断
  3. 有序性问题 :为了性能,编译器 / CPU 会 重排序指令

JMM就是为了:在不牺牲太多性能的前提下,给 Java 程序员一个"可预期"的并发语义。

三、JMM 的核心抽象:主内存 & 工作内存

JMM 把内存抽象成两层(模型)

  • 主内存:所有线程共享
  • 工作内存:每个线程私有(可理解为 CPU 缓存 + 寄存器)

线程不能直接操作主内存

线程只能先把变量拷贝到工作内存,再操作

四、JMM的八种内存操作(重点)

JMM 用 8种操作 来描述线程与主内存之间的交互。

操作 作用对象 含义
lock 主内存 把主内存中的变量加锁
unlock 主内存 释放锁
read 主内存→ 从主内存读取变量的值
load →工作内存 把read的值放入线程工作内存
use 工作内存→CPU 线程真正使用变量(参与计算)
assign CPU→工作内存 给工作内存中的变量赋新值
store 工作内存→ 把工作内存中的值准备写回主内存
write →主内存 把store的值写入主内存

五、JMM的核心规定(常考)

  1. 不允许操作乱序
    • read后必须load
    • store后必须write
    • 不能单独readwrite
  2. 工作内存不能凭空产生变量
    • 变量必须来自主内存
    • 不允许"无中生有"的 assign
  3. assign 后必须同步回主内存
  4. 线程不能直接操作主内存
    • 所有变量操作必须通过工作内存
    • 线程之间 不能互相访问对方的工作内存
  5. lock / unlock 的约束
    • 一个变量同一时刻 只能被一个线程 lock
    • lock 会清空工作内存中该变量的旧值
    • unlock 前,必须先把修改同步回主内存
  6. unlock 不能解锁没 lock 的变量
  7. volatile 的特殊规则
    • volatile 写:立即写回主内存
    • volatile 读:直接从主内存读
    • 禁止指令重排序(内存屏障)

16、volatile

一、volatile是什么

volatile是Java提供的一个 轻量级并发关键字 ,用于修饰 共享变量

java 复制代码
volatile boolean flag = true;

volatile三大特性

  1. 保证共享变量在多线程之间的可见性
  2. 禁止指令重排 (通过内存屏障保证有序性);
  3. 不保证原子性

二、如何保证可见性

  1. 什么是可见性:

    一个线程对共享变量的修改,能否被其他线程立即看到

  2. 可见性问题是怎么产生的?

    原因不是Java,而是 CPU缓存机制

    • 每个线程都有工作内存(CPU缓存)
    • 线程读写的往往是"缓存副本",不是主内存
    java 复制代码
    public class VolatileTest {
        // 加 volatile可以保证可见性
        // private static volatile int num = 0;
        private static boolean flag = true;
        public static void main(String[] args) {
            new Thread(()->{
                int i = 0;
                while(flag) {
                }
            }, "A").start();
    
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
    
            flag = false;
            System.out.println(flag);
        }
    }

    如果flag不是 volatile:

    • 线程 A 可能一直使用缓存中的true
    • 永远跳不出循环
  3. volatile如何保证可见性?

    JMM 明确规定:

    • 写 volatile 变量 → 立即刷新到主内存
    • 读 volatile 变量 → 强制从主内存中读取

    不允许线程使用"旧缓存"

理论依据(happens-before)

在JMM中,A happens-before B,并不是"时间先后",而是:

  • A 的结果对 B 可见
  • A的执行顺序不能被重排到B之后

JMM规定:对一个 volatile 变量的写 happens-before 任意后续对该 volatile 变量的读

三、不保证原子性

什么是原子性:一个操作要么一次完成,要么完全不发生,中途不能被打断

java 复制代码
volatile boolean count = 0;
count++;
  • 加了 volatile 并不保证线程安全!

为什么 volatile不保证原子性?

count++实际上是三步操作:

  1. 读取 count
  2. +1
  3. 写回 count

即使count是 volatile:线程A、B仍可能同时读到同一个旧值,更新结果被覆盖。

volatile 只能保证:每一步读写是可见的,但三步不能合成一个原子操作

结论

volatile 只能保证"单次读 / 单次写"的原子性,不能保证"复合操作"的原子性

需要使用synchronized或者AtomicInteger解决原子性问题

四、禁止指令重排

什么是指令重排:为了提升性能,编译器和 CPU 允许在不影响单线程结果的前提下调整指令顺序

这是正常且必要的优化

指令重排在多线程中的问题

java 复制代码
a = 1;
flag = true;

可能会被重排为:

java 复制代码
flag = true;
a = 1;

另一线程看到:

java 复制代码
if(flag) {
    // a还是0
}

这就是有序性问题

volatile如何禁止指令重排

volatile写:

  • volatile写之前的普通写
  • 不能被重排到volatile写之后

volatile读:

  • volatile读之后的普通读
  • 不能被重排到volatile读之前

实现原理:内存屏障

volatile在读写处插入内存屏障(Memory Barrier):

  • 限制指令执行顺序
  • 保证前后的操作"看起来有序"

⚠️注意:volatile 不是禁止所有重排,而是禁止"跨 volatile 变量"的重排

17 单例模式

一、什么是单例模式

单例模式保证一个类在整个程序运行期间只有一个实例,并提供一个全局访问点。

核心两点:

  • 只能创建一个对象
  • 外部可以随时拿到这个对象

二、为什么要用单例模式

常见使用场景

  • 配置类(Config)
  • 线程池(ThreadPool)
  • 日志系统(Logger)
  • 缓存管理器
  • 连接池管理器

目的总结

目的 说明
节省资源 避免重复创建重量级对象
统一状态 全局共享同一份数据
控制访问 防止对象被随意 new

三、单例模式的基本结构

java 复制代码
class SingleTon {
    // 1. 私有构造方法
    private SingleTon() {}

    // 2. 私有静态实例
    private static SingleTon instance;

    // 3. 公共静态获取方法
    public static SingleTon getInstance() {
        if(instance == null) {
            instance = new SingleTon();
        }
        return instance;
    }
}

四、单例模式的几种常见实现方法(重点)

单例模式的设计目标

单例模式要同时解决三个问题

  1. 唯一性:整个JVM只存在一个实例
  2. 可访问性:提供全局访问点
  3. 并发安全性:多线程下不产生多个实例

不同实现方式,本质上是在这三点之间取不同的平衡

1、饿汉式(Eager Initialization)
  1. 实现方式

    java 复制代码
    class SingleTon02 {
        private SingleTon02() {}
    
        private final static SingleTon02 INSTANCE = new SingleTon02();
        
        public static SingleTon02 getInstance() {
            return INSTANCE;
        }
    }
  2. 实现原理(非常重要)

    • 实例在类加载阶段就创建
    • Java的类加载过程由JVM保证线程安全
    • 同一个类在JVM中只会被加载一次

    因此:类加载 = 天然的同步屏障

  3. 特点总结

    方面 表现
    线程安全 ✅(JVM 保证)
    懒加载
    实现复杂度
    性能 ⭐⭐⭐⭐
  4. 优缺点

    优点

    • 实现最简单
    • 没有并发问题

    缺点

    • 不管用不用,类一加载就创建对象
    • 如果实例很"重",会浪费资源
  5. 适用场景

    • 实例创建成本低
    • 启动即需要使用
    • 对资源不敏感的工具类
2、饿汉式(Lazy Initialization) - 线程不安全
  1. 实现方法

    java 复制代码
    class SingleTon03 {
        private SingleTon03() {}
        
        private static SingleTon03 instance;
        
        public static SingleTon03 getInstance() {
            if(instance == null) {
                instance = new SingleTon03();
            }
            return instance;
        }
    }
  2. 实现原理

    • 第一次调用时才创建对象
    • 依赖普通的if判断

    线程不安全!

    多线程下可能发生:

    markdown 复制代码
    线程 A:if (instance == null) → true
    线程 B:if (instance == null) → true
    线程 A:new Singleton()
    线程 B:new Singleton()

    违背"唯一性"

  3. 特点总结

    方面 表现
    线程安全
    懒加载
    推荐程度
  4. 结论

    只存在于"教材"和"反例"中,实际开发中不使用。

3、同步方法懒汉式 - 线程安全但性能较差
  1. 实现方式

    java 复制代码
    class SingleTon04 {
        private SingleTon04() {}
    
        private static SingleTon04 instance;
    
        public static synchronized SingleTon04 getInstance() {
            if(instance == null) {
                instance = new SingleTon04();
            }
            return instance;
        }
    }
  2. 实现原理

    • 利用synchronized:同一时间只有一个线程进入方法
    • 保证instance创建过程互斥
  3. 特点总结

    方面 表现
    线程安全
    懒加载
    性能 ❌(每次调用都加锁)
  4. 结论

    实际上:

    • 只有第一次创建需要同步
    • 但后续每次访问都要加锁

    锁的粒度过大,性价比低

    能用,但几乎没有使用价值

4、双重检查锁(DCL + volatile)- 重点
  1. 实现方式

    java 复制代码
    class SingleTon05 {
        private SingleTon05() {}
    
        private static volatile SingleTon05 instance;
    
        public static SingleTon05  getInstance() {
            if(instance == null) {
                synchronized (SingleTon05.class) {
                    if(instance == null) {
                        instance = new SingleTon05();
                    }
                }
            }
            return instance;
        }
    }
  2. 实现原理

    为什么要"双重检查"?(重要)

    • 外层if:避免不必要的同步(提高性能)
    • 内层if:防止多个线程同时创建实例

    为什么必须加volatile

    如果 DCL 中没有使用 volatile,对象可能在"尚未完全初始化"时就被其他线程看到并使用,从而导致程序在运行期出现不可预测的错误,包括空指针、状态错乱、违反不变量等严重问题。

    对象创建不是原子操作,可能被重排:

    markdown 复制代码
    正常顺序:
    1.分配内存
    2.初始化对象
    3.instance指向内存
    
    可能被重排为:
    1 → 3 → 2

    结果:

    • 其他线程看到instance != null
    • 但对象还没初始化完成

    volatile的作用:

    • 禁止指令重排
    • 保证可见性
  3. 特点总结

    方面 表现
    线程安全
    懒加载
    性能 ⭐⭐⭐⭐
    理解难度 ⭐⭐⭐⭐
  4. 总结

    工程可用,但代码复杂、容易写错

5、静态内部类(最推荐方案)
  1. 实现方法

    java 复制代码
    class SingleTon06 {
        private SingleTon06() {}
        
        private static class Holder {
            private final static SingleTon06 INSTANCE = new SingleTon06();
        }
        
        public static SingleTon06 getInstance() {
            return Holder.INSTANCE;
        }
    }
  2. 实现原理

    • 外部类加载时:不会加载静态内部类
    • 第一次调用getInstance()
      • 触发Holder类加载
      • JVM在类加载阶段创建INSTANCE
    • JVM保证类加载线程安全
  3. 特点总结

    方面 表现
    线程安全 ✅(类加载)
    懒加载
    性能 ⭐⭐⭐⭐⭐
    代码复杂度 ⭐⭐
  4. 总结

    综合最优解,实际项目中最推荐

6、枚举单例(最安全)
  1. 实现方式

    java 复制代码
    enum SingleTon07 {
        INSTANCE;
    }
  2. 实现原理

    • 枚举本质:由JVM在类加载阶段创建
    • JVM:
      • 保证线程安全
      • 防止反射创建
      • 防止反序列化破坏
  3. 特点总结

    方面 表现
    线程安全
    反射攻击
    反序列化
    灵活性
  4. 总结

    理论上最安全,但工程灵活性一般

7、实现方法对比
实现方式 线程安全 懒加载 推荐度
饿汉式 ⭐⭐⭐
懒汉式
同步方法 ⭐⭐
DCL + volatile ⭐⭐⭐⭐
静态内部类 ⭐⭐⭐⭐⭐
枚举 ⭐⭐⭐⭐

单例模式的核心在于"唯一实例 + 线程安全"。在所有实现方式中,静态内部类是工程上最推荐的方案,而 DCL + volatile 是并发原理考察的重点。

相关推荐
好家伙VCC2 小时前
### WebRTC技术:实时通信的革新与实现####webRTC(Web Real-TimeComm
java·前端·python·webrtc
南极星10052 小时前
蓝桥杯JAVA--启蒙之路(十)class版本 模块
java·开发语言
消失的旧时光-19432 小时前
第十三课:权限系统如何设计?——RBAC 与 Spring Security 架构
java·架构·spring security·rbac
不能隔夜的咖喱3 小时前
牛客网刷题(2)
java·开发语言·算法
serve the people3 小时前
python环境搭建 (十二) pydantic和pydantic-settings类型验证与解析
java·网络·python
lekami_兰3 小时前
Java 并发工具类详解:4 大核心工具 + 实战场景,告别 synchronized
java·并发工具
有位神秘人3 小时前
Android中Notification的使用详解
android·java·javascript
tb_first4 小时前
LangChain4j简单入门
java·spring boot·langchain4j
独自破碎E4 小时前
【BISHI9】田忌赛马
android·java·开发语言