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 是并发原理考察的重点。

相关推荐
OxYGC6 小时前
[AI工程] Spring AI 第十四篇:Agent 五种模式在 2.0 里怎么写
java·spring·ai·ai编程
code2cat6 小时前
【随笔】从聊天到调用工具:理解MCP在AI应用中的位置
java·人工智能
2501_933923257 小时前
Spring Bean作用域揭秘:单例、原型、请求与会话的区别
java·后端·spring·java-ee
学编程就要猛7 小时前
基于Spring AI 的智能聊天机器人
java·spring ai·chat robot
修炼室7 小时前
Java开发工程师笔试经验贴【高频知识】
java
木井巳7 小时前
【记忆化搜索】最长递增子序列
java·算法·leetcode·深度优先·剪枝·推荐算法
杨杨杨大侠7 小时前
知识库已经有了,Java 程序员还要做什么?Spring AI RAG 实战
java·openai·ai编程
伊信7 小时前
Spring AI 基础学习与应用
java·后端
夜郎king7 小时前
基于 Java + Playwright 实现网站自动访问与数据采集(以腾讯云开发者社区为例)
java·playwright·网页数据获取
Anastasiozzzz8 小时前
重新定义 Agent 基建:Redis 在现代 AI 与智能体系统中的工程实践
java·人工智能·redis·ai