一、前言
线程安全是 Java 并发编程的核心问题。当多个线程同时访问共享数据时,如果没有正确的同步机制,可能导致数据不一致、脏读、死锁等问题。本文将从入门到精通,系统介绍 Java 中的线程安全实现方式、适用场景以及优缺点。
二、什么是线程安全?
线程安全:在多线程环境下,程序能够按照预期正确执行,不会出现数据错误或异常行为。
常见线程安全问题:
- 竞态条件:多个线程同时修改共享变量,结果不可预期。
- 可见性问题:线程对变量的修改,其他线程不可见。
- 指令重排序:CPU 和编译器优化导致执行顺序与预期不符。
Java 内存模型(JMM)保证:
- 原子性:基本操作不可分割。
- 可见性:通过
volatile、锁等保证修改对其他线程可见。 - 有序性:通过
happens-before规则保证执行顺序。
三、实现线程安全的方式
1. 不可变对象(Immutable Object)
- 实现方式:使用
final修饰字段,不提供修改方法。 - 示例:
public final class SafePoint {
private final int x;
private final int y;
public SafePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
}
- 优点:简单、安全、无需同步。
- 缺点:需要复制对象,开销较大。
- 适用场景:值对象、配置类、常量类。
2. synchronized 关键字
- 作用:保证原子性、可见性和有序性。
- 示例:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
- 优点:易用、JVM 保证。
- 缺点:阻塞,性能较低,容易导致死锁。
- 适用场景:小规模并发,读写比例均衡。
3. 显式锁(ReentrantLock, ReadWriteLock, StampedLock)
- 示例:
ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
// 临界区
} finally {
lock.unlock();
}
- 优点:更灵活,可实现公平锁、可中断锁、读写分离。
- 缺点:代码复杂度增加,可能忘记释放锁。
- 适用场景:高并发、读多写少(使用 ReadWriteLock / StampedLock)。
4. volatile 关键字
- 保证可见性和禁止指令重排序,但 不保证原子性。
- 示例:
private volatile boolean running = true;
public void stop() {
running = false;
}
- 优点:轻量级,性能好。
- 缺点:不能替代锁,不适合复杂操作。
- 适用场景:状态标记、单例双检锁。
5. 原子类(AtomicInteger, LongAdder 等)
- 基于 CAS(Compare And Swap) 实现原子操作。
- 示例:
AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
- 优点:性能好,无锁实现。
- 缺点:ABA 问题,长时间自旋消耗 CPU。
- 适用场景:计数器、累加器。
6. 并发集合(ConcurrentHashMap, CopyOnWriteArrayList 等)
- 示例:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
- 优点:线程安全,高性能。
- 缺点:CopyOnWrite 开销大,适合读多写少。
- 适用场景:共享集合,缓存。
7. 线程本地变量(ThreadLocal)
- 示例:
private static ThreadLocal<SimpleDateFormat> sdf =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
- 优点:避免共享,天然线程安全。
- 缺点:内存泄漏风险,使用不当可能导致问题。
- 适用场景:用户会话、连接管理、格式化器。
8. 并发工具类(Semaphore, CountDownLatch, CyclicBarrier, Phaser)
- 示例(CountDownLatch):
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 执行任务
latch.countDown();
}).start();
}
latch.await(); // 等待所有线程完成
- 优点:简化线程协调。
- 缺点:场景有限,学习成本较高。
- 适用场景:任务分发、批处理、并发控制。
四、方式对比
| 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 不可变对象 | 简单、安全 | 需要复制对象 | 值对象、配置 |
| synchronized | 易用、可靠 | 性能差、阻塞 | 小规模并发 |
| ReentrantLock | 灵活 | 代码复杂 | 高并发、读写分离 |
| volatile | 轻量 | 无原子性 | 状态标志 |
| 原子类 | 性能好 | ABA 问题 | 计数器 |
| 并发集合 | 高性能 | 部分开销大 | 缓存、集合 |
| ThreadLocal | 无共享 | 内存泄漏风险 | 会话、格式化器 |
| 并发工具类 | 协调简单 | 适用面窄 | 任务协作 |
五、最佳实践
- 优先使用不可变对象,减少共享。
- 读多写少时优先使用
ReadWriteLock、CopyOnWriteArrayList。 - 计数器类优先使用
LongAdder而不是AtomicInteger。 - 共享集合推荐
ConcurrentHashMap。 - 跨线程协作用
CountDownLatch或CyclicBarrier。 - 避免滥用 ThreadLocal,必须手动清理。
典型线程安全实战示例:
1. 线程安全计数器(AtomicLong vs LongAdder)
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
public class SafeCounterDemo {
private static final int THREADS = 10;
private static final int ITERATIONS = 100_000;
public static void main(String[] args) throws InterruptedException {
AtomicLong atomicCount = new AtomicLong(0);
LongAdder adderCount = new LongAdder();
Thread[] threads = new Thread[THREADS];
// AtomicLong 计数
for (int i = 0; i < THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < ITERATIONS; j++) {
atomicCount.incrementAndGet();
}
});
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println("AtomicLong 计数结果: " + atomicCount.get());
// LongAdder 计数
for (int i = 0; i < THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < ITERATIONS; j++) {
adderCount.increment();
}
});
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println("LongAdder 计数结果: " + adderCount.sum());
}
}
✅ 对比:AtomicLong 在高并发下可能会成为瓶颈,而 LongAdder 分段累加性能更好。
2. 线程池 + CountDownLatch(等待所有子任务完成)
import java.util.concurrent.*;
public class ThreadPoolCountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int taskCount = 5;
ExecutorService executor = Executors.newFixedThreadPool(taskCount);
CountDownLatch latch = new CountDownLatch(taskCount);
for (int i = 0; i < taskCount; i++) {
int taskId = i;
executor.submit(() -> {
try {
System.out.println("任务 " + taskId + " 开始执行");
Thread.sleep(1000L * (taskId + 1)); // 模拟耗时任务
System.out.println("任务 " + taskId + " 完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
// 等待所有任务完成
latch.await();
System.out.println("所有任务已完成,继续主线程逻辑");
executor.shutdown();
}
}
✅ 场景:适合批处理任务,比如并行下载文件、并行计算,等待全部完成后合并结果。
3. 读多写少缓存(ReadWriteLock)
import java.util.concurrent.locks.*;
import java.util.*;
public class ReadWriteLockCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.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 static void main(String[] args) {
ReadWriteLockCache<String, String> cache = new ReadWriteLockCache<>();
cache.put("user:1", "Alice");
new Thread(() -> System.out.println("读取: " + cache.get("user:1"))).start();
new Thread(() -> cache.put("user:1", "Bob")).start();
}
}
✅ 场景:适合读多写少的缓存系统(例如配置中心、热点数据)。
4. 生产者-消费者模型(BlockingQueue)
import java.util.concurrent.*;
public class ProducerConsumerDemo {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
// 生产者
Runnable producer = () -> {
for (int i = 0; i < 10; i++) {
try {
queue.put(i);
System.out.println("生产 -> " + i);
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 消费者
Runnable consumer = () -> {
for (int i = 0; i < 10; i++) {
try {
Integer value = queue.take();
System.out.println("消费 <- " + value);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
new Thread(producer).start();
new Thread(consumer).start();
}
}
✅ 场景:任务队列、消息队列、线程池内部的任务调度。
六、总结
- Java 提供了多种线程安全实现方式,从语言级关键字(
synchronized,volatile),到并发包工具类,再到高性能无锁数据结构。 - 选择合适的方式取决于 并发场景、性能要求、代码复杂度。
- 最佳实践是:能不共享就不共享,能用不可变就用不可变,必须共享时选合适的同步手段。

被折叠的 条评论
为什么被折叠?



