You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java中如何保证LocalCache组合数据读取的原子性?

解决LocalCache多线程下状态一致性问题

当前代码的核心问题是:add方法虽加了同步,但读取size、keys、data的操作是分开执行的,没有原子性保障。比如foo()里先拿size,之后其他线程执行add修改了size、keys、data,再拿keys和data时,三者的状态就对应不上了。

下面给出几种实用的解决思路:

1. 统一同步所有读写操作

给getData()、getKeys()、getSize()都加上synchronized修饰,这样任何线程读取这三个字段时,都会持有LocalCache对象的锁,确保读取期间没有add操作执行,自然能保证三者状态一致。

修改后的示例:

public class LocalCache {
    int size;
    List<String> keys;
    float[] data;

    public LocalCache(int initialCapacity) {
        // 初始化data数组,避免扩容前数组越界
        this.data = new float[initialCapacity * 256];
        this.keys = new ArrayList<>(initialCapacity);
        this.size = 0;
    }

    public synchronized void add(String oneKey, float[] oneData) {
        // 校验数据长度,符合设计预期
        if (oneData.length != 256) {
            throw new IllegalArgumentException("输入数据长度必须为256");
        }
        // 动态扩容data数组
        if (size * 256 + 256 > data.length) {
            this.data = Arrays.copyOf(data, data.length * 2);
        }
        System.arraycopy(oneData, 0, this.data, size * 256, 256);
        keys.add(oneKey);
        size++;
    }

    public synchronized float[] getData() {
        // 返回有效数据的拷贝,避免外部修改内部数组
        return Arrays.copyOf(data, size * 256);
    }

    public synchronized List<String> getKeys() {
        // 返回拷贝,避免外部修改内部列表
        return new ArrayList<>(keys);
    }

    public synchronized int getSize() {
        return size;
    }
}

这种方式简单直接,但缺点是如果读取操作很频繁,会导致add操作被频繁阻塞,影响写入性能。

2. 提供原子性的快照获取方法

定义一个封装了size、keys、data的快照类,然后提供一个同步的方法一次性返回完整快照。这样foo()只需要调用一次方法就能拿到一致的状态,同时减少锁的持有时间(只在生成快照时持有锁)。

示例代码:

public class LocalCache {
    int size;
    List<String> keys;
    float[] data;

    public LocalCache(int initialCapacity) {
        this.data = new float[initialCapacity * 256];
        this.keys = new ArrayList<>(initialCapacity);
        this.size = 0;
    }

    public synchronized void add(String oneKey, float[] oneData) {
        if (oneData.length != 256) {
            throw new IllegalArgumentException("输入数据长度必须为256");
        }
        if (size * 256 + 256 > data.length) {
            this.data = Arrays.copyOf(data, data.length * 2);
        }
        System.arraycopy(oneData, 0, this.data, size * 256, 256);
        keys.add(oneKey);
        size++;
    }

    // 一次性获取一致的快照
    public synchronized CacheSnapshot getSnapshot() {
        return new CacheSnapshot(
            size,
            new ArrayList<>(keys),
            Arrays.copyOf(data, size * 256)
        );
    }

    // 内部快照类,封装一致的状态
    public static class CacheSnapshot {
        public final int size;
        public final List<String> keys;
        public final float[] data;

        private CacheSnapshot(int size, List<String> keys, float[] data) {
            this.size = size;
            this.keys = keys;
            this.data = data;
        }
    }
}

使用时foo()改成:

void foo() {
    LocalCache.CacheSnapshot snapshot = localCache.getSnapshot();
    // 使用snapshot.size、snapshot.keys、snapshot.data,三者状态完全一致
}

这种方式更高效,因为生成快照后外部可以随意使用,不需要一直持有锁,同时返回的是拷贝,避免外部修改缓存内部状态。

额外注意点

  • 原代码中data数组没有初始化,add时会直接抛出数组越界异常,必须在构造函数中初始化,或者在add方法中做动态扩容。
  • 原add方法用for循环复制数据,效率不如System.arraycopy,建议替换以提升性能。
  • 返回数据时尽量返回拷贝,避免外部代码修改缓存内部的keys或data数组,导致状态混乱。

内容的提问来源于stack exchange,提问作者Tiina

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 05:36:05