← 返回软件技术总览 软件技术 · 知识点深化 · 线程安全设计模式:不可变、线程封闭、共享发布
软件技术 · 知识点深化 · 并发编程

线程安全设计模式:不可变、线程封闭、共享发布

写线程安全代码不只会加锁。不可变对象天生线程安全,线程封闭让数据根本不共享,安全发布保证对象初始化后才被别人看到。这一页把三种根本策略讲透,比死记 synchronized 有用得多。

① 怎么学(4 步走,约 60 分钟)

先建立直觉再抠细节,按这四步走最稳:

1看图建立直觉(10 分钟)
读②③:先在脑子里画出本课核心结构图。
2记完整体系(15 分钟)
读④:把对比表和公式看懂,不要急着背。
3跟例题走一遍(20 分钟)
精读⑤:看三个例题怎么用知识点解题。
4刷题纠错(剩余时间)
做⑦⑩,错题回⑥诊断。
本课小目标学完你要能:① 说出线程安全三大策略;② 解释不可变对象为什么安全;③ 区分 ThreadLocal 与 synchronized;④ 列举安全发布的几种方式。

② 一图看懂:线程安全三大策略

线程安全 不可变 Immutable final 字段,初始化后不变 线程封闭 Thread Confinement 数据不跨线程 安全发布 Safe Publication 初始化后再共享 锁/同步 兜底保护可变共享 应用:缓存/单例/状态机 高并发后端 易错:未安全发布的半初始化对象 DCL 缺 volatile
读法:中心是本课主题,四条分支展开核心维度,下方是典型应用与高频易错点。

③ 本质直觉:线程安全的根本是"别让人乱抢"

线程安全问题只发生在多线程共享可变数据的时候。所以解决思路有三个方向:

1. 让数据不可变:对象一旦创建就不能改,谁读都一样,根本不需要锁。像 String、Integer 都是不可变类。

2. 让数据别共享:每个线程自己一份,别人碰不到。ThreadLocal 就是这个思路——每个线程有自己的 Map,存自己的变量。

3. 必须共享时,控制好发布时机:对象还没初始化完别让别的线程看到。这就是 DCL 单例要 volatile 的原因。

锁是最后手段,不是第一选择。能不可变就不可变,能封闭就封闭,实在要共享再考虑加锁。

不可变天然安全 线程封闭不共享 安全发布初始化完再给 锁/同步(最后手段)
优先顺序不可变 > 线程封闭 > 安全发布 > 加锁。越靠前性能越好、bug 越少。

④ 完整知识体系

三大策略对比

策略原理典型例子
不可变所有字段 final,状态不修改String、Integer、LocalDate
线程封闭数据只在单线程内使用ThreadLocal、栈封闭
安全发布初始化完成后再共享静态枚举、volatile 引用
加锁互斥访问可变共享数据synchronized、Lock
不可变对象三要素所有字段 private final + 类不允许子类化 + 不暴露可变引用
ThreadLocal 结构每个 Thread 有一个 ThreadLocalMap,key 是 ThreadLocal 实例

安全发布的几种方式

① 静态初始化器里创建对象(JVM 保证);② volatile 引用或 AtomicReference;③ 放到 final 字段里(构造函数完成后);④ 放到线程安全容器(ConcurrentHashMap)。

ThreadLocal 内存泄漏

ThreadLocalMap 的 key 是弱引用,value 是强引用。线程池线程不销毁,value 一直存在。用完必须 remove()。

⑤ 应用场景与例题

例1 String 为什么是线程安全的?
多线程读 String 要加锁吗?
不需要。String 所有字段 final,创建后不可修改。任何线程看到的都是同一个不可变状态,没有竞态条件。
例2 ThreadLocal 为什么能避免锁?
每个线程一份变量,别人看不到。
ThreadLocal 为每个线程维护独立副本,A 线程 set 的值只有 A 能 get。天然不共享,不需要锁。典型场景:数据库连接、用户上下文。
例3 DCL 单例为什么是安全发布问题?
不加 volatile 会怎样?
new 对象三步重排可能导致引用先赋值、对象后初始化。另一个线程通过不安全发布拿到半初始化对象。volatile 保证初始化完成后引用才可见。
做题心法先问能不能不可变,再问能不能不共享,最后才考虑锁。

⑥ 高频错误诊断(4 条)

错误1:什么共享变量都加 synchronized过度同步降低性能。优先考虑不可变或线程封闭。
错误2:ThreadLocal 用完不 remove线程池复用线程时,value 残留导致内存泄漏和脏数据。
错误3:发布对象时没等初始化完构造函数里 this 逸出(在构造函数中启动线程引用 this)。
错误4:以为 final 字段就完全安全final 保证构造过程的安全发布,但如果引用的对象本身可变,仍需同步。

⑦ 考点真题演练(5 题)

考点分布

考法出题形式应对
不可变问为什么安全状态不变,无竞态
线程封闭问 ThreadLocal 原理每线程独立副本
安全发布问 DCL 为什么要 volatile防半初始化
内存泄漏问 ThreadLocal 为什么泄漏key 弱 value 强+线程池

真题basic1. 以下哪种对象天生线程安全?

真题mid2. ThreadLocal 用完后应该?

真题mid3. DCL 单例中 volatile 的作用是?

真题hard4. 以下哪个不是安全发布方式?

真题hard5. 不可变对象的必要条件不包括?

⑧ 必背知识点卡

三策略:不可变 > 封闭 > 发布 > 加锁
不可变:final 字段,状态不改 String/LocalDate
封闭:ThreadLocal 每线程副本 用完 remove
发布:初始化完再共享 DCL 要 volatile
锁:最后手段,别滥用
逸出:构造函数中 this 传给其他线程 危险
ThreadLocalMap:key 弱引用 value 强引用 线程池泄漏

⑨ 动手输出:设计一个用户上下文的线程安全方案

场景:Web 请求中要把当前用户 ID 传到下游各层,怎么设计?
① 选型:用 ThreadLocal 存 userId,每个请求线程独立。
② 为什么不用锁:请求间互不干扰,不需要共享,加锁反而慢。
③ 生命周期:请求进入时 set,请求结束时 finally remove。
④ 线程池风险:Tomcat 线程复用,不 remove 会串用户。
⑤ 备选:如果要跨线程池传递,用 TransmittableThreadLocal。
口述思路"数据不共享就不需要锁,ThreadLocal 是最优雅的封闭方案。"

⑩ 分层练习(基础 + 中档 + 拔高)

▍基础 6 题

基础1线程安全问题的根源是什么?
多线程共享可变数据。
基础2不可变对象为什么安全?
状态不变,无竞态条件。
基础3ThreadLocal 用完要做什么?
remove()。
基础4安全发布是什么意思?
对象初始化完成后再被其他线程看到。
基础5String 是可变还是不可变?
不可变。
基础6加锁是第几种策略?
最后手段。

▍中档 5 题

中档1ThreadLocal 怎么实现每线程副本?
每个 Thread 有 ThreadLocalMap,key 是 ThreadLocal。
中档2为什么构造函数里 this 逸出危险?
其他线程可能看到未初始化完的对象。
中档3volatile 算安全发布吗?
算,它保证可见性和有序性。
中档4栈封闭是什么?
局部变量在栈上,线程私有,天然安全。
中档5为什么优先用不可变?
不需要锁,性能好,没有竞态。

▍拔高 5 题

拔高1ThreadLocal 内存泄漏原理?
Entry key 是弱引用被 GC,value 强引用无法回收。
拔高2TransmittableThreadLocal 解决什么?
线程池线程复用后 ThreadLocal 不传递的问题。
拔高3final 字段的安全发布语义?
构造函数结束后 final 字段一定对其他线程可见。
拔高4为什么 StringBuilder 线程不安全而 StringBuffer 安全?
StringBuffer 加了 synchronized。
拔高5安全发布和不可变有什么区别?
不可变是对象本身状态不变;安全发布是把对象分享出去的时机正确。

⑪ 记忆口诀 + 7 天复习计划

三句口诀① 不可变来帮忙,封闭不共享。② 安全发布初始化完,volatile 挡住重排。③ ThreadLocal 用完 remove,构造函数别逸出。
天任务自检
第 1 天读②③,画三策略关系图优先级说清
第 2 天背策略表 + 基础 6 题能举例
第 3 天做中档 5 题,写一个 ThreadLocal 工具类代码正确
第 4 天做拔高 5 题,解释内存泄漏原理能画 Entry
第 5 天做⑦真题 5 题限时每题 2 分钟
第 6-7 天合书口述 DCL 为什么要 volatile不看资料

← 返回软件技术总览