Java并发——共享资源并发风险

发布时间:2026/7/8 1:47:28

Java并发——共享资源并发风险 在多线程编程中线程之间共享资源是常态。然而当多个线程同时访问同一块内存区域时如果没有正确的同步机制就会引发一系列令人头疼的问题。数据覆盖、原子性破坏、临界资源竞争这些概念看似抽象却每天都在并发系统中上演。本文将深入剖析这三种常见的并发风险通过实例代码揭示其本质并提供应对策略。一、共享资源为何成为风险在单线程环境下代码执行顺序是确定的每次运行结果都是一致的。但在多线程环境中操作系统通过时间片轮转让多个线程“同时”运行线程的执行顺序变得不可预测。当多个线程同时读写共享变量时就会出现交错执行导致数据不一致。所谓共享资源就是指多个线程都能访问的变量或对象。例如实例变量堆内存静态变量方法区外部资源文件、数据库连接共享资源本身并不危险危险的是缺乏协调的并发访问。二、数据覆盖问题丢失的更新2.1 问题描述数据覆盖也称为“丢失更新”Lost Update是指两个线程同时对同一变量进行修改后一个线程的写入覆盖了前一个线程的结果导致部分更新丢失。2.2 典型案例计数器假设我们有一个计数器两个线程各执行100万次自增操作理论上最终结果应为200万。但实际运行往往小于200万。public class Counter { private int count 0; public void increment() { count; } public int getCount() { return count; } }启动两个线程每个线程调用increment()100万次Counter counter new Counter(); Thread t1 new Thread(() - { for (int i 0; i 1_000_000; i) { counter.increment(); } }); Thread t2 new Thread(() - { for (int i 0; i 1_000_000; i) { counter.increment(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.getCount()); // 通常输出小于2_000_0002.3 原因分析count并不是一个原子操作它在底层分解为三个步骤从内存读取count的值到寄存器。在寄存器中执行加1操作。将新值写回内存。当两个线程并发执行时可能出现如下交错线程1: 读取count (0) 线程2: 读取count (0) 线程1: 加1 (得到1) 线程2: 加1 (得到1) 线程1: 写回 (count1) 线程2: 写回 (count1)最终结果只有1丢失了一次更新。这就是数据覆盖问题。三、原子性破坏非原子操作的风险3.1 什么是原子性原子性是指一个操作要么全部执行且不受干扰要么完全不执行。在Java中对基本类型变量的赋值和读取通常是原子的long和double例外但像i、i 1这样的复合操作不是原子的。3.2 原子性被破坏的演示除了i任何包含“读-改-写”模式的操作都是非原子的。例如a b c同样涉及读取、计算、赋值三步。当这些步骤之间被其他线程插入时数据就会出错。3.3 字节码层面看原子性使用javap -c Counter查看increment方法的字节码public void increment(); Code: 0: aload_0 1: dup 2: getfield #2 // 读取count 5: iconst_1 6: iadd // 加1 7: putfield #2 // 写回count 10: return可以看到getfield读、iadd加、putfield写三条指令它们之间没有任何锁保护因此线程可能在任意时刻被调度出去导致原子性被破坏。四、临界资源竞争超卖与负数票4.1 问题描述临界资源Critical Resource是指一次只能被一个线程访问的资源。当多个线程同时竞争临界资源且未加保护时就会出现资源被超额使用或状态异常的情况。最经典的例子就是售票系统。4.2 售票系统演示假设有100张票多个窗口线程同时卖票public class TicketSeller { private int tickets 100; public void sell() { if (tickets 0) { // 模拟售票耗时 try { Thread.sleep(10); } catch (InterruptedException e) {} tickets--; System.out.println(Thread.currentThread().getName() 售出一张剩余 tickets); } else { System.out.println(Thread.currentThread().getName() 票已售罄); } } }启动多个线程同时卖票TicketSeller seller new TicketSeller(); for (int i 0; i 20; i) { new Thread(() - { for (int j 0; j 10; j) { seller.sell(); } }, 窗口 i).start(); }运行结果可能出现同一张票被多个线程售出超卖剩余票数出现负数最终票数不等于初始值4.3 原因分析if (tickets 0)和tickets--之间不是原子的。当多个线程同时进入判断时它们都看到tickets大于0然后各自执行减操作导致票数被重复减少甚至出现负数。这种竞争导致了临界资源的不安全访问。五、应对策略让共享资源安全起来针对上述问题Java提供了多种同步机制5.1 synchronized 关键字同步方法锁住当前对象实例方法或Class对象静态方法。同步代码块指定任意对象作为锁粒度更细。public synchronized void increment() { // 实例方法锁 count; } public void sell() { synchronized (this) { if (tickets 0) { tickets--; } } }5.2 Lock 接口提供更灵活的锁操作如尝试锁、可中断锁、公平锁等。Lock lock new ReentrantLock(); lock.lock(); try { // 临界区 } finally { lock.unlock(); }5.3 原子类对于简单的计数器可以使用AtomicInteger等原子类内部通过CASCompare-And-Swap实现无锁并发。private AtomicInteger count new AtomicInteger(0); count.incrementAndGet(); // 原子自增5.4 volatile 与可见性volatile保证变量的可见性但不保证原子性。适用于单次读写的标志位。六、总结并发编程中的共享资源风险源于操作系统的时间片轮转和线程交错执行。数据覆盖、原子性破坏、临界资源竞争是三种最常见的并发问题它们本质都是对共享资源的多线程访问缺乏协调。要避免这些风险必须为临界区提供互斥保护确保同一时刻只有一个线程能执行关键操作。从synchronized到Lock再到Atomic类Java提供了丰富的工具开发者应根据场景选择合适的同步机制既保证正确性又兼顾性能。

相关新闻