1. 为什么 i++ 在多线程中不起作用?
从一个经典问题开始:我们有一个计数器变量,例如已处理的请求数量或下载的文件数量。我们希望多个线程同时增加这个计数器。如果只是写上 i++,会出什么问题?
示例:自增中的数据竞争
public class Counter {
public int count = 0;
public void increment() {
count++; // 不是原子操作!
}
}
设想两个线程同时调用 increment()。两个线程都读取旧值,都把它加 1,然后都写回……相同的新值!结果就是有一次自增被“丢失”。如果这种情况发生很多次,最终结果会比期望值更小。
为什么会这样?
操作 i++ 实际由三个步骤组成:
- 读取变量的值(例如 5)。
- 将该值加上 1。
- 把新值写回内存。
在多线程环境中,其他线程可能会在这些步骤之间修改变量。结果就是出现“数据竞争”(race condition)。
什么是原子操作?
原子操作 是要么全部执行、要么完全不执行的动作,其他任何线程都无法在该操作的中间“插入”。
Java 提供了一组用于基本类型和引用的此类操作的类,位于包 java.util.concurrent.atomic。最常用的是:
- AtomicInteger —— 原子的整数类型。
- AtomicLong —— 原子的 long。
- AtomicBoolean —— 原子的 boolean。
- AtomicReference<T> —— 指向任意类型对象的原子引用。
2. AtomicInteger:线程安全的计数器
声明与基础用法
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子递增
}
public int get() {
return count.get();
}
}
这里的 incrementAndGet() 会把“递增并返回新值”作为一个不可分割的操作来执行。即使有 100 个线程同时调用该方法,也不会有一次自增被丢失。
常用方法:
| 方法 | 说明 |
|---|---|
|
获取当前值 |
|
设置值 |
|
加 1 并返回新值 |
|
返回当前值并加 1 |
|
加上 delta 并返回新值 |
|
如果当前值等于 expect,则设置为 update(CAS) |
示例:多线程计数器
假设我们有一个类,用于统计聊天中已处理的消息数量。
public class MessageStatistics {
private final AtomicInteger messageCount = new AtomicInteger(0);
public void onMessageReceived() {
int newCount = messageCount.incrementAndGet();
System.out.println("总消息数:" + newCount);
}
public int getMessageCount() {
return messageCount.get();
}
}
内部原理:AtomicInteger 如何工作?
在内部,AtomicInteger 使用了处理器的特殊指令——CAS(Compare-And-Swap,“比较并交换”)。这是一种原子操作:比较变量的当前值与期望值是否相等,若相等则写入新值;如果在此期间有其他线程修改了变量——操作不会执行,并会重试。
工作流程:
1. 读取当前值(例如 5)
2. 与期望值比较(5)
3. 若相等——写入新值(6)
4. 若不相等——重试
这一切都在没有加锁(lock‑free)的情况下非常快速地完成。因此,原子类通常比 synchronized 更快,尤其是在线程数量很多的情况下。
3. AtomicReference:对象的原子引用
AtomicReference<T> 是一个通用的原子容器,适用于任何对象。它允许在多个线程中安全地更新对象引用。
示例:线程安全地更新引用
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceExample {
private final AtomicReference<String> latestMessage = new AtomicReference<>("");
public void updateMessage(String message) {
latestMessage.set(message);
}
public String getLatestMessage() {
return latestMessage.get();
}
}
compareAndSet 的应用
最有趣的操作是 compareAndSet(expected, newValue)。它允许仅在自上次读取后值未发生变化时才更新。
public void safeUpdate(String oldValue, String newValue) {
boolean success = latestMessage.compareAndSet(oldValue, newValue);
if (success) {
System.out.println("更新成功!");
} else {
System.out.println("值已被他人修改,请重试。");
}
}
这是无锁算法的基础:从队列、栈到缓存,只要需要避免不必要的加锁,就会用到它。
4. 在应用中的使用示例
示例 1:多线程消息计数器
public class ChatRoom {
private final AtomicInteger messageCount = new AtomicInteger(0);
public void receiveMessage(String message) {
// ... 处理消息 ...
int count = messageCount.incrementAndGet();
System.out.println("新消息:" + message + "。总消息数:" + count);
}
}
示例 2:安全地更新对最后一条消息的引用
public class ChatRoom {
private final AtomicReference<String> lastMessage = new AtomicReference<>("");
public void receiveMessage(String message) {
lastMessage.set(message);
// ... 处理 ...
}
public String getLastMessage() {
return lastMessage.get();
}
}
如果需要仅在最后一条消息未改变时才更新引用(以避免并发更新时“丢失”),请使用 compareAndSet。
5. 限制与陷阱
原子类何时不是万能药?
原子变量非常适合简单操作:自增、设值、检查并替换。但如果需要同时更新多个变量,原子性就无法保证了。举例来说,如果你有两个计数器,并希望把它们作为一个操作同时加 1——这就需要 synchronized 或其他同步机制。
错误用法示例
// 非原子!
if (ref.get() == null) {
ref.set("Hello");
}
在 get() 与 set(...) 之间,其他线程可能会修改该值,从而使条件不再成立。此类场景请使用 compareAndSet。
原子类 ≠ 线程安全对象
如果 AtomicReference 指向的对象本身不是线程安全的,那么替换引用是原子的,但修改对象的字段并不是。例如,如果你在 AtomicReference<List<String>> 中存放的是普通的 ArrayList,那么列表本身并不会变成线程安全(thread‑safe)。
6. 进阶原子类
在包 java.util.concurrent.atomic 中还有其他有用的类:
- AtomicLong、AtomicBoolean —— 用于 long 和 boolean。
- AtomicIntegerArray、AtomicReferenceArray —— 针对数组的原子操作。
- LongAdder、LongAccumulator —— 面向高并发计数场景。
LongAdder 与 LongAccumulator
如果线程非常多,普通的 AtomicInteger 成为“瓶颈”(所有线程竞争同一个变量),请使用 LongAdder。它将计数器拆分为多个内部单元,并在读取时求和,从而在高竞争场景下获得更好的性能。
import java.util.concurrent.atomic.LongAdder;
public class FastCounter {
private final LongAdder adder = new LongAdder();
public void increment() {
adder.increment();
}
public long getCount() {
return adder.sum();
}
}
7. 使用原子变量的常见错误
错误 1:指望复杂操作具备原子性。
如果需要对值执行多个步骤,原子类并不能保证——在各步骤之间其他线程可能会修改数据。对于复合操作,请使用 compareAndSet 或同步手段。
错误 2:忽视嵌套对象的线程安全。
如果 AtomicReference 中放的是普通对象,其方法和字段并不会自动变成线程安全。原子性的仅是引用的替换。
错误 3:不必要地使用原子类。
在单线程代码中,原子类型是多余的,并且由于额外的校验会比普通变量稍慢。
错误 4:过早优化。
有时使用 synchronized 更简单、更可靠,尤其是当逻辑复杂且涉及多个变量时。并不总是值得构建无锁(lock‑free)方案。
错误 5:忘记 ABA 问题。
较少见但很重要的情况:值从 A 变为 B 又变回 A——compareAndSet 会“以为”什么都没变。此类场景请使用专用类,例如 AtomicStampedReference(或 AtomicMarkableReference)。
GO TO FULL VERSION