CodeGym /课程 /JAVA 25 SELF /AtomicInteger、AtomicReference:原子操作

AtomicInteger、AtomicReference:原子操作

JAVA 25 SELF
第 53 级 , 课程 3
可用

1. 为什么 i++ 在多线程中不起作用?

从一个经典问题开始:我们有一个计数器变量,例如已处理的请求数量或下载的文件数量。我们希望多个线程同时增加这个计数器。如果只是写上 i++,会出什么问题?

示例:自增中的数据竞争

public class Counter {
    public int count = 0;

    public void increment() {
        count++; // 不是原子操作!
    }
}

设想两个线程同时调用 increment()。两个线程都读取旧值,都把它加 1,然后都写回……相同的新值!结果就是有一次自增被“丢失”。如果这种情况发生很多次,最终结果会比期望值更小。

为什么会这样?
操作 i++ 实际由三个步骤组成:

  1. 读取变量的值(例如 5)。
  2. 将该值加上 1
  3. 把新值写回内存。

在多线程环境中,其他线程可能会在这些步骤之间修改变量。结果就是出现“数据竞争”(race condition)。

什么是原子操作?

原子操作 是要么全部执行、要么完全不执行的动作,其他任何线程都无法在该操作的中间“插入”。

Java 提供了一组用于基本类型和引用的此类操作的类,位于包 java.util.concurrent.atomic。最常用的是:

  • AtomicInteger —— 原子的整数类型。
  • AtomicLong —— 原子的 long
  • AtomicBoolean —— 原子的 boolean
  • AtomicReference<T> —— 指向任意类型对象的原子引用。

2. AtomicInteger:线程安全的计数器

声明与基础用法

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // 原子递增
    }

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

这里的 incrementAndGet() 会把“递增并返回新值”作为一个不可分割的操作来执行。即使有 100 个线程同时调用该方法,也不会有一次自增被丢失。

常用方法:

方法 说明
get()
获取当前值
set(int value)
设置值
incrementAndGet()
加 1 并返回新值
getAndIncrement()
返回当前值并加 1
addAndGet(int delta)
加上 delta 并返回新值
compareAndSet(expect, update)
如果当前值等于 expect,则设置为 update(CAS)

示例:多线程计数器

假设我们有一个类,用于统计聊天中已处理的消息数量。

public class MessageStatistics {
    private final AtomicInteger messageCount = new AtomicInteger(0);

    public void onMessageReceived() {
        int newCount = messageCount.incrementAndGet();
        System.out.println("总消息数:" + newCount);
    }

    public int getMessageCount() {
        return messageCount.get();
    }
}

内部原理:AtomicInteger 如何工作?

在内部,AtomicInteger 使用了处理器的特殊指令——CAS(Compare-And-Swap,“比较并交换”)。这是一种原子操作:比较变量的当前值与期望值是否相等,若相等则写入新值;如果在此期间有其他线程修改了变量——操作不会执行,并会重试。

工作流程:

1. 读取当前值(例如 5)
2. 与期望值比较(5)
3. 若相等——写入新值(6)
4. 若不相等——重试

这一切都在没有加锁(lock‑free)的情况下非常快速地完成。因此,原子类通常比 synchronized 更快,尤其是在线程数量很多的情况下。

3. AtomicReference:对象的原子引用

AtomicReference<T> 是一个通用的原子容器,适用于任何对象。它允许在多个线程中安全地更新对象引用。

示例:线程安全地更新引用

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceExample {
    private final AtomicReference<String> latestMessage = new AtomicReference<>("");

    public void updateMessage(String message) {
        latestMessage.set(message);
    }

    public String getLatestMessage() {
        return latestMessage.get();
    }
}

compareAndSet 的应用

最有趣的操作是 compareAndSet(expected, newValue)。它允许仅在自上次读取后值未发生变化时才更新。

public void safeUpdate(String oldValue, String newValue) {
    boolean success = latestMessage.compareAndSet(oldValue, newValue);
    if (success) {
        System.out.println("更新成功!");
    } else {
        System.out.println("值已被他人修改,请重试。");
    }
}

这是无锁算法的基础:从队列、栈到缓存,只要需要避免不必要的加锁,就会用到它。

4. 在应用中的使用示例

示例 1:多线程消息计数器

public class ChatRoom {
    private final AtomicInteger messageCount = new AtomicInteger(0);

    public void receiveMessage(String message) {
        // ... 处理消息 ...
        int count = messageCount.incrementAndGet();
        System.out.println("新消息:" + message + "。总消息数:" + count);
    }
}

示例 2:安全地更新对最后一条消息的引用

public class ChatRoom {
    private final AtomicReference<String> lastMessage = new AtomicReference<>("");

    public void receiveMessage(String message) {
        lastMessage.set(message);
        // ... 处理 ...
    }

    public String getLastMessage() {
        return lastMessage.get();
    }
}

如果需要仅在最后一条消息未改变时才更新引用(以避免并发更新时“丢失”),请使用 compareAndSet

5. 限制与陷阱

原子类何时不是万能药?

原子变量非常适合简单操作:自增、设值、检查并替换。但如果需要同时更新多个变量,原子性就无法保证了。举例来说,如果你有两个计数器,并希望把它们作为一个操作同时加 1——这就需要 synchronized 或其他同步机制。

错误用法示例

// 非原子!
if (ref.get() == null) {
    ref.set("Hello");
}

get()set(...) 之间,其他线程可能会修改该值,从而使条件不再成立。此类场景请使用 compareAndSet

原子类 ≠ 线程安全对象

如果 AtomicReference 指向的对象本身不是线程安全的,那么替换引用是原子的,但修改对象的字段并不是。例如,如果你在 AtomicReference<List<String>> 中存放的是普通的 ArrayList,那么列表本身并不会变成线程安全(thread‑safe)。

6. 进阶原子类

在包 java.util.concurrent.atomic 中还有其他有用的类:

  • AtomicLongAtomicBoolean —— 用于 longboolean
  • AtomicIntegerArrayAtomicReferenceArray —— 针对数组的原子操作。
  • LongAdderLongAccumulator —— 面向高并发计数场景。

LongAdder 与 LongAccumulator

如果线程非常多,普通的 AtomicInteger 成为“瓶颈”(所有线程竞争同一个变量),请使用 LongAdder。它将计数器拆分为多个内部单元,并在读取时求和,从而在高竞争场景下获得更好的性能。

import java.util.concurrent.atomic.LongAdder;

public class FastCounter {
    private final LongAdder adder = new LongAdder();

    public void increment() {
        adder.increment();
    }

    public long getCount() {
        return adder.sum();
    }
}

7. 使用原子变量的常见错误

错误 1:指望复杂操作具备原子性。
如果需要对值执行多个步骤,原子类并不能保证——在各步骤之间其他线程可能会修改数据。对于复合操作,请使用 compareAndSet 或同步手段。

错误 2:忽视嵌套对象的线程安全。
如果 AtomicReference 中放的是普通对象,其方法和字段并不会自动变成线程安全。原子性的仅是引用的替换。

错误 3:不必要地使用原子类。
在单线程代码中,原子类型是多余的,并且由于额外的校验会比普通变量稍慢。

错误 4:过早优化。
有时使用 synchronized 更简单、更可靠,尤其是当逻辑复杂且涉及多个变量时。并不总是值得构建无锁(lock‑free)方案。

错误 5:忘记 ABA 问题。
较少见但很重要的情况:值从 A 变为 B 又变回 A——compareAndSet 会“以为”什么都没变。此类场景请使用专用类,例如 AtomicStampedReference(或 AtomicMarkableReference)。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION