【JUC】线程安全
1. 什么叫线程安全?
一句话:
多个线程同时访问同一份共享资源时,不管线程怎么切换、怎么交叉执行,最终结果都始终正确,这就叫线程安全。
比如:
int i = 0;
i++;
如果只有一个线程执行 100 次,最后一定是 100。
但如果两个线程分别执行 100 次,理论上期望结果是 200。
可是实际可能是 188、193、199,甚至更低。
这就说明它线程不安全。
因为多个线程同时修改同一个变量 i,执行结果受到线程切换时机影响。
2. 线程安全的本质是什么?
线程安全的本质就是解决三个问题:
1. 原子性
原子性指的是:
一个操作要么完整执行完,要么完全不执行,中间不能被其他线程打断。
比如 i++ 看起来是一行代码,但它不是一个不可分割的操作。
它大概可以拆成三步理解:
读取 i 的值
对 i 加 1
把新值写回 i
所以 i++ 不是原子操作。
如果两个线程同时执行,就可能出现:
初始 i = 0
线程 A 读取 i = 0
线程 B 读取 i = 0
线程 A 计算 0 + 1 = 1
线程 B 计算 0 + 1 = 1
线程 A 写回 i = 1
线程 B 写回 i = 1
最后结果是 1,而不是 2。
问题出在:两个线程都基于旧值 0 计算,导致其中一次修改被覆盖了。
这就是典型的原子性问题。
2. 可见性
可见性指的是:
一个线程修改了共享变量,其他线程能不能立刻看到这个修改。
比如:
boolean flag = true;
线程 A:
while (flag) {
// do something
}
线程 B:
flag = false;
你期望线程 B 把 flag 改成 false 后,线程 A 停止循环。
但在多线程环境下,线程 A 可能一直读取的是自己工作内存里的旧值 true,看不到线程 B 修改后的 false。
这就是可见性问题。
解决可见性问题常见方式:
volatile boolean flag = true;
或者使用:
synchronized
Lock
AtomicInteger
这些都可以在一定程度上保证可见性。
3. 有序性
有序性指的是:
程序代码的实际执行顺序,可能和你写的顺序不完全一致。
编译器和 CPU 为了优化性能,可能会对指令进行重排序。
比如你写的是:
a = 1;
b = 2;
在单线程里,只要结果不变,CPU 可能先执行 b = 2,再执行 a = 1。
单线程一般没问题,因为最终结果一样。
但多线程下,另一个线程可能会观察到中间状态,导致逻辑错误。
所以多线程下不仅要关心“代码写了什么”,还要关心:
其他线程看到的执行顺序是不是符合预期。
3. 为什么 i++ 会有线程安全问题?
因为 i++ 同时涉及三个动作:
读 i
加 1
写回 i
它不是原子操作。
你写的是:
i++;
但底层不是一步完成的。
可以理解成:
int temp = i;
temp = temp + 1;
i = temp;
多线程问题就发生在这里:
线程 A 读到 i = 0
线程 B 也读到 i = 0
线程 A 写回 1
线程 B 也写回 1
本来两个线程各加一次,应该变成 2。
但因为两个线程都读到了旧值 0,最终只加了一次的效果。
这叫丢失更新。
4. 线程切换为什么会导致问题?
多线程是抢占式调度的。
也就是说,线程 A 执行到一半,CPU 可能突然切换去执行线程 B。
例如:
线程 A 执行:读取 i = 0
线程 A 被挂起
线程 B 执行:读取 i = 0
线程 B 执行:计算 1
线程 B 执行:写回 i = 1
线程 A 恢复:继续基于之前读到的 0 计算
线程 A 写回 i = 1
结果还是 1。
所以问题不是 i++ 写法简单不简单,而是它在底层执行过程中,中间状态被其他线程插入了。
5. 怎么解决 i++ 的线程安全问题?
方案一:使用 synchronized
class Counter {
private int i = 0;
public synchronized void increment() {
i++;
}
public synchronized int get() {
return i;
}
}
synchronized 保证同一时刻只有一个线程能进入这个方法。
也就是说:
线程 A 执行 i++ 时,线程 B 不能进来
线程 A 执行完后,线程 B 才能执行
这样 i++ 就不会被交叉打断。
它解决了:
原子性
可见性
有序性
方案二:使用 Lock
class Counter {
private int i = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
i++;
} finally {
lock.unlock();
}
}
public int get() {
lock.lock();
try {
return i;
} finally {
lock.unlock();
}
}
}
Lock 和 synchronized 作用类似,也是让同一时刻只有一个线程操作共享变量。
区别是 Lock 更灵活,比如可以尝试加锁、可中断加锁、公平锁等。
方案三:使用原子类
AtomicInteger i = new AtomicInteger(0);
i.incrementAndGet();
AtomicInteger 的自增是原子操作。
底层通常依赖 CAS。
它的逻辑大概是:
先读取旧值
计算新值
尝试用 CAS 把旧值改成新值
如果中间没有其他线程修改成功,就更新成功
如果失败,就重新读取,再尝试
所以它适合简单的数值递增、递减、累加等场景。
6. volatile int i 可以解决 i++ 吗?
不能。
这个点很重要。
volatile int i = 0;
i++;
仍然线程不安全。
因为 volatile 主要解决的是:
可见性
禁止部分指令重排序
但它不保证复合操作的原子性。
i++ 是:
读
加
写
即使每次读写都能看到最新值,两个线程仍然可能同时读到同一个值,然后都写回相同的新值。
所以:
volatile int i;
i++;
不安全。
要解决 i++,应该用:
synchronized
Lock
AtomicInteger
7. 最后总结
你可以这样记:
线程安全问题的根源是:多个线程并发访问共享数据,并且至少有一个线程在修改数据。
只读一般不会有线程安全问题。
例如:
多个线程同时读一个变量
通常没问题。
但是:
一个线程写,另一个线程读
多个线程同时写
就可能有问题。
i++ 线程不安全,是因为它不是一个原子操作,而是由“读取、计算、写回”多个步骤组成。线程切换可能发生在这些步骤之间,导致多个线程基于同一个旧值计算,最终出现数据丢失。
一句话总结:
线程安全的本质,就是保证共享数据在多线程并发访问时,依然满足原子性、可见性和有序性,从而让程序结果始终正确。
更多推荐




所有评论(0)