核心概念
线程安全(Thread Safety) 是指当多个线程访问同一个对象或执行同一段代码时,不需要额外的同步措施或调用方进行协调,程序依然能够正确运行,不会出现数据不一致、死锁等问题。
简单来说:多个线程同时访问,结果依然正确。
线程安全的定义
经典定义
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。 —— Brian Goetz《Java Concurrency in Practice》
三个核心要素
- 原子性(Atomicity):操作不可分割,要么全部执行,要么全部不执行
- 可见性(Visibility):一个线程的修改对其他线程立即可见
- 有序性(Ordering):程序执行的顺序按照代码的先后顺序执行
线程不安全的表现
1. 数据竞争(Data Race)
// 典型的线程不安全示例
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 非原子操作:读取、加1、写回
}
public int getCount() {
return count;
}
}
// 测试
UnsafeCounter counter = new UnsafeCounter();
// 10个线程各自执行1000次increment
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
}).start();
}
// 预期结果:10000
// 实际结果:<10000(如9856),发生了数据丢失
问题分析:
时间 | 线程A | 线程B | count值
-----|---------------|---------------|--------
T1 | 读取count=0 | | 0
T2 | | 读取count=0 | 0
T3 | 计算0+1=1 | | 0
T4 | | 计算0+1=1 | 0
T5 | 写回count=1 | | 1
T6 | | 写回count=1 | 1
结果:两次increment操作,count只增加了1次
2. 可见性问题
// 可见性问题示例
public class VisibilityProblem {
private boolean flag = false;
private int value = 0;
// 线程1执行
public void writer() {
value = 42;
flag = true;
}
// 线程2执行
public void reader() {
while (!flag) {
// 可能永远循环,因为看不到flag的修改
}
System.out.println(value); // 可能输出0或42
}
}
问题原因:
- CPU缓存:线程2的flag值在本地缓存中,看不到线程1的修改
- 编译器优化:编译器可能将
while(!flag)优化为if(!flag) while(true)
3. 有序性问题
// 有序性问题示例
public class OrderingProblem {
private int a = 0;
private boolean initialized = false;
// 线程1
public void init() {
a = 42; // 操作1
initialized = true; // 操作2
// 可能发生重排序:操作2在操作1之前执行
}
// 线程2
public void use() {
if (initialized) { // 操作3
int b = a * 2; // 操作4:可能a还是0
}
}
}
问题原因:
- 指令重排序:CPU或编译器为了性能,可能调整执行顺序
- 在单线程中,重排序不影响结果
- 在多线程中,重排序可能导致错误
线程安全的级别
根据《Java Concurrency in Practice》,线程安全可以分为5个级别:
1. 不可变(Immutable)
最高安全级别,对象创建后状态不能改变,天然线程安全。
// String类是不可变的
String str = "Hello";
str.concat(" World"); // 返回新对象,原对象不变
// 自定义不可变类
public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
// 修改操作返回新对象
public ImmutablePoint move(int dx, int dy) {
return new ImmutablePoint(x + dx, y + dy);
}
}
优点:
- 天然线程安全,无需同步
- 可以自由共享和缓存
- 简化并发编程
2. 绝对线程安全(Absolute Thread Safety)
无论运行时环境如何,调用者都不需要额外的同步措施。
// Java中几乎没有真正绝对线程安全的类
// 即使是Vector,也不是绝对线程安全的
Vector<Integer> vector = new Vector<>();
// 线程1
vector.add(1);
// 线程2:可能抛出ArrayIndexOutOfBoundsException
if (vector.size() > 0) { // 假设size=1
// 此时线程1删除了元素,size变为0
vector.get(0); // 抛出异常!
}
// 需要额外同步
synchronized (vector) {
if (vector.size() > 0) {
vector.get(0);
}
}
3. 相对线程安全(Relative Thread Safety)
最常见的线程安全级别,对象的单次操作是线程安全的,但特定顺序的连续调用需要额外同步。
// Vector、ConcurrentHashMap等属于这个级别
// 单次操作线程安全
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1); // 线程安全
Integer value = map.get("key"); // 线程安全
// 复合操作需要额外同步
// ❌ 不是线程安全的
if (!map.containsKey("key")) {
map.put("key", 1);
}
// ✅ 使用原子方法
map.putIfAbsent("key", 1);
4. 线程兼容(Thread Compatible)
对象本身不是线程安全的,但可以通过外部同步来安全使用。
// ArrayList、HashMap等属于这个级别
List<Integer> list = new ArrayList<>();
// ❌ 直接使用不安全
// 多个线程同时add会出问题
// ✅ 通过外部同步使用
synchronized (list) {
list.add(1);
}
// ✅ 或使用包装类
List<Integer> syncList = Collections.synchronizedList(new ArrayList<>());
syncList.add(1); // 内部自动同步
5. 线程对立(Thread Hostile)
无论是否采取同步措施,都无法在多线程环境中安全使用。
// 例如:System.setIn()、System.setOut()
// 这些方法会影响全局状态,即使同步也无法保证安全
// 线程对立的代码示例(反例)
public class ThreadHostile {
private static int id = 0;
public static void setId(int newId) {
// 直接修改静态变量,影响所有使用者
id = newId;
// 其他线程可能读到不一致的状态
}
}
线程安全的实现方式
1. 互斥同步(悲观锁)
// synchronized
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
// 或使用同步块
public void decrement() {
synchronized (this) {
count--;
}
}
}
// ReentrantLock
public class LockExample {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
2. 非阻塞同步(乐观锁)
// CAS(Compare-And-Swap)
public class CasExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 使用CAS实现
}
// 手动使用CAS
public void compareAndSet(int expect, int update) {
count.compareAndSet(expect, update);
}
}
3. 无同步方案
// ThreadLocal
public class ThreadLocalExample {
private static ThreadLocal<Integer> threadLocal =
ThreadLocal.withInitial(() -> 0);
public void increment() {
// 每个线程有自己的副本,无需同步
threadLocal.set(threadLocal.get() + 1);
}
public int get() {
return threadLocal.get();
}
}
// 栈封闭(局部变量)
public class StackConfinement {
public int calculate(int a, int b) {
// 局部变量在栈上,线程独享
int result = a + b;
return result;
}
}
实际场景分析
场景1:单例模式
// ❌ 线程不安全的懒加载
public class UnsafeSingleton {
private static UnsafeSingleton instance;
public static UnsafeSingleton getInstance() {
if (instance == null) {
instance = new UnsafeSingleton(); // 可能创建多个实例
}
return instance;
}
}
// ✅ 线程安全的DCL
public class SafeSingleton {
private static volatile SafeSingleton instance;
public static SafeSingleton getInstance() {
if (instance == null) {
synchronized (SafeSingleton.class) {
if (instance == null) {
instance = new SafeSingleton();
}
}
}
return instance;
}
}
// ✅ 静态内部类(推荐)
public class BestSingleton {
private BestSingleton() {}
private static class Holder {
private static final BestSingleton INSTANCE = new BestSingleton();
}
public static BestSingleton getInstance() {
return Holder.INSTANCE; // 类加载时保证线程安全
}
}
场景2:缓存实现
// ❌ 线程不安全的缓存
public class UnsafeCache {
private final Map<String, Object> cache = new HashMap<>();
public Object get(String key) {
if (!cache.containsKey(key)) {
Object value = loadFromDB(key);
cache.put(key, value); // 多线程可能重复加载
}
return cache.get(key);
}
}
// ✅ 线程安全的缓存
public class SafeCache {
private final ConcurrentHashMap<String, Object> cache =
new ConcurrentHashMap<>();
public Object get(String key) {
return cache.computeIfAbsent(key, k -> loadFromDB(k));
}
private Object loadFromDB(String key) {
// 从数据库加载数据
return new Object();
}
}
场景3:观察者模式
// ❌ 线程不安全的观察者
public class UnsafeObservable {
private final List<Observer> observers = new ArrayList<>();
public void addObserver(Observer o) {
observers.add(o); // add时可能有其他线程在遍历
}
public void notifyObservers() {
for (Observer o : observers) { // 遍历时可能有其他线程在add
o.update(); // 可能抛出ConcurrentModificationException
}
}
}
// ✅ 线程安全的观察者
public class SafeObservable {
private final List<Observer> observers =
new CopyOnWriteArrayList<>(); // 写时复制
public void addObserver(Observer o) {
observers.add(o); // 线程安全
}
public void notifyObservers() {
for (Observer o : observers) { // 读不加锁
o.update();
}
}
}
如何判断线程安全?
判断标准
- 是否有共享数据:没有共享数据,自然线程安全
- 共享数据是否可变:不可变数据,自然线程安全
- 访问是否同步:有同步措施,可能线程安全
- 操作是否原子:原子操作,可能线程安全
检查清单
// 问自己这些问题:
1. 是否有多个线程访问? // 单线程不存在问题
2. 是否有共享的可变状态? // 不可变对象天然安全
3. 是否有竞态条件? // 如check-then-act
4. 是否正确使用了同步? // synchronized/Lock
5. 是否有复合操作? // 需要原子性保证
6. 是否有内存可见性问题? // 需要volatile
7. 是否有死锁风险? // 锁的顺序
面试总结
核心要点
- 定义:多线程访问时,无需额外同步,结果依然正确
- 三要素:原子性、可见性、有序性
- 五级别:不可变、绝对安全、相对安全、线程兼容、线程对立
- 实现方式:互斥同步、非阻塞同步、无同步方案
答题模板
简明版:
- 线程安全是指多线程访问时,无需额外同步即可保证正确性
- 需要保证原子性、可见性、有序性
- 常见实现:synchronized、volatile、Lock、原子类、不可变对象
完整版:
- 定义:多线程环境下,无需调用方协调,对象行为依然正确
- 三大特性:
- 原子性:count++需要synchronized保证
- 可见性:共享变量需要volatile保证
- 有序性:防止指令重排序
- 安全级别:从不可变到线程对立,常见是相对线程安全
- 实现方式:
- 互斥同步:synchronized、Lock
- 非阻塞:CAS、原子类
- 无同步:ThreadLocal、不可变对象
- 实践建议:优先使用不可变对象、JUC工具类、避免过度同步
经典面试题回答
Q:如何保证线程安全?
A:
- 不可变对象:如String、Integer(推荐)
- 同步机制:synchronized、Lock保证互斥
- 原子类:AtomicInteger等CAS操作
- 并发容器:ConcurrentHashMap、CopyOnWriteArrayList
- ThreadLocal:线程本地存储
- volatile:保证可见性和有序性
Q:Vector是线程安全的吗?
A:Vector是相对线程安全的,单次操作安全,但复合操作不安全。例如:
if (vector.size() > 0) {
vector.get(0); // 可能抛异常
}
// 需要额外同步
synchronized (vector) {
if (vector.size() > 0) {
vector.get(0);
}
}
Q:如何检测代码是否线程安全?
A:
- Code Review:检查共享变量访问
- 压力测试:高并发场景测试
- 工具检测:FindBugs、ThreadSanitizer
- 单元测试:并发测试框架(如JCStress)