多线程下对同一变量的操作:从问题到解决方案

在 Java 多线程编程中,多个线程同时操作共享变量时,若不进行合理的同步控制,极易引发数据不一致问题。本文将结合具体案例,深入分析多线程环境下操作共享变量的常见问题,并提供基于synchronizedAtomicInteger的解决方案,帮助开发者理解线程安全的核心原理。

一、问题场景:共享变量引发的线程安全问题

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,各自计算后写入800700,最终结果可能不是预期的500,而是错误的700800

二、解决方案一:使用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)机制实现无锁原子操作:

  1. 读取当前值current
  2. 计算目标值next = current + delta
  3. 通过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 原子引用类:AtomicReferenceAtomicStampedReference

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内存屏障 + 禁止重排序单操作保证保证状态标记、单次读写变量isReadyisShutdown
synchronizedMonitor 锁(阻塞式)代码块内保证保证多变量复合操作、复杂同步逻辑账户余额修改(多步骤操作)
AtomicIntegerCAS(无锁)单变量原子操作保证整数加减、计数统计计数器、库存扣减
ReentrantLock显式锁(可重入)自定义范围保证保证可中断锁、公平锁、条件变量资源竞争激烈的临界区
AtomicReferenceCAS(对象引用)对象级原子操作保证对象状态的原子更新链表节点的原子替换
ThreadLocal线程副本(无共享)无共享需求无(线程内可见)线程独立变量(避免共享)数据库连接、用户上下文

六、总结:选择策略与最佳实践

  1. 优先避免共享:能用ThreadLocal隔离变量,就无需同步。
  2. 简单数值操作:直接使用AtomicInteger/AtomicLong
  3. 复合操作 / 复杂逻辑:使用synchronizedReentrantLock
  4. 对象级原子性:用AtomicReference;需防 ABA 问题时,用AtomicStampedReference
  5. 警惕性能陷阱:高并发下优先尝试无锁方案(CAS);低并发时选择代码简洁的方案。

通过合理组合这些工具,开发者可以在多线程共享变量的场景中,兼顾线程安全、性能与代码可读性,写出健壮的并发程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值