线程安全与并发问题
线程安全与并发问题排查
并发问题的一个难点在于它不一定能够稳定复现。很多 bug 在线下运行多次不出现问题,一旦线上出现并发压力便暴露,例如:
- 数据重复写入
- 余额扣错
- 缓存和数据库状态不一致
- 死锁或线程阻塞
因此,掌握从现象回溯到竞态条件的能力,比单纯记忆概念更为重要。
一、线程安全的核心概念
1.1 线程安全的定义
线程安全的核心不是"代码里用了锁",而是:
- 多线程同时执行时
- 共享状态仍然正确
- 结果可预期
如果一段代码在并发下会出现:
- 丢失更新:两个线程同时修改,其中一个修改被覆盖
- 状态覆盖:一个线程的修改覆盖了另一个线程的修改
- 顺序错乱:操作执行顺序与预期不符
那它就是线程不安全的。
Brian Goetz 在《Java Concurrency in Practice》中对线程安全的定义是:
当多个线程访问同一个对象时,如果不需要考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
1.2 线程安全的三大特性
线程安全必须同时满足三个核心特性:原子性、可见性、有序性。
1.2.1 原子性(Atomicity)
定义:一个或多个操作,要么全部执行成功,要么全部不执行,不会出现执行到一半被中断的情况。
问题场景:
public class Counter {
private int count = 0;
public void increment() {
count++; // 非原子操作
}
}count++ 看起来是一行代码,但实际上包含三个步骤:
- 读取 count 的值
- 将值加 1
- 将新值写回 count
在多线程环境下,可能出现:
线程1: 读取 count=0
线程2: 读取 count=0
线程1: 计算 0+1=1
线程2: 计算 0+1=1
线程1: 写入 count=1
线程2: 写入 count=1 // 覆盖了线程1的结果最终 count 应该是 2,但实际是 1,这就是原子性问题。
原子性保障方式:
- 使用原子类:
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子操作
}
public int getCount() {
return count.get();
}
}- 使用 synchronized:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}- 使用 Lock:
public class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}常见原子类:
| 原子类 | 说明 | 典型应用场景 |
|---|---|---|
AtomicInteger | 整型原子类 | 计数器、序列号生成 |
AtomicLong | 长整型原子类 | 计数器、统计信息 |
AtomicBoolean | 布尔原子类 | 状态标记、开关 |
AtomicReference<V> | 引用原子类 | 更新对象引用 |
AtomicIntegerArray | 整型数组原子类 | 数组元素的原子更新 |
AtomicStampedReference<V> | 带版本戳的原子引用类 | 解决 ABA 问题 |
LongAdder | 长整型累加器 | 高并发计数场景 |
DoubleAdder | 双精度累加器 | 高并发累加统计 |
LongAdder vs AtomicInteger:
// 高并发下性能对比
public class CounterComparison {
private AtomicInteger atomicInt = new AtomicInteger();
private LongAdder longAdder = new LongAdder();
// AtomicInteger: 高并发下有竞争热点
public void incrementAtomic() {
atomicInt.incrementAndGet(); // CAS 循环重试
}
// LongAdder: 分散热点,性能更好
public void incrementLongAdder() {
longAdder.increment(); // 多个 Cell 分散更新
}
}LongAdder 在高并发场景下性能优于 AtomicInteger,因为:
- AtomicInteger 多个线程同时 CAS 更新同一个值,竞争激烈
- LongAdder 内部维护多个 Cell,分散热点,最后求和
1.2.2 可见性(Visibility)
定义:当一个线程修改了共享变量的值,其他线程能够立即看到修改后的值。
问题场景:
public class VisibilityProblem {
private boolean running = true;
public void stop() {
running = false; // 主线程修改
}
public void doWork() {
while (running) { // 工作线程可能看不到修改
// do something
}
}
}工作线程可能一直看不到 running = false 的修改,导致无法停止。
可见性问题的根本原因:
- CPU 缓存:每个 CPU 核心有自己的缓存,线程可能从缓存读取旧值
- 指令重排序:编译器和处理器可能对指令进行重排序
- JIT 优化:JIT 可能优化掉一些内存读取操作
可见性保障方式:
- 使用 volatile:
public class VisibilitySolved {
private volatile boolean running = true;
public void stop() {
running = false;
}
public void doWork() {
while (running) {
// volatile 保证每次都从主内存读取最新值
}
}
}volatile 的内存语义:
- 写操作:强制刷新到主内存
- 读操作:强制从主内存读取
- 使用 synchronized:
public class VisibilitySolved {
private boolean running = true;
public synchronized void stop() {
running = false;
}
public synchronized boolean isRunning() {
return running;
}
}synchronized 的内存语义:
- 进入同步块:清空工作内存,从主内存读取
- 退出同步块:将工作内存的值刷新到主内存
- 使用 final:
public class FinalVisibility {
private final int value;
public FinalVisibility(int value) {
this.value = value; // final 字段在构造函数中初始化后,其他线程立即可见
}
public int getValue() {
return value; // 不需要同步
}
}volatile vs synchronized:
| 特性 | volatile | synchronized |
|---|---|---|
| 原子性 | 不保证 | 保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证 | 保证 |
| 阻塞 | 不会阻塞 | 会阻塞 |
| 适用场景 | 单一变量的读写 | 复杂的临界区操作 |
| 性能 | 较高 | 较低 |
1.2.3 有序性(Ordering)
定义:程序执行的顺序按照代码的先后顺序执行。
问题场景:
public class OrderProblem {
private int a = 0;
private boolean flag = false;
// 线程1
public void writer() {
a = 1; // 1
flag = true; // 2
}
// 线程2
public void reader() {
if (flag) { // 3
int i = a; // 4
}
}
}由于指令重排序,可能的执行顺序:
- 语句 2 可能在语句 1 之前执行
- 线程 2 看到
flag = true后,a可能还是 0
指令重排序类型:
- 编译器重排序:编译器在不改变单线程执行结果的前提下优化指令顺序
- 处理器重排序:现代处理器采用指令级并行技术,可以乱序执行
- 内存系统重排序:缓存和写缓冲区的存在导致加载/存储操作看起来是乱序的
有序性保障方式:
- 使用 volatile:禁止指令重排序
public class OrderSolved {
private int a = 0;
private volatile boolean flag = false;
public void writer() {
a = 1;
flag = true; // volatile 写,前面的普通写不会被重排序到后面
}
public void reader() {
if (flag) { // volatile 读,后面的普通读不会被重排序到前面
int i = a; // 此时 a 一定已经是 1
}
}
}- 使用 synchronized:保证同步块内的有序性
public class OrderSolved {
private int a = 0;
private boolean flag = false;
public synchronized void writer() {
a = 1;
flag = true;
}
public synchronized void reader() {
if (flag) {
int i = a;
}
}
}Happens-Before 原则:
JMM 通过 happens-before 原则来保证有序性:
- 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作
- 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁
- volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读
- 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C
- 线程启动规则:Thread 对象的 start() 方法 happens-before 该线程的每一个动作
- 线程终止规则:线程中的所有操作都 happens-before 其他线程从该线程的 join() 方法成功返回
- 线程中断规则:对线程 interrupt() 方法的调用 happens-before 被中断线程的代码检测到中断事件
- 对象终结规则:一个对象的初始化完成 happens-before 它的 finalize() 方法的开始
1.3 三大特性的关系
线程安全
├── 原子性: 操作不可分割
├── 可见性: 修改立即可见
└── 有序性: 执行顺序可预测
保障方式:
├── synchronized: 保证全部三个特性
├── volatile: 保证可见性和有序性,不保证原子性
├── final: 保证初始化完成后的可见性
├── 原子类: 保证原子性
└── Lock: 保证全部三个特性面试要点:
问题:volatile 能保证线程安全吗?
回答:volatile 只能保证可见性和有序性,不能保证原子性。对于单一变量的读写操作是线程安全的,但对于复合操作(如 i++)不是线程安全的。如果要保证原子性,需要使用原子类或锁。
问题:synchronized 和 volatile 有什么区别?
回答:
- volatile 是轻量级的同步机制,只保证可见性和有序性,不保证原子性
- synchronized 是重量级的同步机制,保证原子性、可见性和有序性
- volatile 不会阻塞线程,synchronized 会阻塞线程
- volatile 适用于单一变量的读写场景,synchronized 适用于复杂的临界区操作
二、线程安全的实现方式
2.1 不可变对象(Immutable Object)
核心思想:状态不可变的对象天然是线程安全的。
实现方式:
public final class ImmutableObject {
private final int value;
private final String name;
private final List<String> list;
public ImmutableObject(int value, String name, List<String> list) {
this.value = value;
this.name = name;
this.list = Collections.unmodifiableList(new ArrayList<>(list)); // 防御性拷贝
}
public int getValue() {
return value;
}
public String getName() {
return name;
}
public List<String> getList() {
return list; // 已经是不可变的
}
// 不提供 setter 方法
}不可变对象的设计原则:
- 类使用
final修饰,防止子类覆盖方法 - 所有字段使用
final修饰,保证初始化后不可变 - 不提供 setter 方法
- 对于可变对象的引用,使用防御性拷贝
- 不暴露 this 引用
典型应用:
// String 是典型的不可变对象
String s1 = "hello";
String s2 = s1.toUpperCase(); // 返回新对象,原对象不变
// BigInteger、BigDecimal 也是不可变对象
BigInteger b1 = new BigInteger("123");
BigInteger b2 = b1.add(new BigInteger("456")); // 返回新对象
// 使用不可变对象作为 Map 的 key
Map<String, Integer> map = new HashMap<>();
map.put("key", 1); // String 是不可变的,线程安全2.2 同步机制
2.2.1 synchronized 关键字
三种使用方式:
- 实例方法:锁当前实例对象
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++; // 锁 this
}
public synchronized int getCount() {
return count; // 锁 this
}
}- 静态方法:锁 Class 对象
public class SynchronizedExample {
private static int count = 0;
public static synchronized void increment() {
count++; // 锁 SynchronizedExample.class
}
public static synchronized int getCount() {
return count; // 锁 SynchronizedExample.class
}
}- 代码块:锁指定对象
public class SynchronizedExample {
private final Object lock = new Object();
private int count = 0;
public void increment() {
synchronized (lock) {
count++;
}
}
}synchronized 的锁升级:
Java 6 之后,synchronized 的锁状态会根据竞争情况逐步升级:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁| 锁状态 | 说明 | 适用场景 | 性能 |
|---|---|---|---|
| 无锁 | 没有锁竞争 | 单线程访问 | 最高 |
| 偏向锁 | 锁偏向第一个获取它的线程 | 同一个线程多次获取 | 高 |
| 轻量级锁 | CAS 自旋获取锁 | 交替执行或短时间竞争 | 中 |
| 重量级锁 | 操作系统互斥量 | 长时间竞争 | 低 |
锁升级过程:
// 1. 对象创建,默认无锁或偏向锁(取决于 JVM 配置)
Object obj = new Object();
// 2. 第一个线程访问,获取偏向锁
synchronized (obj) {
// 偏向锁: Mark Word 记录线程 ID
}
// 3. 另一个线程尝试获取,升级为轻量级锁
synchronized (obj) {
// 轻量级锁: CAS 操作 Mark Word
}
// 4. 竞争激烈,升级为重量级锁
synchronized (obj) {
// 重量级锁: 监视器锁,线程阻塞
}注意:锁只能升级,不能降级。
2.2.2 ReentrantLock
基本使用:
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 必须在 finally 中释放锁
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}ReentrantLock 的高级特性:
- 可中断锁:
public void tryLockWithTimeout() throws InterruptedException {
// 尝试获取锁,最多等待 5 秒
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 获取锁成功
} finally {
lock.unlock();
}
} else {
// 获取锁失败
}
}
public void lockInterruptibly() throws InterruptedException {
// 可响应中断的锁获取
lock.lockInterruptibly();
try {
// 执行业务逻辑
} finally {
lock.unlock();
}
}- 公平锁与非公平锁:
// 非公平锁(默认): 效率高,但可能导致线程饥饿
ReentrantLock unfairLock = new ReentrantLock();
// 公平锁: 按照请求锁的顺序获取,但效率低
ReentrantLock fairLock = new ReentrantLock(true);- 条件变量:
public class BoundedBuffer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Object[] items = new Object[100];
private int putIndex, takeIndex, count;
public void put(Object item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // 等待不满条件
}
items[putIndex] = item;
if (++putIndex == items.length) putIndex = 0;
count++;
notEmpty.signal(); // 通知不空条件
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 等待不空条件
}
Object item = items[takeIndex];
if (++takeIndex == items.length) takeIndex = 0;
count--;
notFull.signal(); // 通知不满条件
return item;
} finally {
lock.unlock();
}
}
}synchronized vs ReentrantLock:
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 锁获取方式 | JVM 关键字 | API 调用 |
| 锁释放方式 | 自动释放 | 手动释放(finally) |
| 可中断性 | 不可中断 | 可中断 |
| 公平性 | 非公平 | 可选公平/非公平 |
| 条件变量 | 单一条件 | 多个条件 |
| 锁状态检测 | 不支持 | 支持 |
| 性能 | 优化后差距不大 | 竞争激烈时略优 |
2.3 并发容器
2.3.1 同步容器
Vector 和 Hashtable:
// 同步容器: 所有方法都用 synchronized 修饰
Vector<String> vector = new Vector<>();
vector.add("element"); // synchronized
Hashtable<String, String> hashtable = new Hashtable<>();
hashtable.put("key", "value"); // synchronizedCollections.synchronizedXXX:
// 将普通容器包装成同步容器
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
Map<String, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
Set<String> synchronizedSet = Collections.synchronizedSet(new HashSet<>());同步容器的问题:
// 复合操作不是原子的
List<String> list = Collections.synchronizedList(new ArrayList<>());
// 线程不安全的复合操作
if (!list.contains("element")) {
list.add("element"); // 在 contains 和 add 之间可能被其他线程修改
}
// 需要额外的同步
synchronized (list) {
if (!list.contains("element")) {
list.add("element");
}
}
// 迭代时需要同步
synchronized (list) {
for (String s : list) {
// 如果不同步,其他线程修改可能导致 ConcurrentModificationException
}
}2.3.2 并发容器
ConcurrentHashMap:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 原子复合操作
map.putIfAbsent("key", 1); // 如果 key 不存在才插入
map.computeIfAbsent("key", k -> calculateValue(k)); // 如果 key 不存在才计算并插入
map.computeIfPresent("key", (k, v) -> v + 1); // 如果 key 存在才更新
map.replace("key", 1, 2); // 如果值匹配才替换
// 批量操作(Java 8+)
map.forEach((k, v) -> System.out.println(k + "=" + v));
map.replaceAll((k, v) -> v * 2);
map.merge("key", 1, (oldVal, newVal) -> oldVal + newVal);ConcurrentHashMap 的实现原理:
- Java 7:分段锁(Segment),默认 16 个段,并发度最高为 16
- Java 8+:CAS + synchronized,锁粒度更细,并发度更高
// Java 8 ConcurrentHashMap 的 put 操作大致流程
public V put(K key, V value) {
// 1. 计算 hash
int hash = spread(key.hashCode());
// 2. 遍历 table
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 3. 如果桶为空,CAS 插入新节点
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // CAS 成功,插入完成
}
// 4. 如果正在扩容,帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 5. 否则锁住头节点,插入或更新
else {
synchronized (f) {
// 遍历链表或红黑树,插入或更新节点
}
}
}
return null;
}CopyOnWriteArrayList:
// 适用于读多写少的场景
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 读操作: 不加锁,直接访问
String element = list.get(0);
// 写操作: 复制整个数组
list.add("element"); // 复制原数组,添加元素,替换原数组
list.remove(0); // 复制原数组,删除元素,替换原数组
// 迭代: 不需要同步,不会抛出 ConcurrentModificationException
for (String s : list) {
// 迭代的是原数组的快照
}CopyOnWriteArrayList 的问题:
- 写操作性能低:每次写都要复制整个数组
- 内存占用高:每次写都创建新数组
- 数据一致性:迭代时可能看不到最新修改
BlockingQueue:
// ArrayBlockingQueue: 有界阻塞队列
BlockingQueue<String> arrayQueue = new ArrayBlockingQueue<>(100);
arrayQueue.put("element"); // 队列满时阻塞
String element = arrayQueue.take(); // 队列空时阻塞
// LinkedBlockingQueue: 可选有界/无界阻塞队列
BlockingQueue<String> linkedQueue = new LinkedBlockingQueue<>(100);
BlockingQueue<String> unboundedQueue = new LinkedBlockingQueue<>(); // 无界
// SynchronousQueue: 无缓冲,直接传递
BlockingQueue<String> syncQueue = new SynchronousQueue<>();
new Thread(() -> {
try {
syncQueue.put("element"); // 等待消费者
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
new Thread(() -> {
try {
String e = syncQueue.take(); // 等待生产者
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
// PriorityBlockingQueue: 优先级队列
BlockingQueue<Integer> priorityQueue = new PriorityBlockingQueue<>();
priorityQueue.put(3);
priorityQueue.put(1);
priorityQueue.put(2);
// take 时按优先级(自然排序)出队: 1, 2, 3
// DelayQueue: 延迟队列
class DelayedElement implements Delayed {
private final long expireTime;
public DelayedElement(long delay) {
this.expireTime = System.nanoTime() + delay;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(expireTime - System.nanoTime(), TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed o) {
return Long.compare(this.expireTime, ((DelayedElement) o).expireTime);
}
}
DelayQueue<DelayedElement> delayQueue = new DelayQueue<>();
delayQueue.put(new DelayedElement(TimeUnit.SECONDS.toNanos(5))); // 5 秒后可用ConcurrentLinkedQueue:
// 高性能无界非阻塞队列
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
// 无阻塞操作
queue.offer("element"); // 入队
String element = queue.poll(); // 出队,队列为空返回 null
String peek = queue.peek(); // 查看队首,不出队
// 适用于高并发场景,性能优于 BlockingQueue2.4 ThreadLocal
基本使用:
// 每个线程独立的变量副本
public class ThreadLocalExample {
private static final ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);
public void setValue(int value) {
threadLocalValue.set(value);
}
public int getValue() {
return threadLocalValue.get();
}
public void remove() {
threadLocalValue.remove(); // 防止内存泄漏
}
}典型应用场景:
- 数据库连接:
public class ConnectionManager {
private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
public static Connection getConnection() {
Connection conn = connectionHolder.get();
if (conn == null) {
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb");
connectionHolder.set(conn);
}
return conn;
}
public static void closeConnection() {
Connection conn = connectionHolder.get();
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
// 处理异常
} finally {
connectionHolder.remove(); // 必须清理
}
}
}
}- 用户上下文:
public class UserContext {
private static final ThreadLocal<User> userHolder = new ThreadLocal<>();
public static void setUser(User user) {
userHolder.set(user);
}
public static User getUser() {
return userHolder.get();
}
public static void clear() {
userHolder.remove();
}
}
// 在拦截器中设置用户上下文
public class UserInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
User user = getUserFromToken(request.getHeader("Authorization"));
UserContext.setUser(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
UserContext.clear(); // 必须清理
}
}- 日期格式化:
// SimpleDateFormat 不是线程安全的
public class DateFormatter {
private static final ThreadLocal<SimpleDateFormat> dateFormatHolder =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static String format(Date date) {
return dateFormatHolder.get().format(date);
}
public static Date parse(String dateStr) throws ParseException {
return dateFormatHolder.get().parse(dateStr);
}
}ThreadLocal 的内存泄漏问题:
// ThreadLocal 的内部结构
Thread
└── threadLocals (ThreadLocalMap)
└── Entry (WeakReference<ThreadLocal<?>> key, Object value)
// Entry 的 key 是弱引用,会被 GC 回收
// 但 value 是强引用,如果线程长期存活,value 不会被回收
// 必须手动清理
try {
threadLocal.set(value);
// 使用 value
} finally {
threadLocal.remove(); // 防止内存泄漏
}ThreadLocal 的最佳实践:
// 1. 使用 static final 修饰
private static final ThreadLocal<User> userHolder = new ThreadLocal<>();
// 2. 在 finally 块中清理
try {
userHolder.set(user);
// 业务逻辑
} finally {
userHolder.remove();
}
// 3. 在线程池中使用时要特别注意
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
try {
userHolder.set(user);
// 业务逻辑
} finally {
userHolder.remove(); // 线程池中线程会复用,必须清理
}
});面试要点:
问题:ThreadLocal 为什么会导致内存泄漏?
回答:ThreadLocal 的 Entry 中,key 是弱引用,会被 GC 回收,但 value 是强引用。如果线程长期存活(如线程池中的线程),value 对象不会被回收,导致内存泄漏。解决方法是使用完 ThreadLocal 后调用 remove() 方法清理。
三、常见的并发问题类型
3.1 竞态条件(Race Condition)
定义:当多个线程访问和操作同一对象时,最终执行结果与执行时序有关,可能正确也可能不正确。
经典案例:双重检查锁定(DCL):
// 错误的双重检查锁定
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 非原子操作
}
}
}
return instance;
}
}问题:instance = new Singleton() 不是原子操作,分为三步:
- 分配内存空间
- 初始化对象
- 将引用指向内存
指令重排序可能导致 2 和 3 交换顺序:
- 线程1 执行了 1 和 3(instance 不为 null,但对象未初始化)
- 线程2 判断 instance 不为 null,直接使用未初始化的对象
正确的双重检查锁定:
public class Singleton {
private static volatile Singleton instance; // volatile 禁止指令重排序
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}3.2 死锁(Deadlock)
定义:两个或多个线程互相持有对方需要的锁,导致所有线程都无法继续执行。
经典案例:哲学家进餐问题:
public class DeadlockDemo {
private static final Object fork1 = new Object();
private static final Object fork2 = new Object();
private static final Object fork3 = new Object();
public static void main(String[] args) {
// 哲学家1: 先拿 fork1,再拿 fork2
new Thread(() -> {
synchronized (fork1) {
System.out.println("哲学家1 拿起 fork1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (fork2) {
System.out.println("哲学家1 拿起 fork2,开始进餐");
}
}
}).start();
// 哲学家2: 先拿 fork2,再拿 fork3
new Thread(() -> {
synchronized (fork2) {
System.out.println("哲学家2 拿起 fork2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (fork3) {
System.out.println("哲学家2 拿起 fork3,开始进餐");
}
}
}).start();
// 哲学家3: 先拿 fork3,再拿 fork1
new Thread(() -> {
synchronized (fork3) {
System.out.println("哲学家3 拿起 fork3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (fork1) {
System.out.println("哲学家3 拿起 fork1,开始进餐");
}
}
}).start();
}
}死锁的四个必要条件:
- 互斥条件:资源一次只能被一个线程使用
- 持有并等待:线程持有资源同时等待其他资源
- 不可剥夺:资源不能被强制抢占
- 循环等待:线程间形成循环等待资源的关系
死锁的预防策略:
- 破坏循环等待:所有线程按相同顺序获取锁
// 修改哲学家进餐问题:所有哲学家按 fork 编号从小到大获取
public class DeadlockFree {
public static void main(String[] args) {
// 所有哲学家按 fork 编号从小到大获取
new Thread(() -> {
synchronized (fork1) {
System.out.println("哲学家1 拿起 fork1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (fork2) {
System.out.println("哲学家1 拿起 fork2,开始进餐");
}
}
}).start();
new Thread(() -> {
synchronized (fork2) { // 先拿 fork2
System.out.println("哲学家2 拿起 fork2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (fork3) { // 再拿 fork3
System.out.println("哲学家2 拿起 fork3,开始进餐");
}
}
}).start();
new Thread(() -> {
synchronized (fork1) { // 先拿 fork1(编号最小的)
System.out.println("哲学家3 拿起 fork1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (fork3) { // 再拿 fork3
System.out.println("哲学家3 拿起 fork3,开始进餐");
}
}
}).start();
}
}- 破坏持有并等待:一次性获取所有资源
public class HoldAndWaitFree {
private static final Object lock = new Object();
public void transfer(Account from, Account to, int amount) {
synchronized (lock) { // 先获取全局锁
synchronized (from) {
synchronized (to) {
from.debit(amount);
to.credit(amount);
}
}
}
}
}- 使用超时机制:
public class TimeoutLock {
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public void transfer() {
boolean lock1Acquired = false;
boolean lock2Acquired = false;
try {
lock1Acquired = lock1.tryLock(100, TimeUnit.MILLISECONDS);
if (lock1Acquired) {
lock2Acquired = lock2.tryLock(100, TimeUnit.MILLISECONDS);
if (lock2Acquired) {
// 执行业务逻辑
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock2Acquired) {
lock2.unlock();
}
if (lock1Acquired) {
lock1.unlock();
}
}
}
}死锁的检测工具:
# 1. 使用 jstack 检测死锁
jstack <pid>
# 2. 使用 jconsole 检测死锁
jconsole
# 3. 使用 VisualVM 检测死锁
jvisualvm3.3 活锁(Livelock)
定义:线程不断改变状态,尝试响应对方,但实际没有进展。
案例:
public class LivelockDemo {
static class Spoon {
private Diner owner;
public Spoon(Diner owner) {
this.owner = owner;
}
public Diner getOwner() {
return owner;
}
public synchronized void setOwner(Diner owner) {
this.owner = owner;
}
public synchronized void use() {
System.out.println(owner.name + " 使用勺子");
}
}
static class Diner {
private String name;
private boolean isHungry;
public Diner(String name) {
this.name = name;
this.isHungry = true;
}
public void eatWith(Spoon spoon, Diner spouse) {
while (isHungry) {
// 如果勺子不在自己手里,等待
if (spoon.getOwner() != this) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
continue;
}
// 如果配偶也饿,就把勺子给配偶
if (spouse.isHungry) {
System.out.println(name + ": " + spouse.name + " 也饿了,把勺子给他");
spoon.setOwner(spouse);
continue;
}
// 使用勺子吃饭
spoon.use();
isHungry = false;
System.out.println(name + " 吃完了");
spoon.setOwner(spouse);
}
}
}
public static void main(String[] args) {
Diner husband = new Diner("丈夫");
Diner wife = new Diner("妻子");
Spoon spoon = new Spoon(husband);
new Thread(() -> husband.eatWith(spoon, wife)).start();
new Thread(() -> wife.eatWith(spoon, husband)).start();
}
}活锁的特点:
- 线程没有阻塞,一直在运行
- 但实际没有进展,一直在做无用功
- 类似两个人在走廊相遇,都想让路,结果同时往同一个方向让
活锁的解决方法:
- 引入随机性
- 设置重试次数上限
- 使用指数退避策略
public class LivelockFree {
static class Diner {
private String name;
private boolean isHungry;
private int retryCount = 0;
private static final int MAX_RETRY = 3;
public void eatWith(Spoon spoon, Diner spouse) {
while (isHungry && retryCount < MAX_RETRY) {
if (spoon.getOwner() != this) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
continue;
}
// 如果配偶也饿,有概率把勺子给配偶
if (spouse.isHungry) {
retryCount++;
// 引入随机性,避免同时让步
if (Math.random() > 0.5) {
spoon.setOwner(spouse);
}
continue;
}
spoon.use();
isHungry = false;
spoon.setOwner(spouse);
}
}
}
}3.4 饥饿(Starvation)
定义:线程长期无法获取所需资源,导致无法执行。
饥饿的场景:
- 不公平的锁:非公平锁可能导致某些线程长期获取不到锁
// 非公平锁可能导致饥饿
ReentrantLock unfairLock = new ReentrantLock(false); // 默认非公平
// 使用公平锁避免饥饿
ReentrantLock fairLock = new ReentrantLock(true);- 线程优先级问题:低优先级线程可能长期得不到执行
public class PriorityStarvation {
public static void main(String[] args) {
// 高优先级线程
Thread highPriority = new Thread(() -> {
while (true) {
// CPU 密集型任务
}
});
highPriority.setPriority(Thread.MAX_PRIORITY); // 优先级 10
// 低优先级线程
Thread lowPriority = new Thread(() -> {
while (true) {
// 可能长期得不到执行
}
});
lowPriority.setPriority(Thread.MIN_PRIORITY); // 优先级 1
highPriority.start();
lowPriority.start();
}
}- 同步块执行时间长:某些线程持有锁时间过长,其他线程等待过久
public class LongLockTime {
private final Object lock = new Object();
public void longTask() {
synchronized (lock) {
// 长时间持有锁
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public void shortTask() {
synchronized (lock) { // 长时间等待
// 短时间任务
}
}
}饥饿的解决方法:
- 使用公平锁
- 合理设置线程优先级(一般不建议修改)
- 减小锁粒度,缩短临界区
- 使用锁分段
3.5 线程池和阻塞队列堆积
问题场景:
public class ThreadPoolProblem {
public static void main(String[] args) {
// 线程池配置不合理
ExecutorService executor = new ThreadPoolExecutor(
10, // 核心线程数
10, // 最大线程数
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(), // 无界队列,可能导致 OOM
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
// 任务提交速度 > 处理速度
for (int i = 0; i < 100000; i++) {
executor.submit(() -> {
try {
Thread.sleep(1000); // 慢任务
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
}线程池堆积的原因:
- 任务处理速度慢(IO 阻塞、数据库查询慢、外部服务慢)
- 线程池配置不合理(核心线程数过小、队列过大)
- 任务提交速度过快(流量突增、批量任务)
- 任务依赖导致死锁(任务 A 等待任务 B,但都在同一线程池)
线程池堆积的解决方法:
- 使用有界队列 + 合理的拒绝策略:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10,
20,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 有界队列
new ThreadPoolExecutor.CallerRunsPolicy() // 调用者执行
);- 监控线程池指标:
public void monitorThreadPool(ThreadPoolExecutor executor) {
ScheduledExecutorService monitor = Executors.newScheduledThreadPool(1);
monitor.scheduleAtFixedRate(() -> {
System.out.println("核心线程数: " + executor.getCorePoolSize());
System.out.println("最大线程数: " + executor.getMaximumPoolSize());
System.out.println("当前线程数: " + executor.getPoolSize());
System.out.println("活跃线程数: " + executor.getActiveCount());
System.out.println("已完成任务数: " + executor.getCompletedTaskCount());
System.out.println("队列大小: " + executor.getQueue().size());
}, 0, 1, TimeUnit.SECONDS);
}- 动态调整线程池参数:
public void adjustThreadPool(ThreadPoolExecutor executor, int corePoolSize, int maxPoolSize) {
executor.setCorePoolSize(corePoolSize);
executor.setMaximumPoolSize(maxPoolSize);
}四、并发问题排查工具详解
4.1 jstack:线程堆栈分析
基本使用:
# 查看 Java 进程 PID
jps -l
# 输出线程堆栈
jstack <pid>
# 输出线程堆栈到文件
jstack <pid> > thread_dump.txt
# 检测死锁
jstack -l <pid>线程堆栈分析:
// 线程堆栈示例
"http-nio-8080-exec-1" #25 daemon prio=5 os_prio=0 tid=0x00007f8a9c001000 nid=0x7a03 waiting on condition [0x00007f8a8c1f8000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000e1a2a8c0> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
at com.example.UserService.transfer(UserService.java:25)
at com.example.TransferController.transfer(TransferController.java:15)
...线程状态解析:
| 状态 | 说明 | 可能问题 |
|---|---|---|
RUNNABLE | 运行中或等待 CPU | CPU 使用率高 |
BLOCKED | 等待获取监视器锁 | 锁竞争激烈 |
WAITING | 等待其他线程通知 | 未被唤醒、等待队列 |
TIMED_WAITING | 有超时的等待 | sleep、wait、park |
NEW | 新建未启动 | - |
TERMINATED | 已终止 | - |
常见问题诊断:
- CPU 飙高:
# 找到 CPU 使用率高的进程
top -H -p <pid>
# 找到 CPU 使用率高的线程
printf "%x\n" <tid> # 转换为十六进制
# 在 jstack 输出中搜索
jstack <pid> | grep -A 20 <hex_tid>- 死锁检测:
jstack -l <pid> | grep -A 10 "Found one Java-level deadlock"示例输出:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f8a9c0034c8 (object 0x00000000e1a2a8c0, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x00007f8a9c0032c8 (object 0x00000000e1a2a8d0, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.example.DeadlockDemo.run(DeadlockDemo.java:25)
- waiting to lock <0x00000000e1a2a8c0> (a java.lang.Object)
- locked <0x00000000e1a2a8d0> (a java.lang.Object)
"Thread-2":
at com.example.DeadlockDemo.run(DeadlockDemo.java:35)
- waiting to lock <0x00000000e1a2a8d0> (a java.lang.Object)
- locked <0x00000000e1a2a8c0> (a java.lang.Object)
Found 1 deadlock.4.2 jconsole:图形化监控工具
启动方式:
jconsole功能介绍:
-
内存监控:
- 堆内存使用情况
- 非堆内存使用情况
- 内存池使用情况
- GC 统计
-
线程监控:
- 线程数量
- 线程状态分布
- 线程堆栈查看
- 死锁检测
-
类加载监控:
- 已加载类数量
- 已卸载类数量
-
MBean 管理:
- 查看和操作 MBean
- 修改配置参数
使用场景:
# 1. 启动应用,开启 JMX 远程监控
java -Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9010 \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false \
-jar app.jar
# 2. 使用 jconsole 连接
# 本地进程: 选择本地 Java 进程
# 远程进程: 输入 hostname:port线程监控界面:
线程数: 25
峰值: 30
总启动线程数: 45
线程列表:
- main (RUNNABLE)
- http-nio-8080-exec-1 (WAITING)
- http-nio-8080-exec-2 (BLOCKED)
- ...
点击"检测死锁"按钮,自动检测是否存在死锁4.3 VisualVM:全功能性能分析工具
启动方式:
jvisualvm主要功能:
-
内存分析:
- 堆内存使用趋势
- 堆 dump 分析
- 内存泄漏检测
- 对象统计
-
CPU 分析:
- CPU 采样
- 方法执行时间统计
- 热点方法识别
-
线程分析:
- 线程时间线
- 线程状态可视化
- 线程 dump
-
插件扩展:
- VisualGC: GC 可视化
- BTrace: 动态追踪
- JConsole 插件
内存泄漏排查:
// 1. 应用启动时开启 JMX
java -Dcom.sun.management.jmxremote=true \
-Dcom.sun.management.jmxremote.port=9010 \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false \
-jar app.jar
// 2. VisualVM 连接后,点击"堆 Dump"
// 3. 在堆 dump 页面,点击"类",按实例数排序
// 4. 找到实例数异常多的类
// 5. 右键点击类,选择"在堆中显示"
// 6. 查看对象引用链,找到 GC RootCPU 热点分析:
// 1. VisualVM 连接应用
// 2. 点击"Sampler"标签
// 3. 点击"CPU"按钮开始采样
// 4. 执行业务操作
// 5. 点击"Stop"停止采样
// 6. 查看方法执行时间排序
// 7. 找到耗时最长的方法
// 示例输出:
Method Time (ms) Invocations
com.example.UserService.getUser 5000 100
com.example.UserService.queryDB 4500 100
com.example.CacheService.get 500 10004.4 Arthas:阿里开源的 Java 诊断工具
安装与启动:
# 下载并启动
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
# 选择要诊断的 Java 进程
# Arthas 会自动列出所有 Java 进程常用命令:
- 查看线程信息:
# 查看所有线程
thread
# 查看指定线程
thread <thread_id>
# 查看 CPU 使用率最高的 3 个线程
thread -n 3
# 查看处于 BLOCKED 状态的线程
thread -state BLOCKED
# 检测死锁
thread -b示例输出:
thread -n 3
"C1 CompilerThread0" [Internal] cpuUsage=45.23% deltaTime=0ms time=12345ms
"C2 CompilerThread0" [Internal] cpuUsage=32.15% deltaTime=0ms time=11234ms
"http-nio-8080-exec-1" cpuUsage=12.34% deltaTime=0ms time=5678ms- 查看方法执行情况:
# 监控方法执行时间
trace com.example.UserService getUser
# 监控方法调用路径
trace com.example.UserService getUser '#cost > 100' # 只显示耗时 > 100ms 的调用
# 监控方法调用次数和成功率
monitor -c 5 com.example.UserService getUser # 每 5 秒输出一次统计示例输出:
`---[12.345ms] com.example.UserService:getUser()
+---[0.123ms] com.example.CacheService:get()
`---[12.222ms] com.example.UserDao:queryById()
`---[12.111ms] java.sql.PreparedStatement:executeQuery()- 查看方法调用参数和返回值:
# 查看方法入参和返回值
watch com.example.UserService getUser '{params, returnObj}' -x 2
# 查看方法入参、返回值和异常
watch com.example.UserService getUser '{params, returnObj, throwExp}' -x 2
# 条件过滤
watch com.example.UserService getUser '{params, returnObj}' 'params[0] > 100' -x 2示例输出:
method=com.example.UserService.getUser location=AtExit
ts=2024-01-01 10:00:00; result=@ArrayList[
@Object[][
@Integer[1],
],
@User[
id=@Integer[1],
name=@String[张三],
age=@Integer[25],
],
]- 动态修改日志级别:
# 查看日志级别
logger
# 修改日志级别
logger --name com.example --level debug- 查看类信息:
# 查看类信息
sc -d com.example.UserService
# 查看类的方法
sm com.example.UserService
# 查看 JVM 已加载的类
sc -d *UserService*- 反编译类:
# 反编译类
jad com.example.UserService
# 反编译指定方法
jad com.example.UserService getUser- 查看 JVM 信息:
# 查看 JVM 基本信息
jvm
# 查看 GC 信息
memory
# 查看 ClassLoader
classloader实战案例:排查慢接口:
# 1. 使用 trace 监控接口方法
trace com.example.OrderController createOrder
# 2. 发现某方法耗时较长
`---[1234.567ms] com.example.OrderController:createOrder()
+---[1.234ms] com.example.ValidationService:validate()
+---[1233.333ms] com.example.InventoryService:checkStock()
| `---[1233.111ms] com.example.InventoryDao:queryStock()
| `---[1232.999ms] java.sql.PreparedStatement:executeQuery()
`---[0.001ms] com.example.OrderService:createOrder()
# 3. 进一步追踪数据库查询
trace com.example.InventoryDao queryStock
# 4. 发现 SQL 执行慢
`---[1232.999ms] com.example.InventoryDao:queryStock()
`---[1232.888ms] java.sql.PreparedStatement:executeQuery()
# 5. 查看数据库连接池状态
vmtool --action getInstances --className com.zaxxer.hikari.HikariDataSource --express 'instances.{? #this.poolName == "mypool"}.{#this.getHikariPoolMXBean().{#this.getActiveConnections(), #this.getIdleConnections(), #this.getThreadsAwaitingConnection()}}'4.5 工具对比
| 工具 | 类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| jstack | 命令行 | 快速、轻量、无需安装 | 只能看线程堆栈 | 线程问题排查 |
| jconsole | 图形化 | 简单易用、功能全面 | 性能分析较弱 | 日常监控 |
| VisualVM | 图形化 | 功能强大、插件丰富 | 内存占用较高 | 性能分析 |
| Arthas | 命令行 | 功能强大、动态诊断、无需重启 | 学习成本较高 | 线上问题诊断 |
版本差异(旧版 → Java 21)
| 特性 | 旧版(Java 8/11) | Java 21 |
|---|---|---|
| 竞态条件/死锁 | 分析手段不变 | 不变;虚拟线程引入新的并发场景 |
| 排查工具 | jstack/jmap/jcmd | 不变;新增虚拟线程堆栈识别 |
| 虚拟线程栈 | 无 | jstack 中虚拟线程显示为 virtual 标记 |
| 线程耗尽风险 | 平台线程数受限 | 虚拟线程无上限,但针垫效应(synchronized 阻塞)需关注 |
| 监控护栏 | 线程数/队列深度 | 新增虚拟线程调度器指标(jdk.virtualThreadScheduler) |