线程安全的核心是解决多线程同时访问共享资源时出现的原子性、可见性、有序性问题。我把常用的方法归纳为5 大类,从简单到复杂依次说明:
无状态 / 不可变对象 (最优雅的方案)
- 原理:没有共享可变状态,天然线程安全
- 实现:
- 使用
final修饰所有字段 - 不提供修改方法,构造函数一次性初始化
- 典型:
string、integer等包装类、枚举
- 使用
- 优点:性能最高,无需任何同步开销
- 缺点:灵活性差,每次修改都要创建新对象
使用线程安全类(开箱即用)
jdk 提供了大量线程安全的工具类,优先使用这些成熟实现:
| 分类 | 推荐使用 | 替代的非线程安全类 |
|---|---|---|
| 集合 | concurrenthashmap、copyonwritearraylist、copyonwritearrayset | hashmap、arraylist、hashset |
| 原子类 | atomicinteger、atomiclong、atomicreference | 普通基本类型 / 引用 |
| 计数器 | longadder、doubleadder | atomiclong(高并发下性能更好) |
| 同步工具 | countdownlatch、cyclicbarrier、semaphore | 手写等待 / 通知 |
volatile 关键字 (轻量级同步)
- 解决的问题:可见性和有序性(禁止指令重排序)
- 不解决的问题:原子性(比如
count++这种复合操作) - 典型使用场景:
- 状态标记位(
boolean flag = true;) - 双重检查锁(dcl)单例模式
- 发布不可变对象
- 状态标记位(
// dcl单例模式的正确写法
public class singleton {
private static volatile singleton instance; // 必须加volatile
private singleton() {}
public static singleton getinstance() {
if (instance == null) { // 第一次检查
synchronized (singleton.class) { // 加锁
if (instance == null) { // 第二次检查
instance = new singleton();
}
}
}
return instance;
}
}
锁机制 (最常用的同步手段)
1. synchronized 关键字
- 特点:jvm 内置锁,自动加锁和释放锁
- 锁的粒度:
- 实例方法锁:锁当前对象
- 静态方法锁:锁 class 对象
- 代码块锁:锁指定对象
- 优化:jdk1.6 引入偏向锁、轻量级锁、重量级锁的升级机制
2. java.util.concurrent.locks 包下的显式锁
- reentrantlock:可重入锁,比 synchronized 更灵活
- 支持公平锁 / 非公平锁
- 可中断锁
- 可尝试获取锁(
trylock())
- reentrantreadwritelock:读写锁
- 读 - 读不互斥,读 - 写互斥,写 - 写互斥
- 适合读多写少的场景,大幅提升并发性能
线程封闭(隔离共享资源)
原理:让数据只在一个线程内访问,从根本上避免竞争
实现方式:
- 1.栈封闭:局部变量天然线程封闭
- 2.threadlocal:每个线程拥有自己的变量副本
private static threadlocal<simpledateformat> dateformatthreadlocal =
threadlocal.withinitial(() -> new simpledateformat("yyyy-mm-dd"));
注意:threadlocal 使用不当会导致内存泄漏,必须在 finally 中调用remove()
各种方法的性能与适用场景对比
┌─────────────────────────────────────────────────────────────────────┐
│ 代码复杂度 高 ↑ │
│ │
│ 特定场景使用 │ 尽量避免 │
│ │ │
│ │ │
│ 读写锁 ● │ │
│ │ │
│ reentrantlock ● │ │
│ │ │
│ threadlocal ● │ │
│ │ │
│ 原子类 ● │ │
│ volatile ● │ │
│ 线程安全类 ● │ │
│ │ │
│ 不可变对象 ● │ │
│ 无状态对象 ● │ │
│────────────────────────────────────┼──────────────────────────────│
│ │ │
│ │ │
│ │ synchronized ●
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ 推荐优先使用 │ 权衡使用 │
│ │
└─────────────────────────────────────────────────────────────────────┘
性能 高 →
| 优先级 | 方法名称 | 性能指数 | 代码复杂度 | 推荐指数 |
|---|---|---|---|---|
| 1 | 无状态对象 | ⭐⭐⭐⭐⭐ | ⭐ | 🌟🌟🌟🌟🌟 |
| 2 | 不可变对象 | ⭐⭐⭐⭐⭐ | ⭐ | 🌟🌟🌟🌟🌟 |
| 3 | volatile 关键字 | ⭐⭐⭐⭐ | ⭐⭐ | 🌟🌟🌟🌟 |
| 4 | 原子类 (atomic 系列) | ⭐⭐⭐⭐ | ⭐⭐ | 🌟🌟🌟🌟 |
| 5 | juc 线程安全类 | ⭐⭐⭐⭐ | ⭐⭐ | 🌟🌟🌟🌟 |
| 6 | threadlocal | ⭐⭐⭐ | ⭐⭐⭐ | 🌟🌟🌟 |
| 7 | 读写锁 | ⭐⭐⭐ | ⭐⭐⭐⭐ | 🌟🌟🌟 |
| 8 | reentrantlock | ⭐⭐⭐ | ⭐⭐⭐⭐ | 🌟🌟 |
| 9 | synchronized | ⭐⭐ | ⭐⭐⭐ | 🌟🌟 |
面试加分项
- 优先使用高层抽象:能用线程安全类就不用自己写锁
- 最小化锁的范围:只在必要的代码块上加锁
- 读写分离:读多写少场景优先使用读写锁
- 避免死锁:按顺序获取锁、设置超时时间、使用 trylock
- 并发工具优先:优先使用 juc 包下的工具,不要重复造轮子
总结
保证线程安全的优先级:无状态 > 不可变 > 线程安全类 > volatile > 显式锁 > synchronized。实际开发中,要根据具体场景选择最合适的方案,在性能和复杂度之间找到最佳平衡点。
核心代码与技术亮点实现
1. 不可变对象的正确实现(面试高频考点)
// 技术亮点:防止子类破坏不可变性、保证所有字段的可见性
public final class immutableuser { // 1. 类用final修饰,禁止继承
private final long id; // 2. 所有字段用final修饰
private final string name;
private final date birthday; // 3. 对可变字段进行防御性拷贝
public immutableuser(long id, string name, date birthday) {
this.id = id;
this.name = name;
// 关键:不直接引用外部传入的可变对象
this.birthday = new date(birthday.gettime());
}
// 4. 不提供任何setter方法
public long getid() { return id; }
public string getname() { return name; }
// 5. getter返回可变对象的拷贝,而不是原引用
public date getbirthday() {
return new date(birthday.gettime());
}
}
2. cas 原子操作原理(atomicinteger 源码核心)
// 技术亮点:无锁编程、cpu原语支持、乐观锁思想
public class atomicinteger extends number implements java.io.serializable {
private volatile int value; // 保证可见性
public final int getandincrement() {
// cas操作:compareandswapint是native方法,由cpu指令保证原子性
return unsafe.getandaddint(this, valueoffset, 1);
}
// unsafe类中的核心方法
// public final native boolean compareandswapint(
// object obj, long offset, int expect, int update);
}
// 手写一个简单的自旋锁(面试常考)
public class spinlock {
private atomicboolean locked = new atomicboolean(false);
public void lock() {
// 循环尝试获取锁,直到成功
while (!locked.compareandset(false, true)) {
// 空自旋,消耗cpu但避免线程上下文切换
}
}
public void unlock() {
locked.set(false);
}
}
3. reentrantlock 正确使用范式(避免死锁)
// 技术亮点:可中断、可超时、公平锁支持
public class reentrantlockexample {
private final reentrantlock lock = new reentrantlock(true); // 公平锁
public void dosomething() {
lock.lock(); // 1. 加锁
try {
// 2. 业务逻辑(必须在try块内)
system.out.println("执行业务逻辑");
} finally {
// 3. 必须在finally中释放锁!!!
lock.unlock();
}
}
// 更安全的写法:trylock避免死锁
public boolean trydosomething(long timeout, timeunit unit) throws interruptedexception {
if (lock.trylock(timeout, unit)) {
try {
system.out.println("获取锁成功,执行业务");
return true;
} finally {
lock.unlock();
}
}
// 获取锁失败
return false;
}
}
4. 读写锁高性能实现(读多写少场景神器)
// 技术亮点:读写分离、读-读不互斥、性能提升10倍以上
public class readwritelockcache<k, v> {
private final map<k, v> cache = new hashmap<>();
private final reentrantreadwritelock rwlock = new reentrantreadwritelock();
private final lock readlock = rwlock.readlock();
private final lock writelock = rwlock.writelock();
// 读操作:加读锁,多个线程可以同时读
public v get(k key) {
readlock.lock();
try {
return cache.get(key);
} finally {
readlock.unlock();
}
}
// 写操作:加写锁,阻塞所有读和写
public void put(k key, v value) {
writelock.lock();
try {
cache.put(key, value);
} finally {
writelock.unlock();
}
}
// 缓存更新:先写后读的原子操作
public v putifabsent(k key, v value) {
writelock.lock();
try {
if (!cache.containskey(key)) {
cache.put(key, value);
return null;
}
return cache.get(key);
} finally {
writelock.unlock();
}
}
}
5. threadlocal 正确使用(避免内存泄漏)
// 技术亮点:线程封闭、避免参数传递、simpledateformat线程安全问题解决
public class threadlocalexample {
// 正确写法:使用static final修饰threadlocal
private static final threadlocal<simpledateformat> date_format =
threadlocal.withinitial(() -> new simpledateformat("yyyy-mm-dd hh:mm:ss"));
public string formatdate(date date) {
try {
return date_format.get().format(date);
} finally {
// 关键:使用完必须remove!!!防止内存泄漏
date_format.remove();
}
}
}
核心技术难点与解决方案汇总
| 技术难点 | 问题表现 | 根本原因 | 最佳解决方案 |
|---|---|---|---|
| 死锁问题 | 程序卡死,cpu 使用率低,线程 blocked 状态 | 多个线程循环等待对方持有的锁 | 1. 按固定顺序获取锁2. 使用trylock()设置超时3. 避免一个线程同时持有多个锁4. 使用jstack命令排查死锁 |
| 可见性问题 | 一个线程修改了变量,另一个线程看不到 | cpu 缓存与主内存不一致 | 1. 使用volatile关键字2. 使用 synchronized 或 lock3. 使用原子类 |
| 指令重排序问题 | dcl 单例模式返回 null、对象半初始化 | jvm 为了优化性能对指令进行重排序 | 1. 使用volatile禁止指令重排序2. 使用静态内部类实现单例 |
| 原子性问题 | count++结果不正确、超卖问题 | 复合操作不是原子性的 | 1. 使用原子类(atomicinteger)2. 使用 synchronized 或 lock3. 使用分段锁(longadder) |
| threadlocal 内存泄漏 | oom 异常、内存占用持续升高 | threadlocalmap 的 entry 是弱引用,value 是强引用 | 1. 使用static final修饰 threadlocal2. 必须在 finally 中调用remove()3. 避免存储大对象 |
| 锁性能问题 | 并发量上不去、吞吐量低 | 锁竞争激烈、锁粒度过大 | 1. 缩小锁的范围(只锁必要代码)2. 使用读写锁(读多写少)3. 使用分段锁(concurrenthashmap)4. 使用无锁编程(cas) |
| 并发集合陷阱 | concurrenthashmap的 putifabsent 误用 | 复合操作不保证原子性 | 1. 使用原子方法(putifabsent、compute)2. 不要先检查再操作3. 避免在遍历中修改集合 |
| 伪共享问题 | 多线程操作数组性能极差 | 多个变量位于同一个 cpu 缓存行 | 1. 使用缓存行填充(@contended 注解)2. 避免多个线程频繁操作相邻变量 |
面试加分亮点代码
静态内部类单例模式(完美解决 dcl 问题)
// 技术亮点:懒加载、线程安全、无锁、性能最高
public class singleton {
private singleton() {} // 私有构造函数
// 静态内部类,只有在第一次被调用时才会加载
private static class singletonholder {
private static final singleton instance = new singleton();
}
public static singleton getinstance() {
return singletonholder.instance;
}
}
longadder 分段锁思想(高并发计数器首选)
// 技术亮点:分段锁、减少cas竞争、高并发下性能比atomiclong高10倍
public class longadder extends striped64 implements serializable {
// 核心思想:将一个long值拆分成多个cell
// 不同线程更新不同的cell,最后求和
// 只有当cell竞争失败时才会扩容
public void add(long x) {
cell[] as; long b, v; int m; cell a;
if ((as = cells) != null || !casbase(b = base, b + x)) {
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getprobe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longaccumulate(x, null, uncontended);
}
}
public long sum() {
cell[] as = cells;
long sum = base;
if (as != null) {
for (cell a : as)
if (a != null)
sum += a.value;
}
return sum;
}
}
总结
线程安全的核心是正确处理原子性、可见性、有序性三个问题。在实际开发中,我们应该遵循 "能不用锁就不用锁,能用高层抽象就不用底层实现" 的原则,优先选择无状态、不可变对象和 juc 包提供的成熟工具类,只有在必要时才使用锁机制。
真实面试模拟
真实面试模拟
面试官 :行,先来个基础题暖暖场。保证线程安全的方法你能想到哪些?随便说,不要求全。
候选人:好的面试官。我先说下我的理解:线程不安全本质上是多个线程同时写同一个可变资源导致的竞态条件。
解决方案我习惯分成三大类:
- 互斥同步(悲观锁):同一时间只让一个线程操作,比如
synchronized、reentrantlock。 - 非阻塞同步(乐观锁):基于 cas 的原子类,如
atomicinteger。 - 无同步方案:干脆不共享,比如不可变对象、
threadlocal、栈封闭。
这三类就像处理多人抢卫生间:第一类是排队加锁,第二类是大家先上,冲突再协调,第三类是给每人发个独立卫生间 。
面试官:哈哈,这个比喻有意思。那我们先聊第一类。synchronized 你用过吧?它和 reentrantlock 有什么本质区别?什么时候你会选后者?
候选人:用过,我项目里支付回调的幂等控制就用了 synchronized。
区别的话:
synchronized是 jvm 内置锁,自动释放,代码简单,但是功能上比较“憨”,没法中途放弃。reentrantlock是 api 层面的锁,灵活:可以trylock带超时、可中断、可设公平锁,还能绑定多个condition做精准唤醒。
选后者的场景:
比如调用一个外部接口,我不希望线程傻等,就用 trylock(200, timeunit.milliseconds),拿不到锁直接降级。这种场合 synchronized 就做不到。
面试官 :很好,抓住了灵活性的关键点。那 读写锁你了解吗?什么情况下它比独占锁性能好?
候选人:
读写锁就是 reentrantreadwritelock,它把锁分成读锁(共享锁)和写锁(独占锁)。读和读不互斥,读和写互斥。
典型场景:配置中心,读配置的线程非常多,写配置很少。如果用 synchronized,所有读都互斥,吞吐量上不去。换成读写锁后,大部分读操作可以并发,只有写时才阻塞所有人。当然,用时要小心锁降级的规则。
面试官 :不错。那咱们来看第二类——非阻塞同步。cas 是什么原理?原子类为什么能无锁却能线程安全?
候选人 :
cas 就是 compare-and-swap,是 cpu 级别的原子指令。好比你去改一个值,改之前先核对一下手里的旧值是不是跟主存里一致,一致就换上新值,不一致就说明别人改过,重新读再试。
java 里的 atomicinteger.incrementandget() 内部就是循环 cas,直到成功,不依赖操作系统挂起线程,所以高并发下吞吐量更高,没有上下文切换开销。不过它有 aba 问题,可以用 atomicstampedreference 加版本号解决。
面试官 :接着你来谈谈无同步方案吧,怎么才能做到不共享?
候选人:
核心思想是“彻底消灭 共享资源”,有三个典型手段:
- 不可变对象:比如
string,一经创建状态不可改。自己写类用final修饰 class,所有字段private final,不提供 setter。这种对象天生线程安全,随便多线程读。 - threadlocal:给每个线程一个独立的数据副本,线程之间物理隔离。像 spring 的
requestcontextholder就用threadlocal保存当前请求。用完后必须remove(),否则在线程池场景下会内存泄漏。 - 栈封闭:方法里的局部变量存在每个线程自己栈里,绝无共享,自然安全。只要别把局部变量的引用发布出去就行。
另外,无状态设计也算:一个 service 没有成员变量,所有逻辑都在方法参数和局部变量上,多线程调用天然安全。
面试官 :思路很清晰。那你能不能快速给我画个全景图,把今天聊的这些串起来?
候选人 :
没问题,我文字描述下,您可以想象一个脑图
🎯 保证线程安全
├─ 🔒 互斥同步(悲观锁)
│ ├─ synchronized(内置锁,自动)
│ ├─ reentrantlock(显式锁,灵活)
│ └─ readwritelock/stampedlock(读多写少)
├─ ⚡ 非阻塞同步(乐观锁)
│ └─ cas 原子类(atomicinteger, atomicreference)
└─ 🚫 无同步方案(避免共享)
├─ 不可变对象(string,integer)
├─ threadlocal(线程私有副本)
├─ 栈封闭(局部变量)
└─ 无状态设计(无成员变量)
面试官 :最后考你一下场景:如果让你给一个全局的统计接口调用次数设计线程安全的计数器,读写比大概 100:1,你选哪个方法?
候选人 :
读写比极高,明显读远大于写。
方案一:直接用 atomiclong,读非常快(无锁),写也是 cas 轻量自旋,足够。
但如果读的是很复杂的衍生数据,我可能会用 reentrantreadwritelock,让大量读线程完全并发。不过对于简单计数器,atomiclong 更简洁,实测性能也极好。我再补一句,jdk8 还提供了 longadder,高并发写时有更好的分散热点设计。
面试官:我看你理论掌握得很扎实,接下来咱们走点实战的。你刚才提到了几种同步方式,能不能现场写几段核心代码,把最有技术亮点的用法展示出来?顺便说说为什么这么写有亮点。
候选人 :好的,那我写三段代码,分别对应互斥同步的高级用法、非阻塞同步的精髓以及无同步方案里最容易踩坑的点。
reentrantlock 的 trylock 超时与降级处理
public class timeoutretryservice {
private final lock lock = new reentrantlock();
public boolean processwithtimeout(string orderid) {
boolean acquired = false;
try {
// 🌟 技术亮点:尝试获取锁,等200ms拿不到就降级,不阻塞
acquired = lock.trylock(200, timeunit.milliseconds);
if (!acquired) {
// 降级策略:放入延迟队列,或返回“操作进行中”
system.out.println("系统繁忙,订单 " + orderid + " 稍后重试");
return false;
}
// 临界区:真正处理业务
doactualwork(orderid);
return true;
} catch (interruptedexception e) {
thread.currentthread().interrupt();
return false;
} finally {
// 🌟 亮点:防止意外释放未持有的锁
if (acquired) {
lock.unlock();
}
}
}
private void doactualwork(string orderid) {
// 模拟幂等操作
}
}
亮点说明:
trylock带超时 + 降级,避免线程饿死在锁上。finally中通过acquired标志判断是否成功获取,防止illegalmonitorstateexception,这是工程里很常见的健壮性写法。
atomicinteger 的 cas 循环 —— 高性能计数器
public class cascounter {
private final atomicinteger count = new atomicinteger(0);
public int incrementandget() {
int prev, next;
do {
prev = count.get();
next = prev + 1;
// 🌟 亮点:cas自旋,无锁,高并发下吞吐量远超synchronized
} while (!count.compareandset(prev, next));
return next;
}
// jdk8 提供的更简洁写法,内部就是 cas
public int quickincrement() {
return count.incrementandget(); // 一行搞定
}
}
亮点说明:
- 手动展示了 cas 循环的原始逻辑,让面试官知道你不只会用 api,还懂原理。
- 对比
incrementandget(),说明底层也是 cas,上层才是原子包装。
threadlocal 的正确使用与内存泄漏预防
public class requestcontextholder {
private static final threadlocal<map<string, object>> context =
threadlocal.withinitial(hashmap::new);
public static void set(string key, object value) {
context.get().put(key, value);
}
public static object get(string key) {
return context.get().get(key);
}
// 🌟 核心亮点:显式提供清理方法,防止线程池复用导致内存泄漏
public static void clear() {
context.remove(); // 必须调!
}
}
// 使用方:在过滤器 finally 块中清理
try {
requestcontextholder.set("userid", "10086");
// ... 业务处理
} finally {
requestcontextholder.clear(); // ⚠️ 关键一步
}
亮点说明:
threadlocal.withinitial()提供初始值,代码更优雅。- 重点强调了
remove(),这是生产环境中内存泄漏的高发点,能提到它说明你有 jvm 调优意识。
面试官:不错,三段代码都把“坑”和“亮点”点到了。最后,咱们总结一下,在保证线程安全的设计中,你遇到过哪些技术难点,分别怎么解决?
候选人:
我总结五个最常见的难点和方案:
| 技术难点 | 典型场景 | 解决方案 |
|---|---|---|
| 1. 复合操作的原子性 | if(!map.containskey(key)) map.put(...) 这样的“检查-执行”操作 | 用 concurrenthashmap.putifabsent(),或把操作包在 synchronized 块中,但优先用并发容器的原子方法 |
| 2. 锁粒度控制与死锁 | 大锁导致性能差,多锁嵌套导致死锁 | 缩小同步范围(快进快出),统一加锁顺序,用 trylock 超时预防死锁,或用 jstack 检测 |
| 3. cas 的 aba 问题 | 链表头节点从 a→b→a,虽然值相同但状态已变 | 使用带版本号的 atomicstampedreference 或 atomicmarkablereference,在链表操作中尤其关键 |
| 4. threadlocal 内存泄漏 | 线程池线程复用,threadlocal 引用不清理导致 oom | 强制在 finally 中调用 remove(),或者使用带弱引用的自定义 threadlocal,建议所有框架都内置清理 |
| 5. 可见性 & 指令重排 | 一个线程改了值,另一个线程永远看不见 | 用 volatile 保证可见性和禁止指令重排,或者使用锁(隐式内存屏障),避免依赖“运气” |
以上就是一文梳理java中保证线程安全的5大常见方法的详细内容,更多关于java线程安全的资料请关注代码网其它相关文章!
发表评论