原子性指"一组操作要么全都操作成功,要么全都失败,不能只操作成功其中的一部分"。java.util.concurrent.atomic 下的类是具有原子性的类,可原子性地执行添加、递增、递减等操作。如多线程下线程不安全的 i++ 问题,可用功能相同且线程安全的 getAndIncrement 方法解决。
原子类的作用与锁类似,是保证并发情况下线程安全。原子类相比锁有一定优势:
原子类共分为 6 类:
| 类型 | 具体类 |
|---|---|
| Atomic* 基本类型原子类 | AtomicInteger、AtomicLong、AtomicBoolean |
| Atomic*Array 数组类型原子类 | AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray |
| Atomic*Reference 引用类型原子类 | AtomicReference、AtomicStampedReference、AtomicMarkableReference |
| Atomic*FieldUpdater 升级类型原子类 | AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater |
| Adder 累加器 | LongAdder、DoubleAdder |
| Accumulator 积累器 | LongAccumulator、DoubleAccumulator |
Atomic* 基本类型原子类包括 AtomicInteger、AtomicLong、AtomicBoolean。
以 AtomicInteger 为例:它是 int 类型的封装,提供原子性的访问和更新。若需要整型变量且用于并发场景,可直接使用 AtomicInteger 而不使用基本类型 int 或包装类型 Integer,自动具备原子能力。
public final int get():获取当前的值。因它是 Java 类而非基本类型,获取值需通过 get 方法。
public final int getAndSet(int newValue):获取当前的值,并设置新的值。
public final int getAndIncrement():获取当前的值,并自增。
public final int getAndDecrement():获取当前的值,并自减。
public final int getAndAdd(int delta):获取当前的值,并加上预期的值。参数为想改变多少值,可正可负,正数增加、负数减少。getAndIncrement 和 getAndDecrement 修改数值默认为 +1 或 -1,getAndAdd 可一次性地加减任意数值。
boolean compareAndSet(int expect, int update):若输入的数值等于预期值,则以原子方式将该值更新为输入值(update)。此方法是 CAS 的一个重要体现。
Atomic*Array 数组类型原子类可保证数组里的每个元素原子性。AtomicIntegerArray 相当于把 AtomicInteger 聚合成一个数组。共 3 种:
AtomicReference 与 AtomicInteger 无本质区别:AtomicInteger 让整数保证原子性,AtomicReference 让对象保证原子性。AtomicReference 能力明显比 AtomicInteger 强,因为一个对象可包含很多属性。
此类别下除 AtomicReference 外还有:
Atomic*FieldUpdater 原子更新器共 3 种:
若已有变量(如整型 int)并不具备原子性,但已被定义,可用 Atomic*FieldUpdater 让已声明变量拥有 CAS 操作能力。这是非互斥同步手段,对已声明变量进行 CAS 操作达到同步目的。
既然想让变量具备原子性,为何不一开始就声明为 AtomicInteger?有以下情况:
代码示例:
public class AtomicIntegerFieldUpdaterDemo implements Runnable{
static Score math;
static Score computer;
public static AtomicIntegerFieldUpdater<Score> scoreUpdater = AtomicIntegerFieldUpdater
.newUpdater(Score.class, "score");
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
computer.score++;
scoreUpdater.getAndIncrement(math);
}
}
public static class Score {
volatile int score;
}
public static void main(String[] args) throws InterruptedException {
math =new Score();
computer =new Score();
AtomicIntegerFieldUpdaterDemo r = new AtomicIntegerFieldUpdaterDemo();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("普通变量的结果:"+ computer.score);
System.out.println("升级后的结果:"+ math.score);
}
}
代码演示类的用法:两个 Score 类型的实例 math 和 computer,Score 内部有分数 score;声明 AtomicIntegerFieldUpdater,构造时传入两个参数 Score.class(类名)和 "score"(属性名)。
run 方法对两个实例分别自加:
main 函数定义 math 和 computer,启动两个线程各执行 run 方法,两个 score 各被加 2000 次,join 等待后打印结果:
普通变量的结果:1942
升级后的结果:2000普通变量因不具备线程安全性,多线程操作时部分操作被冲突抵消,最终结果小于 2000;使用 AtomicIntegerFieldUpdater 可对普通类型 score 进行原子自加,结果与加的次数一致(2000)。
Adder 加法器有两种:LongAdder 和 DoubleAdder。
Accumulator 积累器有两种:LongAccumulator 和 DoubleAccumulator。这两种原子类会在后续节中展开介绍。
以 AtomicInteger 的 getAndAdd 方法为突破口,分析如何通过 CAS 操作实现并发下的累加操作。
该方法在 Java 1.8 中的实现:
//JDK 1.8实现
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
里面使用 Unsafe 类,调用 unsafe.getAndAddInt 方法。
Unsafe 类主要用于和操作系统打交道,大部分 Java 代码无法直接操作内存,必要时可利用 Unsafe 类与操作系统交互,CAS 正是利用到 Unsafe 类。
AtomicInteger 的重要代码:
public class AtomicInteger extends Number implements java.io.Serializable {
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
public final int get() {return value;}
...
}
数据定义部分获取 Unsafe 实例,定义 valueOffset。static 代码块在类加载时执行,调用 Unsafe 的 objectFieldOffset 方法得到当前原子类 value 的偏移量并赋给 valueOffset。valueOffset 含义是 value 在内存中的偏移地址,Unsafe 根据内存偏移地址获取数据原值,从而通过 Unsafe 实现 CAS。
value 用 volatile 修饰,是原子类存储值的变量,保证多线程间看到同一份 value,保证可见性。
Unsafe 的 getAndAddInt 方法实现:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
结构是 do-while 循环(死循环),直到满足退出条件才退出。
var5 = this.getIntVolatile(var1, var2):native 方法,获取 var1 中 var2 偏移处的值。传入两个参数:第一个是当前原子类,第二个是最初获取到的 offset,从而获取当前内存中偏移量的值并保存到 var5。此时 var5 代表当前时刻原子类的数值。
while 退出条件 compareAndSwapInt,传入 4 个参数 var1、var2、var5、var5 + var4,对应含义分别为 object、offset、expectedValue、newValue:
compareAndSwapInt 的作用:判断当前原子类 value 是否等于之前获取的 var5,相等则把 var5 + var4 更新上去,实现 CAS 过程。
CAS 操作成功则退出 while 循环;失败说明获取 var5 之后、CAS 操作之前 value 数值已变化,有其他线程修改过变量。此时再次执行循环体,重新获取 var5(最新原子变量数值),再次用 CAS 尝试更新,直到成功,故为死循环。
总结:Unsafe 的 getAndAddInt 通过循环 + CAS 实现,用 compareAndSwapInt 尝试更新 value,失败则重新获取再尝试,直到更新成功。
6 类原子类:Atomic* 基本类型、Atomic*Array 数组类型、Atomic*Reference 引用类型、Atomic*FieldUpdater 升级类型、Adder 加法器、Accumulator 积累器。已逐一介绍基本作用和用法,并以 AtomicInteger 为例分析利用 CAS 实现原子操作。从 getAndAdd 方法出发深入至 Unsafe 的 getAndAddInt 方法,其原理是利用自旋不停尝试直到成功。
参考:占小狼https://www.jianshu.com/p/fb6e91b013cc
JDK 1.5 新增并发情况下使用的 Integer/Long 对应原子类 AtomicInteger 和 AtomicLong。
并发场景实现计数器时,利用 AtomicInteger 和 AtomicLong 可避免加锁和复杂代码逻辑,只需执行封装好的方法(原子增、原子减)即可满足大部分业务场景需求。
但业务场景并发量很大时,这两个原子类存在较大性能问题。
示例代码:
/**
* 描述: 在16个线程下使用AtomicLong
*/
public class AtomicLongDemo {
public static void main(String[] args) throws InterruptedException {
AtomicLong counter = new AtomicLong(0);
ExecutorService service = Executors.newFixedThreadPool(16);
for (int i = 0; i < 100; i++) {
service.submit(new Task(counter));
}
service.shutdown();
Thread.sleep(2000);
System.out.println(counter.get());
}
static class Task implements Runnable {
private final AtomicLong counter;
public Task(AtomicLong counter) {
this.counter = counter;
}
@Override
public void run() {
counter.incrementAndGet();
}
}
}
代码新建原始值 0 的 AtomicLong,线程数为 16 的线程池,添加 100 次相同任务。Task 类每次调用 AtomicLong 的 incrementAndGet 方法(一次自加),作用是把原子类从 0 开始、100 个任务各自加一次。
运行结果毫无疑问是 100。虽多线程并发访问,AtomicLong 仍可保证 incrementAndGet 操作的原子性,不会发生线程安全问题。
深入看内部情景,简化成两个线程同时工作的并发场景(两个线程与更多线程本质上一样),如图所示:

图中每个线程运行在自己 core 中,各有独用的本地内存;本地内存下方是两个 CPU 核心共享的共享内存。
AtomicLong 内部的 value 属性(保存当前数值)被 volatile 修饰,需保证自身可见性。因此每次数值变化都需 flush 和 refresh:例如 ctr 初始为 0,core 1 改成 1 后,先把结果 flush 到下方共享内存,再 refresh 到 core 2 的本地内存,core 2 才能感知变化。
竞争激烈时,flush 和 refresh 耗费很多资源,且 CAS 经常失败。
JDK 8 新增 LongAdder,是针对 Long 类型的操作工具类。
示例(与上例类似,工具类从 AtomicLong 变为 LongAdder,打印方法从 get 变为 sum,其余逻辑相同):
/**
* 描述: 在16个线程下使用LongAdder
*/
public class LongAdderDemo {
public static void main(String[] args) throws InterruptedException {
LongAdder counter = new LongAdder();
ExecutorService service = Executors.newFixedThreadPool(16);
for (int i = 0; i < 100; i++) {
service.submit(new Task(counter));
}
service.shutdown();
Thread.sleep(2000);
System.out.println(counter.sum());
}
static class Task implements Runnable {
private final LongAdder counter;
public Task(LongAdder counter) {
this.counter = counter;
}
@Override
public void run() {
counter.increment();
}
}
}
运行结果同样是 100,但速度比 AtomicLong 实现更快。高并发下 LongAdder 比 AtomicLong 效率更高的原因:
LongAdder 引入分段累加概念,内部有两个参数参与计数:base(变量)和 Cell[](数组)。
LongAdder 把不同线程对应到不同 Cell 上修改,降低冲突概率,是分段理念,提高并发性,与 Java 7 ConcurrentHashMap 的 16 个 Segment 思想类似。
竞争激烈时,LongAdder 通过计算每个线程的 hash 值把线程分配到不同 Cell,每个 Cell 相当于独立计数器,Cell 之间不存在竞争关系。自加过程中大幅减少 flush、refresh,降低冲突概率,故吞吐量比 AtomicLong 大。本质是空间换时间,因有多个计数器同时工作,占用内存相对更大。
LongAdder 最终通过 sum 方法实现多线程计数,把各线程 Cell 累计求和并加 base 形成总和:
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
sum 方法先取 base 值,再遍历所有 Cell,把每个 Cell 值相加形成总和。因统计时未加锁,sum 不一定完全准确(计算过程中 Cell 值可能被修改)。
低竞争情况下,AtomicLong 和 LongAdder 吞吐量相似。竞争激烈时 LongAdder 预期吞吐量高得多,实验约为 AtomicLong 的十倍。代价是 LongAdder 高效的同时消耗更多空间。
不可完全替代,需区分场景。
LongAdder 只提供 add、increment 等简单方法,适合统计求和计数场景(场景单一);AtomicLong 还具有 compareAndSet 等高级方法,可应对除加减外更复杂的需 CAS 的场景。
结论:若场景仅需加减操作,可直接用更高效的 LongAdder;若需利用 CAS(如 compareAndSet)等操作,则需使用 AtomicLong。
疑问:LongAdder 最后相加时可能不准确,不也是线程不安全么,为何还要使用?
答案:https://www.cnblogs.com/thisiswhy/p/13176237.html 。简答:sum() 无锁地读取 base 与各 Cell 的瞬时快照,属于"弱一致性"设计——统计计数类场景通常不要求精确的瞬时值,牺牲强一致换来无锁高吞吐;若业务需要精确读数,应改用 AtomicLong 或加锁。
案例中有两个线程:

左上角有一个公共的 boolean flag 标记位,初始赋值为 true。线程 2 进入 while 循环,根据 flag 值决定是否继续执行或退出。
最初 flag 为 true,先执行一定时期循环。某时刻线程 1 把 flag 改为 false,期望线程 2 看到变化后停止运行。
这样做有风险:线程 2 可能不能立刻停止,也可能过一段时间才停止,极端情况下可能永远不停止。
理解原因需看 CPU 内存结构(双核 CPU 简单示意图):

线程 1 和线程 2 分别在不同 CPU 核心上运行,每个核心有自己的本地内存,下方有共享内存。最初都能读到 flag 为 true,但线程 1 改为 false 后线程 2 不能及时看到(线程 2 不能直接访问线程 1 的本地内存),这是典型的可见性问题。
可见性问题:线程 2 无法直接访问线程 1 的本地内存,若 flag 变化没有及时同步,线程 2 会一直读到旧值(缓存 stale 值),导致无法感知更新。
给变量加上
volatile后,每次修改都会立即刷新到主内存,且禁用指令重排序,保证其他线程立即可见。
在变量前加 volatile 关键字即可解决。加上该关键字后,每次变量被修改时其他线程对此都可见,线程 1 改变值后线程 2 可立刻看到,从而退出 while 循环。

加关键字后可拥有可见性,原因:线程 1 的更改会 flush 到共享内存,再 refresh 到线程 2 的本地内存,线程 2 能感受到变化。volatile 关键字最主要用来解决可见性问题,可一定程度上保证线程安全。
回顾多线程同时进行 value++ 的场景:

若初始化为每个线程加 1000 次,最终结果很可能不是 2000。value++ 不是原子的,多线程下会出现线程安全问题。使用 volatile 能否解决?

即便使用 volatile 也不能保证线程安全,因为这里的问题不单是可见性问题,还包含原子性问题。
有多种解决方法:
第 1 种是使用 synchronized 关键字:

两个线程不能同时更改 value 数值,保证 value++ 语句的原子性,且 synchronized 同样保证可见性:第 1 个线程修改 value 后,第 2 个线程可立刻看见本次修改。
第 2 种是使用原子类:

用 AtomicInteger,每个线程调用它的 incrementAndGet 方法。利用原子变量后无需加锁,该操作底层由 CPU 指令保证原子性,多线程同时运行也不会发生线程安全问题。
volatile 和原子类使用场景不同。若只有可见性问题,可用 volatile 关键字;若是组合操作、需用同步解决原子性问题,则用原子变量,不能使用 volatile 关键字。
原子类和 synchronized 关键字都可保证线程安全,但实现原理、使用范围、粒度、性能不同。通过经典自增案例代码对比说明。
原始的线程不安全代码:两个线程对共享 value 各自加 10000 次,join 确保执行完毕。因 value++ 非原子操作,结果小于 20000(如 14611)。
public class Lesson42 implements Runnable {
static int value = 0;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Lesson42();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(value);
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
value++;
}
}
}
方法一:原子类解决
public class Lesson42Atomic implements Runnable {
static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Lesson42Atomic();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(atomicInteger.get());
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
atomicInteger.incrementAndGet();
}
}
}
计数变量改用 AtomicInteger,自加操作用 incrementAndGet()。原子类保证自加原子性,结果恒等于 20000。
方法二:synchronized 解决
public class Lesson42Syn implements Runnable {
static int value = 0;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Lesson42Syn();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(value);
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (this) {
value++;
}
}
}
}
在 run 方法中加入 synchronized 代码块,保证代码块内部原子性,结果恒等于 20000。
两者在四方面存在差异:
synchronized 基于 monitor 锁,执行同步代码前获取 monitor 锁、执行后释放;原子类基于 CAS 操作。synchronized 可修饰方法或代码块,控制范围灵活,适用场景更广。synchronized 锁。synchronized 是悲观锁,竞争激烈时拿不到锁的线程会阻塞;悲观锁开销固定,不随竞争时间线性增长。synchronized,竞争不激烈时原子类效果更好。synchronized 随 JDK 升级不断优化,从无锁升级为偏向锁、轻量级锁,最后才升级为阻塞的重量级锁,竞争不激烈时性能也可接受。Adder 和 Accumulator 均为 Java 8 引入的类,Accumulator 是更通用版本的 Adder。
典型代表 LongAdder(见前文《AtomicInteger 的高并发优化》一节):高并发下 LongAdder 比 AtomicLong 效率更高。AtomicLong 仅适合低并发场景,高并发下 CAS 冲突概率大,导致频繁自旋、影响效率。
LongAdder 引入分段锁思想:竞争不激烈时所有线程通过 CAS 修改同一个 Base 变量;竞争激烈时,不同线程对应到不同 Cell 上修改,降低冲突概率、提高并发性。
LongAccumulator 是 LongAdder 的功能增强版:LongAdder 的 API 仅支持数值加减,而 LongAccumulator 提供自定义函数操作。
public class LongAccumulatorDemo {
public static void main(String[] args) throws InterruptedException {
LongAccumulator accumulator = new LongAccumulator((x, y) -> x + y, 0);
ExecutorService executor = Executors.newFixedThreadPool(8);
IntStream.range(1, 10).forEach(i -> executor.submit(() -> accumulator.accumulate(i)));
executor.shutdown();
Thread.sleep(2000);
System.out.println(accumulator.getThenReset());
}
}
执行过程:
LongAccumulator,传入两个参数。IntStream 提交 1~9 九个任务。accumulator 值。运行结果为 45,即 0+1+2+...+8+9。
LongAccumulator 构造参数:第一个是二元表达式,第二个是 x 的初始值(示例为 0)。二元表达式中,x 是上一次计算结果(首次为初始值),y 是本次新传入值。
LongAccumulator accumulator = new LongAccumulator((x, y) -> x + y, 0);
accumulate(1) 首次执行时 x=0(初始值)、y=1,结果 0+1=1 赋给下次 x,后续依次累加:
0+1=1;
1+2=3;
3+3=6;
6+4=10;
10+5=15;
15+6=21;
21+7=28;
28+8=36;
36+9=45;
加的顺序不固定(可能先加 5、再加 3、再加 6),但加法满足交换律,最终结果恒为 45。
二元表达式可替换为任意函数,如乘法、求最大/最小值:
LongAccumulator counter = new LongAccumulator((x, y) -> x + y, 0);
LongAccumulator result = new LongAccumulator((x, y) -> x * y, 1); //乘法初始值须为 1,若为 0 结果恒为 0
LongAccumulator min = new LongAccumulator((x, y) -> Math.min(x, y), 0);
LongAccumulator max = new LongAccumulator((x, y) -> Math.max(x, y), 0);
for 循环为串行计算,按固定顺序累加;LongAccumulator 可利用线程池并行计算,效率远高于串行。各线程执行顺序不可控,但最终结果确定。
for 循环;计算量大、需提升效率时,用线程池配合 LongAccumulator 实现并行。ThreadLocal 在业务开发中有两种典型使用场景:
用于保存线程不安全的工具类,典型代表是 SimpleDateFormat。
每个 Thread 内都有自己的实例副本,副本只能由当前 Thread 访问使用,相当于线程内部本地变量(ThreadLocal 命名含义)。因各线程独享副本而非公用,不存在多线程间共享问题。
1. 2 个线程都要用到 SimpleDateFormat
public class ThreadLocalDemo01 {
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
String date = new ThreadLocalDemo01().date(1);
System.out.println(date);
}).start();
Thread.sleep(100);
new Thread(() -> {
String date = new ThreadLocalDemo01().date(2);
System.out.println(date);
}).start();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
return simpleDateFormat.format(date);
}
}
两个线程分别创建自己的 SimpleDateFormat 对象,互不干扰:

运行结果:
00:01
00:02
2. 10 个线程都要用到 SimpleDateFormat
public class ThreadLocalDemo02 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
String date = new ThreadLocalDemo02().date(finalI);
System.out.println(date);
}).start();
Thread.sleep(100);
}
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
return simpleDateFormat.format(date);
}
}
for 循环 10 次,每次新建线程并在 date 方法中创建 SimpleDateFormat 对象,共 10 个对象:

运行结果:
00:00
00:01
00:02
00:03
00:04
00:05
00:06
00:07
00:08
00:09
3. 需求变成 1000 个线程
线程不能无限创建(占用资源多),需用线程池复用线程:
public class ThreadLocalDemo03 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDemo03().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
return dateFormat.format(date);
}
}
用 16 线程线程池提交 1000 次任务,每个任务在 date 方法中创建 SimpleDateFormat 对象。运行结果(多线程下不唯一)能正确打印 00:00~16:39,无重复:
00:00
00:07
00:04
00:02
...
16:29
16:28
16:27
16:26
16:39

左侧线程池、右侧 1000 个任务,即 1000 个任务对应 1000 个 simpleDateFormat 对象。对象创建与销毁均有开销,同时存在内存中属内存浪费,需优化。
4. 所有线程共用一个 simpleDateFormat 对象
public class ThreadLocalDemo04 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDemo04().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
return dateFormat.format(date);
}
}
把 simpleDateFormat 提取为 static 静态变量,省略创建 1000 个对象的开销:

多个线程执行任务,但调用的 simpleDateFormat 是同一个对象,导致线程不安全。
5. 线程不安全,出现并发安全问题
运行结果(多线程下不唯一)出现时间重复:
00:04
00:04
00:05
00:04
...
16:15
16:14
16:13
第一行、第二行均为 04 秒,说明内部已出错。
6. 加锁
出错原因:simpleDateFormat 对象本身不是线程安全的,不应被多线程同时访问。用 synchronized 加锁:
public class ThreadLocalDemo05 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDemo05().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
String s = null;
synchronized (ThreadLocalDemo05.class) {
s = dateFormat.format(date);
}
return s;
}
}
在 date 方法中对 simpleDateFormat 调用加锁。运行结果(多线程下不唯一)无重复时间:
00:00
00:01
00:06
...
15:56
16:37
16:36
但 synchronized 使多个线程排队等待,无法同时工作,整体效率降低。
7. 使用 ThreadLocal
目标:既不过度浪费内存,又保证线程安全——让每个线程拥有自己的 simpleDateFormat 对象:
public class ThreadLocalDemo06 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDemo06().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("mm:ss");
}
};
}
ThreadLocal 为每个线程生成独有的 simpleDateFormat 对象。因线程池只有 16 个线程,故最多只有 16 个对象,不会过多创建。运行结果(多线程下不唯一)正确、无重复:
00:05
00:04
00:01
...
16:37
16:36
16:32

左侧线程池 16 个线程对应 16 个 simpleDateFormat 对象,右侧 1000 个任务。任务虽多,但 simpleDateFormat 对象数量仅与线程数相同,既高效利用内存,又保证线程安全。
每个线程内保存类似全局变量的信息(如拦截器获取的用户信息),供不同方法直接使用,避免参数传递,且不被多线程共享(不同线程获取到的用户信息不同)。
例如用 ThreadLocal 保存业务内容(用户权限信息、用户名、用户 ID 等),同一线程内相同、不同线程不同。在线程生命周期内,通过静态 ThreadLocal 的 get() 获取自己 set 过的对象,避免将 user 对象作为参数层层传递:

假设用户系统:一个请求由一个线程执行,依次调用分布在不同类中的 service-1()~service-4()。service-1() 创建 user 对象,后续 service-2()(下订单)、service-3()(发货)、service-4()(完结订单)都需要用户信息,需将 user 对象层层传递,导致代码冗余。
用 HashMap 解决:在 service-1() 时把用户信息 put 进去,后续直接从静态 User map get:

但 web 服务器多为多线程,需保证线程安全。HashMap 线程不安全,可改用 synchronized 或 ConcurrentHashMap:

两个线程同时访问 service-1/2/3/4(),需 User map 线程安全。但无论 synchronized 还是 ConcurrentHashMap(含少量同步或 CAS 过程)都有性能损耗。
更优方案是使用 ThreadLocal,无需层层传参、不影响性能即可保存当前线程对应的用户信息:

多个线程同时访问 ThreadLocal,各取只属于自己的独享对象,无需额外措施即保证线程安全:
public class ThreadLocalDemo07 {
public static void main(String[] args) {
new Service1().service1();
}
}
class Service1 {
public void service1() {
User user = new User("示例用户");
UserContextHolder.holder.set(user);
new Service2().service2();
}
}
class Service2 {
public void service2() {
User user = UserContextHolder.holder.get();
System.out.println("Service2拿到用户名:" + user.name);
new Service3().service3();
}
}
class Service3 {
public void service3() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名:" + user.name);
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
UserContextHolder 保存 ThreadLocal,Service1 中存入 user 对象,后续方法用 get 直接取出,无参数层层传递。
运行结果:
Service2拿到用户名:示例用户
Service3拿到用户名:示例用户
ThreadLocal 的两个典型使用场景:
常见面试问题。答案:不是,ThreadLocal 并不是用来解决共享资源问题的。
ThreadLocal 虽可解决多线程情况下的线程安全问题,但其资源并非共享,而是每个线程独享。这道题存在陷阱。
ThreadLocal 解决线程安全问题,相比"锁"换了一个思路:把资源变成各线程独享的资源,避免同步操作。可在 initialValue 中 new 出线程独享的资源,多个线程访问的对象本身不共享,不存在并发问题。
如果把放进 ThreadLocal 的资源用 static 修饰变成共享资源,即使使用 ThreadLocal 也会产生线程安全问题。对前文 SimpleDateFormat 例子改造,在 SimpleDateFormat 前加 static 并放入 ThreadLocal:
public class ThreadLocalStatic {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalStatic().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return ThreadLocalStatic.dateFormat;
}
}
}
多线程同时获取并使用时仍出现时间重复:
00:15
00:15
00:05
00:16
...
00:15 被多次打印,发生线程安全问题。若放进 ThreadLocal 的对象是共享的(被 static 修饰),则本就不需要 ThreadLocal,用了也不能解决线程安全问题。对于共享变量,应使用 synchronized 或加锁等方法保证线程安全。
两者都能解决线程安全问题。当 ThreadLocal 用于把对象给每个线程生成独享副本时,它与 synchronized 都是保证线程安全的手段(如前文 SimpleDateFormat 例子),但效果和实现原理不同:
synchronized 效率更低、但内存占用更少。此时两者效果不同但均可达线程安全目的。
当 ThreadLocal 用于让多个类更方便地获取每个线程独立保存的信息(如每个线程对应一个 user 对象)时,侧重点是避免传参,此时与 synchronized 属于两个不同维度的工具。
总结:ThreadLocal 不是用来解决共享资源多线程访问的,因为每个线程中的资源并不共享。

Thread 对象持有 ThreadLocalMap 类型的成员变量(图中 Thread 1 拥有 ThreadLocalMap 1)。ThreadLocalMap 类似 Map,含 key-value 键值对:key 是 ThreadLocal 引用(ThreadLocal 1、2...n),value 是希望存储的内容(如 user 对象)。Thread 只有一个 ThreadLocalMap,一个 ThreadLocalMap 内可有很多 ThreadLocal,每个 ThreadLocal 对应一个 value。因一个线程可调用多个 ThreadLocal,故采用 Map 结构存放。public T get() {
//获取到当前线程
Thread t = Thread.currentThread();
//获取到当前线程内的 ThreadLocalMap 对象,每个线程内都有一个 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//获取 ThreadLocalMap 中的 Entry 对象并拿到 Value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果线程内之前没创建过 ThreadLocalMap,就创建
return setInitialValue();
}
Thread.currentThread() 获取当前线程引用,传入 getMap 拿到当前线程的 ThreadLocalMap。map == null 时,说明该线程未创建过 ThreadLocalMap,调用 setInitialValue 创建。map != null 时,通过 this(当前 ThreadLocal 对象引用)获取对应 Entry,再通过 Entry 拿到 value 返回。ThreadLocalMap 保存在线程 Thread 类中,而非 ThreadLocal 中。ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocalMap 是线程的成员变量,名为 threadLocals,初始值为 null:
ThreadLocal.ThreadLocalMap threadLocals = null;
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap。map == null 时创建 map,否则调用 map.set 保存 value。map.set(this, value) 中:第一个参数 this 是当前 ThreadLocal 引用(再次印证 key 类型为 ThreadLocal),第二个是传入的 value。static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
//...
}
ThreadLocalMap 是每个 Thread 类的成员变量,核心是 Entry 内部类。其中有一个 Entry 类型的数组 table。Entry 的键值对为:
ThreadLocalMap 类似 HashMap,同样具备 set、get、rehash、resize 等标准操作,但实现有差异:

注:第一张图片来自网络,未能找到原始出处,原作者若看到,欢迎联系,将进行标注。
内存泄漏:对象不再有用时,其占用内存却无法被回收。正常情况下 GC 应清理无用对象,使内存可重新分配;若垃圾对象持续积累,可用内存越来越少,最终触发 OOM 错误。
每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 类型变量 threadLocals,线程访问 ThreadLocal 后会在其 Entry 中维护 ThreadLocal 与具体实例的映射。
若业务代码执行 ThreadLocal instance = null 想清理实例,但 ThreadLocalMap 的 Entry 强引用该实例,则在 Thread 类中仍存在引用链。GC 可达性分析发现 ThreadLocal 对象仍可达,故不回收,造成内存泄漏。
JDK 已处理此问题:ThreadLocalMap 的 Entry 继承 WeakReference 弱引用:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
弱引用的特点:对象若只被弱引用关联而无强引用,则可被回收,弱引用不阻止 GC。因此该机制避免了 ThreadLocal(key)的内存泄漏——这是 Entry 的 key 使用弱引用的原因。
虽然 Entry 对 key 是弱引用,但其对 value 是强引用(value = v):
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
value = v 即发生强引用。正常情况下线程终止时 value 可被回收(无强引用);但线程生命周期可能很长,迟迟不终止,此时 ThreadLocal 及其 value 可能早已无用,应保证它们能被回收。
引用链路(实线为强引用,虚线为弱引用):

左侧引用栈含 ThreadLocal 引用和线程引用,右侧堆为对象实例。重点链路:Thread Ref → Current Thread → ThreadLocalMap → Entry → Value → 可能泄漏的value实例。
该链路随线程存在而一直存在。若线程执行耗时任务不停止,GC 可达性分析时 Value 仍可达、不被回收,但业务逻辑可能已完成、不再需要该 Value,即发生内存泄漏。
JDK 的应对:执行 ThreadLocal 的 set、remove、rehash 等方法时,会扫描 key 为 null 的 Entry,若发现则将其 value 置为 null,使 value 对象可被正常回收。
但若 ThreadLocal 已不被使用,则 set、remove、rehash 也不会被调用,同时线程一直存活不终止,调用链一直存在,导致 value 内存泄漏。
解决方法是调用 remove() 方法,删除对应 value 对象:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
先获取 ThreadLocalMap 引用并调用其 remove 方法,清理 key 对应的 value,使 value 可被 GC 回收。
使用完 ThreadLocal 后应手动调用 remove() 防止内存泄漏。
| 特性 | 旧版(Java 8/11) | Java 21 |
|---|---|---|
| 原子类 | CAS + Unsafe | 不变;AtomicInteger 等仍适用 |
| 高性能计数 | LongAdder(8+) | 不变 |
| ThreadLocal | 需手动 remove | 不变;虚拟线程中需警惕 ThreadLocal 膨胀 |
| ScopedValue | 无 | Java 21 预览(JEP 446)引入,JDK 25 转正(JEP 506),替代线程内传参 |
| 虚拟线程适配 | 无 | 每任务一个虚拟线程时 ThreadLocal 按任务隔离 |
虚拟线程中的 ThreadLocal 注意点:虚拟线程数量可达百万级,若每个虚拟线程都写入 ThreadLocal 且不清理,内存占用会线性增长。优先使用
ScopedValue(JDK 25 转正,JEP 506)或显式remove()。
注:第一张图片和引用链相关内容,参考自 https://blog.csdn.net/zhongxiangbo/article/details/70859181,但未能找到更原始的出处,原作者若看到,欢迎联系,将进行标注。