在 Java 多线程编程中,多个线程同时操作共享变量时,若不进行合理的同步控制,极易引发数据不一致问题。本文将结合具体案例,深入分析多线程环境下操作共享变量的常见问题,并提供基于synchronized
和AtomicInteger
的解决方案,帮助开发者理解线程安全的核心原理。
一、问题场景:共享变量引发的线程安全问题
1.1 案例复现
假设我们有一个银行账户余额money
,需要模拟两个线程同时进行取款操作:
java
public class UnsafeWithdraw {
private static Integer money = 1000; // 共享变量
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
money = money - 200; // 线程A取款200
}, "A取钱");
Thread threadB = new Thread(() -> {
money = money - 300; // 线程B取款300
}, "B取钱");
threadA.start();
threadB.start();
}
}
1.2 问题分析
上述代码看似简单,却存在两个致命问题:
- 变量可见性问题:
Integer
是普通对象,未使用volatile
修饰,线程间修改可能不会及时同步,导致其他线程读取到旧值。 - 原子性缺失:
money = money - 200
由 3 步操作组成(读取、计算、写入),多线程下可能出现指令交错,引发竞态条件(Race Condition)。
例如,若两个线程同时读取到money=1000
,各自计算后写入800
和700
,最终结果可能不是预期的500
,而是错误的700
或800
。
二、解决方案一:使用synchronized
保证同步
2.1 原理分析
synchronized
关键字通过Monitor 锁机制保证同一时间只有一个线程进入同步块,确保操作的原子性和可见性。
2.2 代码实现
java
public class SynchronizedWithdraw {
private static Integer money = 1000;
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
synchronized (SynchronizedWithdraw.class) { // 同步锁
int temp = money;
temp -= 200;
money = temp;
System.out.println(Thread.currentThread().getName() + " 余额:" + money);
}
}, "A取钱");
Thread threadB = new Thread(() -> {
synchronized (SynchronizedWithdraw.class) { // 同一锁对象
int temp = money;
temp -= 300;
money = temp;
System.out.println(Thread.currentThread().getName() + " 余额:" + money);
}
}, "B取钱");
threadA.start();
threadB.start();
threadA.join(); // 等待线程执行完毕
threadB.join();
System.out.println("最终余额:" + money); // 输出500
}
}
2.3 优缺点
- 优点:简单直观,适用于复杂同步逻辑(如多个共享变量的操作)。
- 缺点:高并发下锁竞争会导致性能瓶颈;锁范围需精准控制,避免过度同步。
三、解决方案二:使用AtomicInteger
实现无锁原子操作
3.1 核心原理
AtomicInteger
位于java.util.concurrent.atomic
包中,基于CAS(Compare-And-Swap)机制实现无锁原子操作:
- 读取当前值
current
; - 计算目标值
next = current + delta
; - 通过
CAS
比较并更新:若当前值仍为current
,则写入next
,否则重试。
3.2 常用方法
方法 | 说明 | 示例(初始值为 1000) |
---|---|---|
addAndGet(delta) | 先加后返回新值 | addAndGet(-200) → 800 |
getAndAdd(delta) | 先返回旧值再加 | getAndAdd(-200) → 1000,值变为 800 |
compareAndSet(expect, update) | 预期值匹配时更新 | 若当前值为 1000,则更新为 800 |
3.3 代码实现
java
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicWithdraw {
private static AtomicInteger money = new AtomicInteger(1000); // 原子变量初始化
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
int newMoney = money.addAndGet(-200); // 原子性取款200
System.out.println(Thread.currentThread().getName() + " 余额:" + newMoney);
}, "A取钱");
Thread threadB = new Thread(() -> {
int newMoney = money.addAndGet(-300); // 原子性取款300
System.out.println(Thread.currentThread().getName() + " 余额:" + newMoney);
}, "B取钱");
threadA.start();
threadB.start();
threadA.join();
threadB.join();
System.out.println("最终余额:" + money.get()); // 输出500
}
}
3.4 优缺点
- 优点:无锁机制,高并发下性能优于
synchronized
;API 简洁,适合单个变量的原子操作。 - 缺点:仅支持单个变量的原子操作;存在 ABA 问题(可通过
AtomicStampedReference
解决)。
四、补充方案:更多线程安全控制方法
4.1 volatile
关键字:保证可见性与有序性
volatile
关键字通过强制线程从主内存读取 / 写入变量,确保修改对其他线程的可见性,并禁止指令重排序。
注意:volatile
不保证原子性,仅适用于单次读 / 写操作。
java
public class VolatileDemo {
private static volatile boolean isReady = false; // 状态标记
public static void main(String[] args) {
new Thread(() -> {
while (!isReady) {
Thread.yield();
}
System.out.println("子线程开始执行");
}, "工作线程").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
isReady = true; // 主线程修改状态,子线程可见
}
}
4.2 Lock
接口(如ReentrantLock
):灵活锁控制
ReentrantLock
是显式锁,支持可中断锁、公平锁、条件变量等高级功能。
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private static Integer money = 1000;
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
lock.lock();
try {
money -= 200;
System.out.println("A取钱后余额:" + money);
} finally {
lock.unlock(); // 必须在finally中释放锁
}
}, "A取钱");
Thread threadB = new Thread(() -> {
if (lock.tryLock()) { // 尝试获取锁,避免阻塞
try {
money -= 300;
System.out.println("B取钱后余额:" + money);
} finally {
lock.unlock();
}
} else {
System.out.println("B获取锁失败,放弃操作");
}
}, "B取钱");
threadA.start();
threadB.start();
threadA.join();
threadB.join();
}
}
4.3 原子引用类:AtomicReference
与AtomicStampedReference
4.3.1 AtomicReference
:对象的原子操作
用于对对象引用的原子更新。
java
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceDemo {
private static class Account {
int balance;
public Account(int balance) { this.balance = balance; }
}
private static AtomicReference<Account> accountRef =
new AtomicReference<>(new Account(1000));
public static void main(String[] args) {
new Thread(() -> {
Account oldAccount = accountRef.get();
Account newAccount = new Account(oldAccount.balance - 200);
while (!accountRef.compareAndSet(oldAccount, newAccount)) {
oldAccount = accountRef.get();
newAccount = new Account(oldAccount.balance - 200);
}
}, "A取钱").start();
}
}
4.3.2 AtomicStampedReference
:解决 ABA 问题
通过给对象添加版本号,避免 CAS 操作中值被修改后又改回的问题。
java
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABAProblemSolution {
private static AtomicStampedReference<Integer> money =
new AtomicStampedReference<>(1000, 0);
public static void main(String[] args) {
int stamp = money.getStamp();
// 模拟线程A:先减200,再加200(产生ABA问题)
new Thread(() -> {
money.compareAndSet(1000, 800, stamp, stamp + 1);
money.compareAndSet(800, 1000, stamp + 1, stamp + 2);
}, "A线程").start();
// 模拟线程B:尝试将1000改为700(实际值未变,但版本号已变)
new Thread(() -> {
int currentStamp = money.getStamp();
while (!money.compareAndSet(1000, 700, currentStamp, currentStamp + 1)) {
currentStamp = money.getStamp();
}
System.out.println("B线程最终余额:" + money.getReference()); // 输出700
}, "B线程").start();
}
}
4.4 ThreadLocal
:线程封闭(避免共享)
ThreadLocal
为每个线程创建变量副本,使变量完全属于当前线程。
java
public class ThreadLocalDemo {
private static final ThreadLocal<Integer> threadMoney =
ThreadLocal.withInitial(() -> 1000);
public static void main(String[] args) {
new Thread(() -> {
int money = threadMoney.get();
money -= 200;
threadMoney.set(money);
System.out.println("A线程余额:" + threadMoney.get()); // 输出800
}, "A线程").start();
new Thread(() -> {
int money = threadMoney.get();
money -= 300;
threadMoney.set(money);
System.out.println("B线程余额:" + threadMoney.get()); // 输出700
}, "B线程").start();
}
}
五、全方案对比与选择指南
方案 | 核心机制 | 原子性 | 可见性 | 适用场景 | 典型案例 |
---|---|---|---|---|---|
普通变量 | 无同步 | 不保证 | 不保证 | 单线程环境 | 局部变量 |
volatile | 内存屏障 + 禁止重排序 | 单操作保证 | 保证 | 状态标记、单次读写变量 | isReady 、isShutdown |
synchronized | Monitor 锁(阻塞式) | 代码块内保证 | 保证 | 多变量复合操作、复杂同步逻辑 | 账户余额修改(多步骤操作) |
AtomicInteger | CAS(无锁) | 单变量原子操作 | 保证 | 整数加减、计数统计 | 计数器、库存扣减 |
ReentrantLock | 显式锁(可重入) | 自定义范围保证 | 保证 | 可中断锁、公平锁、条件变量 | 资源竞争激烈的临界区 |
AtomicReference | CAS(对象引用) | 对象级原子操作 | 保证 | 对象状态的原子更新 | 链表节点的原子替换 |
ThreadLocal | 线程副本(无共享) | 无共享需求 | 无(线程内可见) | 线程独立变量(避免共享) | 数据库连接、用户上下文 |
六、总结:选择策略与最佳实践
- 优先避免共享:能用
ThreadLocal
隔离变量,就无需同步。 - 简单数值操作:直接使用
AtomicInteger
/AtomicLong
。 - 复合操作 / 复杂逻辑:使用
synchronized
或ReentrantLock
。 - 对象级原子性:用
AtomicReference
;需防 ABA 问题时,用AtomicStampedReference
。 - 警惕性能陷阱:高并发下优先尝试无锁方案(CAS);低并发时选择代码简洁的方案。
通过合理组合这些工具,开发者可以在多线程共享变量的场景中,兼顾线程安全、性能与代码可读性,写出健壮的并发程序。