Java中还有哪些方式可以保证多线程环境下的原子性?

在 Java 多线程环境中,保证原子性的核心是让 "读取 - 修改 - 写入" 这类复合操作不可分割(要么全执行,要么全不执行)。除了之前提到的 synchronizedLock,还有以下常用方式,涵盖 无锁方案、原子类、并发工具 等,附原理、场景和注意事项:

一、java.util.concurrent.atomic 原子类(无锁方案,首选)

Atomic 系列类是 JDK 1.5 引入的无锁并发工具,底层基于 CAS(Compare-And-Swap)操作 + volatile 实现原子性,无需加锁,性能优于 synchronized(高并发下无锁竞争开销)。

1. 核心原理

  • CAS 操作:CPU 提供的原子指令,核心逻辑是 "比较内存值与预期值,若相等则更新为新值,否则失败",整个过程原子化。
  • volatile 保证可见性 :原子类的变量被 volatile 修饰,确保修改后立即写回主内存,其他线程能实时读取最新值。
  • 自旋重试:若 CAS 操作失败,会循环重试直到成功(轻量级开销,适合低冲突场景)。

2. 常用原子类及场景

原子类 功能描述 适用场景
AtomicInteger/AtomicLong 基本类型的原子增减、赋值操作 计数器、序号生成器(如订单 ID)
AtomicBoolean 布尔值的原子修改(true/false 切换) 状态标记(如 "是否初始化完成")
AtomicReference 引用类型的原子赋值(支持泛型) 原子更新对象引用(如单例对象)
AtomicStampedReference 带版本号的原子引用(解决 ABA 问题) 避免 CAS 中的 ABA 漏洞(如链表操作)
AtomicIntegerFieldUpdater 原子更新对象的非 volatile 字段(反射实现) 无需修改原有类,对字段进行原子操作

3. 示例代码

java

运行

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

// 原子计数器:多线程并发自增,保证计数准确
public class AtomicCounter {
    // 原子类变量(volatile + CAS 保证原子性)
    private final AtomicInteger count = new AtomicInteger(0);

    // 原子自增(替代 count++,避免非原子操作)
    public void increment() {
        count.incrementAndGet(); // 底层:CAS 自旋重试,直到成功
    }

    // 原子赋值(替代 count = x)
    public void set(int value) {
        count.set(value);
    }

    // 原子累加(替代 count += x)
    public void add(int delta) {
        count.addAndGet(delta);
    }

    public int getCount() {
        return count.get();
    }
}

4. 注意事项

  • ABA 问题 :普通原子类(如 AtomicInteger)可能出现 "值被修改后又改回原值" 的 ABA 漏洞(例如线程 1 读取值为 A,线程 2 改为 B 再改回 A,线程 1 CAS 仍会成功)。解决方案:用 AtomicStampedReference(带版本号,每次修改版本号递增,避免误判)。
  • 高冲突场景性能下降 :若并发冲突频繁(如上千线程同时 CAS),自旋重试会消耗大量 CPU,此时需改用锁(synchronized/Lock)。
  • 不支持复合逻辑 :原子类仅支持单一操作(如 incrementAndGet()addAndGet()),若需多个原子操作组合(如 "先自增再判断是否超过阈值"),仍需额外同步。

二、java.util.concurrent.locks.ReentrantLock + Condition(灵活锁方案)

ReentrantLockLock 接口的核心实现,通过 显式加锁 / 解锁 保证同步块内操作的原子性,功能比 synchronized 更灵活(支持超时、中断、公平锁),原子性保证逻辑与 synchronized 一致(互斥执行)。

1. 原理

  • 同一时间只有一个线程能获取锁,同步块内的所有操作(包括复合操作)会被 "包裹" 为原子执行单元,其他线程需等待锁释放后才能进入。
  • 支持 可重入:同一线程可多次获取锁(需对应释放,避免死锁)。

2. 示例代码(原子执行复合操作)

java

运行

java 复制代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// 用 ReentrantLock 保证"自增+判断"的原子性
public class LockAtomicExample {
    private final Lock lock = new ReentrantLock();
    private int count = 0;
    private static final int MAX_COUNT = 1000;

    // 复合操作:自增后判断是否超过阈值(原子执行)
    public boolean incrementAndCheck() {
        lock.lock(); // 显式加锁
        try {
            count++; // 读取-修改-写入复合操作
            return count > MAX_COUNT; // 原子性保证:自增和判断不可分割
        } finally {
            lock.unlock(); // 必须在 finally 释放锁,避免锁泄露
        }
    }
}

3. 适用场景

  • 复合原子操作(如 "先修改再判断""多变量原子更新")。
  • 高并发场景下需要 公平锁 (按请求顺序获取锁,避免线程饥饿)或 超时等待(避免死锁)。

4. 注意事项

  • 必须在 finally 块中释放锁(否则线程异常时会导致锁泄露,其他线程无法获取)。
  • 公平锁性能较低(需维护等待队列),默认是非公平锁(优先唤醒正在自旋的线程,性能更优)。

三、java.util.concurrent.locks.StampedLock(读写分离锁,优化读多写少场景)

StampedLock 是 JDK 1.8 引入的新型锁,支持 乐观读、悲观读、写锁 三种模式,原子性保证依赖 "写锁的互斥性",适合 读多写少 的场景(读操作无锁竞争,性能远超 ReentrantLock)。

1. 核心原理

  • 写锁互斥:获取写锁后,其他线程无法获取读锁或写锁,保证写操作(如修改共享变量)的原子性。
  • 乐观读无锁:读操作时无需加锁,仅通过 "版本戳" 判断期间是否有写操作修改数据,若未修改则直接使用,若已修改则升级为悲观读锁(避免数据不一致)。

2. 示例代码(写操作原子性保证)

java

运行

csharp 复制代码
import java.util.concurrent.locks.StampedLock;

public class StampedLockAtomicExample {
    private final StampedLock lock = new StampedLock();
    private int value = 0;

    // 写操作:原子修改 value(获取写锁,互斥执行)
    public void setValue(int newValue) {
        long stamp = lock.writeLock(); // 获取写锁,返回版本戳
        try {
            value = newValue; // 原子操作(无其他线程干扰)
        } finally {
            lock.unlockWrite(stamp); // 释放写锁,需传入版本戳
        }
    }

    // 读操作:乐观读(无锁),若被修改则升级为悲观读
    public int getValue() {
        long stamp = lock.tryOptimisticRead(); // 乐观读,无锁
        int currentValue = value;
        // 验证期间是否有写操作修改数据
        if (!lock.validate(stamp)) {
            stamp = lock.readLock(); // 升级为悲观读锁
            try {
                currentValue = value;
            } finally {
                lock.unlockRead(stamp);
            }
        }
        return currentValue;
    }
}

3. 适用场景

  • 读操作远多于写操作(如缓存查询、数据统计),需保证写操作的原子性,同时提升读性能。

4. 注意事项

  • 不支持可重入(同一线程多次获取写锁会死锁),需手动避免。
  • 乐观读升级为悲观读时需注意性能开销,避免频繁升级。

四、ThreadLocal(线程隔离,避免共享变量竞争)

ThreadLocal 并非直接保证 "共享变量的原子性",而是通过 线程隔离 彻底避免共享 ------ 每个线程持有变量的独立副本,线程间互不干扰,自然无需考虑原子性问题。

1. 核心原理

  • ThreadLocal 内部维护一个 ThreadLocalMap(线程私有),每个线程通过 ThreadLocal 访问自己的副本变量,修改操作仅影响当前线程的副本,与其他线程无关。
  • 本质:"不共享即无竞争",间接实现 "线程内操作的原子性"(无需同步)。

2. 示例代码(线程隔离避免原子性问题)

java

运行

csharp 复制代码
public class ThreadLocalExample {
    // 每个线程持有独立的计数器副本
    private final ThreadLocal<Integer> threadLocalCount = ThreadLocal.withInitial(() -> 0);

    // 线程内自增:仅修改当前线程的副本,无并发竞争
    public void increment() {
        threadLocalCount.set(threadLocalCount.get() + 1);
    }

    public int getCount() {
        return threadLocalCount.get();
    }

    public static void main(String[] args) {
        ThreadLocalExample example = new ThreadLocalExample();

        // 线程1:自增后输出 1
        new Thread(() -> {
            example.increment();
            System.out.println(example.getCount()); // 1
        }).start();

        // 线程2:自增后输出 1(与线程1的副本独立)
        new Thread(() -> {
            example.increment();
            System.out.println(example.getCount()); // 1
        }).start();
    }
}

3. 适用场景

  • 变量无需线程间共享(如每个线程的上下文信息:用户 ID、请求 ID)。
  • 避免频繁创建临时变量(如工具类中的线程私有缓存)。

4. 注意事项

  • 内存泄漏风险 :若线程池复用线程(如 Tomcat 线程池),ThreadLocal 副本未手动移除(remove()),会导致副本变量长期占用内存。解决方案:在使用完后调用 threadLocal.remove() 清理。
  • 不适合线程间数据共享:若需线程间传递数据,ThreadLocal 无效,需改用原子类或锁。

五、java.util.concurrent.Semaphore(信号量,控制并发访问数)

Semaphore 是 "并发访问控制器",通过控制 允许同时访问的线程数 间接保证原子性 ------ 当信号量为 1 时,等价于互斥锁(同一时间仅一个线程执行临界区操作),从而保证原子性。

1. 核心原理

  • 信号量维护一个 "许可数",线程获取许可(acquire())后才能执行操作,执行完释放许可(release())。
  • 当许可数 = 1 时,成为 "互斥信号量",实现与 synchronized 类似的互斥效果,保证临界区操作原子性。

2. 示例代码(信号量保证原子性)

java

运行

java 复制代码
import java.util.concurrent.Semaphore;

public class SemaphoreAtomicExample {
    // 信号量许可数 = 1,等价于互斥锁
    private final Semaphore semaphore = new Semaphore(1);
    private int count = 0;

    // 原子自增:通过信号量控制仅一个线程执行
    public void increment() throws InterruptedException {
        semaphore.acquire(); // 获取许可(无许可则阻塞)
        try {
            count++; // 原子操作(互斥执行)
        } finally {
            semaphore.release(); // 释放许可(必须在 finally 中)
        }
    }

    public int getCount() {
        return count;
    }
}

3. 适用场景

  • 需限制并发访问数(如 "最多 3 个线程同时操作数据库"),同时保证单个线程操作的原子性。
  • synchronized 灵活:支持动态调整许可数(如根据系统负载修改并发数)。

4. 注意事项

  • 必须在 finally 中释放许可,否则会导致许可泄露(其他线程永远无法获取许可)。
  • 支持中断(acquireInterruptibly())和超时等待(tryAcquire(timeout)),可避免死锁。

六、java.util.concurrent.locks.ReadWriteLock(读写锁,读多写少优化)

ReadWriteLock 是 "读写分离锁",核心是 "读锁共享,写锁互斥"------ 多个线程可同时获取读锁(无竞争),但写锁与读锁、写锁与写锁互斥,从而保证写操作的原子性,读操作的一致性。

1. 核心原理

  • 写锁(WriteLock)互斥:获取写锁后,其他线程无法获取读锁或写锁,保证 "修改操作" 的原子性(如 "读取 - 修改 - 写入" 不可分割)。
  • 读锁(ReadLock)共享:多个线程可同时获取读锁,提升读操作性能(适合读多写少场景)。

2. 示例代码(写操作原子性保证)

java

运行

java 复制代码
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
    private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
    private int data = 0;

    // 写操作:原子修改数据(互斥执行)
    public void updateData(int newValue) {
        writeLock.lock(); // 获取写锁,互斥
        try {
            data = newValue; // 原子操作(无其他线程干扰)
        } finally {
            writeLock.unlock();
        }
    }

    // 读操作:共享获取数据(无竞争)
    public int getData() {
        readLock.lock();
        try {
            return data;
        } finally {
            readLock.unlock();
        }
    }
}

3. 适用场景

  • 读操作远多于写操作(如缓存、配置中心),需保证写操作的原子性,同时提升读性能。

4. 注意事项

  • 避免 "写锁饥饿":若读锁长期被占用,写锁可能一直无法获取(ReentrantReadWriteLock 默认非公平锁,可通过构造函数指定公平锁缓解)。
  • 读锁不能升级为写锁:若线程已获取读锁,再尝试获取写锁会导致死锁(需先释放读锁)。

七、总结:不同原子性方案的选择建议

方案 核心优势 适用场景 性能对比(高并发)
Atomic 原子类 无锁、高性能、API 简洁 单变量原子操作(计数器、状态标记) 最优(无锁竞争)
ReentrantLock 灵活(超时、中断、公平锁)、支持复合操作 多变量复合原子操作、高冲突场景 中(锁竞争开销)
StampedLock 读多写少场景性能最优(乐观读无锁) 读远多于写、需保证写操作原子性 读操作最优,写操作与 ReentrantLock 相当
ThreadLocal 无并发竞争、线程隔离 变量无需共享(上下文、临时变量) 无竞争,性能极高(但不共享)
Semaphore(许可 = 1) 支持动态调整并发数、灵活控制访问权限 需限制并发数的原子操作 中(信号量调度开销)
ReadWriteLock 读锁共享、写锁互斥,读多写少场景优化 读多写少、需保证写操作原子性 读操作最优,写操作互斥开销

核心选择原则

  1. 单变量原子操作(如计数器):优先用 Atomic 原子类(无锁,性能最好)。
  2. 多变量复合操作(如 "先修改再判断"):用 ReentrantLocksynchronized(互斥保证原子性)。
  3. 读多写少场景:用 StampedLockReadWriteLock(平衡读性能和写原子性)。
  4. 变量无需共享:用 ThreadLocal(线程隔离,无竞争)。
  5. 需限制并发数:用 Semaphore(许可 = 1 等价互斥锁,支持动态调整)。
相关推荐
IT_陈寒1 小时前
JavaScript开发者必知的7个ES2023新特性,让你的代码效率提升50%
前端·人工智能·后端
j***63081 小时前
SpringbootActuator未授权访问漏洞
android·前端·后端
用户377833043491 小时前
构建Agnet(2) 提示词模板使用
后端
Cache技术分享1 小时前
254. Java 集合 - 使用 Lambda 表达式操作 Map 的值
前端·后端
踏浪无痕1 小时前
手写一个Nacos配置中心:搞懂长轮询推送机制(附完整源码)
后端·面试·架构
用户345848285051 小时前
java除了`synchronized`关键字,还有哪些方式可以保证Java中的有序性?
后端
y***13642 小时前
【wiki知识库】07.用户管理后端SpringBoot部分
spring boot·后端·状态模式