在多线程编程中,线程池是提升系统性能的常用手段。但当多个线程通过线程池并发操作同一个共享变量时,稍有不慎就会引发数据错乱、状态不一致等问题。比如一个电商系统里,多个线程同时处理用户下单扣减库存,若没处理好共享变量的安全问题,就可能出现超卖。
问题从哪来?
线程池里的每个线程都可能读写同一个变量。由于CPU调度的不确定性,多个线程对共享变量的修改可能交错进行。比如两个线程同时执行 count++,这个操作其实包含读取、加1、写回三步,如果中间被打断,最终结果可能只加了一次,而不是两次。
用 synchronized 控制访问
最直接的办法是加锁。Java 中可以用 synchronized 关键字保证同一时间只有一个线程能进入临界区。
private int stock = 100;
public synchronized void decreaseStock() {
if (stock > 0) {
stock--;
}
}
这种方式简单有效,但锁的粒度如果太大,会降低并发效率,就像超市只有一个收银台,顾客再多也只能排长队。
用 AtomicInteger 等原子类
对于简单的计数或数值操作,推荐使用 java.util.concurrent.atomic 包下的原子类。它们底层利用了 CAS(比较并交换)机制,无需加锁也能保证线程安全。
private AtomicInteger stock = new AtomicInteger(100);
public void decreaseStock() {
if (stock.get() > 0) {
stock.decrementAndGet();
}
}
这种方式性能更好,适合高并发场景,比如秒杀系统的库存扣减。
避免共享,优先局部
最安全的共享变量,就是不共享。尽量让每个线程操作自己的局部变量,最后再合并结果。比如用线程池做一批数据统计,可以让每个线程先算出自己的小计,最后由主线程汇总。
ExecutorService executor = Executors.newFixedThreadPool(4);
List<Integer> results = new ArrayList<>();
for (int i = 0; i < 4; i++) {
executor.submit(() -> {
int localSum = 0;
// 处理各自的数据块
for (int j = 0; j < 1000; j++) {
localSum += j;
}
synchronized (results) {
results.add(localSum);
}
});
}
这样只有最后汇总时才需要同步,大大减少了竞争。
使用 ThreadLocal 隔离数据
有时候我们希望每个线程有自己的变量副本,互不干扰。ThreadLocal 就是干这个的。比如记录每个请求的用户信息,可以用 ThreadLocal 存储,避免传参麻烦又保证隔离。
private static ThreadLocal<String> userHolder = new ThreadLocal<>();
// 设置当前线程的数据
userHolder.set("user123");
// 获取
String user = userHolder.get();
注意用完记得调用 remove(),防止内存泄漏,尤其是在线程池这种线程复用的场景。
合理设计数据结构
有些集合天生就不怕并发。比如 ConcurrentHashMap,允许多个线程同时读写,性能比加锁的 HashMap 高很多。在共享变量是容器时,优先考虑这类线程安全的实现。
ConcurrentHashMap<String, Integer> cache = new ConcurrentHashMap<>();
cache.putIfAbsent("key1", 100);
它不仅能避免冲突,还提供了很多原子操作方法,用起来更安心。
线程池本身是为了高效,但如果共享变量没管好,反而会拖慢系统甚至引发严重 bug。关键不是一味加锁,而是根据场景选择合适的方法:原子类、无锁结构、局部变量、ThreadLocal 或并发容器,让并发既安全又高效。