Java线程安全问题的3种主流解决方案
在Java并发编程中,多个线程同时访问共享变量往往导致数据不一致。本文通过实际演示非线程安全的现象,详细介绍如何使用AtomicInteger原子类、synchronized同步锁以及ThreadLocal线程本地变量这三种主流机制来保障数据一致性,并对比各方案的操作特点与适用场景。
线程安全问题演示
线程安全是指代码在多线程环境下能够正确执行,不会出现数据错误或污染。为了直观展示非线程安全的问题,我们构建一个基础场景:定义全局变量 number 初始值为0,创建两个线程,分别对 number 执行100万次自增(++)和自减(--)操作。若程序完全线程安全,最终结果应为0;否则即为存在竞争条件。
public class ThreadSafeTest {
// 全局变量
private static int number = 0;
// 循环次数(100W)
private static final int COUNT = 1_000_000;
public static void main(String[] args) throws InterruptedException {
// 线程1:执行 100W 次 ++ 操作
Thread t1 = new Thread(() -> {
for (int i = 0; i < COUNT; i++) {
number++;
}
});
t1.start();
// 线程2:执行 100W 次 -- 操作
Thread t2 = new Thread(() -> {
for (int i = 0; i < COUNT; i++) {
number--;
}
});
t2.start();
// 等待线程 1 和线程 2 执行完
t1.join();
t2.join();
System.out.println("number 最终结果:" + number);
}
}
上述程序的执行结果如下所示:
从输出结果可以看出,number 的最终值通常不为0。这是因为 ++ 和 -- 并非原子操作,它们包含读取、修改、写回三个步骤,多线程交替执行时会导致部分计算被覆盖,这就是典型的线程安全问题。
解决线程安全问题
针对上述问题,Java 提供了多种解决方案。主要手段包括使用线程安全类、加锁排队执行或使用线程本地变量。接下来我们将逐一介绍具体实现。
第1步:使用原子类AtomicInteger
AtomicInteger 是 Java 并发包中提供的线程安全类,它基于CAS(Compare-And-Swap)算法将基本操作的读-改-写过程变为原子性操作,无需显式加锁即可解决并发问题。
将之前的 int 类型替换为 AtomicInteger,并使用 incrementAndGet() 和 decrementAndGet() 方法:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerExample {
// 创建 AtomicInteger
private static AtomicInteger number = new AtomicInteger(0);
// 循环次数
private static final int COUNT = 1_000_000;
public static void main(String[] args) throws InterruptedException {
// 线程1:执行 100W 次 ++ 操作
Thread t1 = new Thread(() -> {
for (int i = 0; i < COUNT; i++) {
number.incrementAndGet();
}
});
t1.start();
// 线程2:执行 100W 次 -- 操作
Thread t2 = new Thread(() -> {
for (int i = 0; i < COUNT; i++) {
number.decrementAndGet();
}
});
t2.start();
// 等待线程执行完毕
t1.join();
t2.join();
System.out.println("最终结果:" + number.get());
}
}
运行该程序,结果如下图所示:
可以看到,最终结果准确地返回了0。这种方式性能较高,适合简单的计数或状态更新场景。
第2步:加锁排队执行
通过锁机制确保同一时刻只有一个线程能访问共享资源,从而实现排队执行。Java中主要提供两种锁:synchronized 关键字和 ReentrantLock。
2.1 同步锁 synchronized
synchronized 是 JVM 层面实现的自动加锁和释放锁机制,使用简单且不易出错。只需在需要保护的代码块前加上 synchronized (锁对象) 即可。
public class SynchronizedExample {
private static int number = 0;
private static final int COUNT = 1_000_000;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < COUNT; i++) {
synchronized (SynchronizedExample.class) {
number++;
}
}
});
t1.start();
Thread t2 = new Thread(() -> {
for (int i = 0; i < COUNT; i++) {
synchronized (SynchronizedExample.class) {
number--;
}
}
});
t2.start();
t1.join();
t2.join();
System.out.println("number 最终结果:" + number);
}
}
执行效果如下图所示:
2.2 可重入锁 ReentrantLock
ReentrantLock 是 API 级别的锁,需要程序员手动调用 lock.lock() 和 lock.unlock()。虽然代码略显繁琐,但它提供了更灵活的锁控制能力(如尝试获取锁、公平锁等)。
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private static int number = 0;
private static final int COUNT = 1_000_000;
// 创建 ReentrantLock
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < COUNT; i++) {
lock.lock(); // 手动加锁
try {
number++;
} finally {
lock.unlock(); // 手动释放锁
}
}
});
t1.start();
Thread t2 = new Thread(() -> {
for (int i = 0; i < COUNT; i++) {
lock.lock();
try {
number--;
} finally {
lock.unlock();
}
}
});
t2.start();
t1.join();
t2.join();
System.out.println("number 最终结果:" + number);
}
}
注意:在实际生产环境中,建议使用 try-finally 块包裹 unlock() 调用,以确保即使发生异常也能正确释放锁,防止死锁。原文代码未包含此保护,此处已优化逻辑描述,但保留原图对应位置。
执行结果如下所示:
第3步:使用线程本地变量 ThreadLocal
ThreadLocal 提供了线程局部的变量副本。每个线程都会拥有自己的变量独立副本,互不干扰,从而从根本上避免竞争。适用于上下文传递或存储线程特有状态的场景。
实现思路:每个线程操作自己内部的计数器,最后再将各自的结果汇总到全局变量中。务必在 finally 块中调用 remove() 以防止内存泄漏。
public class ThreadLocalExample {
// 创建 ThreadLocal(设置每个线程中的初始值为 0)
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
private static int number = 0;
private static final int COUNT = 1_000_000;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
for (int i = 0; i < COUNT; i++) {
threadLocal.set(threadLocal.get() + 1);
}
number += threadLocal.get();
} finally {
threadLocal.remove(); // 清除资源
}
});
t1.start();
Thread t2 = new Thread(() -> {
try {
for (int i = 0; i < COUNT; i++) {
threadLocal.set(threadLocal.get() - 1);
}
number += threadLocal.get();
} finally {
threadLocal.remove(); // 清除资源
}
});
t2.start();
t1.join();
t2.join();
System.out.println("最终结果:" + number);
}
}
程序执行结果如下图所示:
总结
解决Java线程安全问题主要有三种途径:
- 原子类:如
AtomicInteger,适用于轻量级状态更新,性能开销低。 - 锁机制:如
synchronized或ReentrantLock,适用于复杂逻辑的资源保护,确保排他性。 - 线程本地变量:如
ThreadLocal,适用于隔离线程内部状态,消除共享竞争。
以上就是Java线程安全问题的3种主流解决方案的详细内容,更多关于Java多线程开发技巧的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









