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();
        }
    }
}

Locksynchronized 作用类似,也是让同一时刻只有一个线程操作共享变量。

区别是 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++ 线程不安全,是因为它不是一个原子操作,而是由“读取、计算、写回”多个步骤组成。线程切换可能发生在这些步骤之间,导致多个线程基于同一个旧值计算,最终出现数据丢失。

一句话总结:

线程安全的本质,就是保证共享数据在多线程并发访问时,依然满足原子性、可见性和有序性,从而让程序结果始终正确。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐