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

多线程访问的简易定时过期缓存技术问题咨询

线程安全过期缓存的疑问与修复

背景代码

public class ExpirationCache<T> {
    private final Supplier<T> computable;
    private final long validTime;
    private final Lock lock = new ReentrantLock();
    private LocalDate lastAccess;
    private Future<T> data;

    public ExpirationCache(Supplier<T> computable, long validTime) {
        this.computable = computable;
        this.validTime = validTime;
    }

    public boolean hasExpired() {
        if( lastAccess == null ){
            return true;
        }
        return LocalDateTime.now().isAfter(lastAccess.plus(validTime, ChronoUnit.MILLIS));
    }

    public T getData() throws InterruptedException {
        while (true) {
            if (hasExpired()) {
                FutureTask<T> ft = new FutureTask<T>(computable::get);
                try {
                    lock.lock();
                    if (hasExpired()) {
                        data.cancel(true);
                        data = ft;
                    }
                } finally {
                    lastAccess = LocalDate.now();
                    lock.unlock();
                }
            }
            try {
                return data.get();
            } catch (CancellationException e) {
                throw new RuntimeException(e);
            } catch (ExecutionException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

核心疑问

围绕data和lastAccess变量的线程安全问题:

  1. 是否存在场景:线程A调用hasExpired()返回false,但随后线程B调用getData()时hasExpired()返回true,进入同步块修改data引用,导致线程A出现未定义行为?
  2. 加锁能否防止指令重排?会不会出现线程设置了data和lastAccess的新值,但其他线程只看到lastAccess新值、data旧值的情况?

如果上述场景存在,正确的修复方式是什么?是否需要将lastAccess或data改为AtomicReference?对hasExpired()加锁会让整个类变成同步实现,有没有更优方案?

另外,当数据计算耗时超过validTime时,代码会在设置新的Future引用前取消原有操作,这是否意味着computable需要监听中断标志以正确退出计算?


问题解答

1. 第一个场景确实存在风险

线程A调用hasExpired()返回false后,线程B刚好触发缓存过期并进入同步块修改data引用。线程A后续执行data.get()时,可能拿到刚被取消的Future,或是新的Future,但核心问题是:线程A读取data时无锁保护,无法保证看到最新值,甚至可能因缓存一致性或指令重排问题,读取到过期的data引用,导致get()调用出现预期外异常。

2. 指令重排与可见性问题

  • 加锁(ReentrantLock)本身能防止指令重排:lock()和unlock()会建立内存屏障,保证锁内操作不会被重排到锁外,且解锁后变量修改对后续加锁线程可见。
  • 但当前代码存在逻辑漏洞:lastAccess在finally块中无条件更新,即使同步块内判断缓存未过期,也会刷新有效期,不符合预期。同时,锁外读取lastAccess和data时无同步机制,可能出现“看到lastAccess新值、但data还是旧引用”的情况——因为没有内存屏障保证两个变量修改的原子性可见性。

修复方案

方案一:保证可见性与逻辑正确性

无需使用AtomicReference,通过volatile和逻辑调整即可解决:

private volatile LocalDateTime lastAccess; // 替换LocalDate:LocalDate无时分秒,过期判断不准确
private volatile Future<T> data;

public boolean hasExpired() {
    if (lastAccess == null) {
        return true;
    }
    return LocalDateTime.now().isAfter(lastAccess.plus(validTime, ChronoUnit.MILLIS));
}

public T getData() throws InterruptedException {
    while (true) {
        lock.lock();
        try {
            if (hasExpired()) {
                if (data != null) {
                    data.cancel(true);
                }
                FutureTask<T> ft = new FutureTask<>(computable::get);
                data = ft;
                lastAccess = LocalDateTime.now();
                ft.run(); // 锁内启动任务,避免后续get()阻塞
            }
        } finally {
            lock.unlock();
        }
        
        try {
            return data.get();
        } catch (CancellationException e) {
            // 任务被取消,循环重试获取新数据
            continue;
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }
}

关键调整点:

  • 用LocalDateTime替换LocalDate,保证过期时间计算精确到毫秒。
  • 给lastAccess和data加volatile修饰,确保锁外读取能获取最新值。
  • 将lastAccess更新移到锁内,仅在缓存过期时刷新,修正原逻辑错误。
  • 锁内启动Future任务,避免多线程等待同一任务执行。

方案二:全锁同步(简单直接)

若不介意性能损耗,可将所有对data和lastAccess的访问(包括hasExpired()和data.get())都放入锁内。这种方式完全避免可见性问题,但data.get()的阻塞会占用锁,导致其他线程无法获取缓存,性能不如方案一。

关于computable的中断处理

是的,调用data.cancel(true)后,computable的get()方法必须监听中断标志才能正确退出。如果computable不响应中断,cancel(true)仅会标记Future为取消状态,无法真正停止后台计算,会造成资源浪费。

示例响应中断的computable实现:

Supplier<BigData> computable = () -> {
    while (!Thread.currentThread().isInterrupted()) {
        // 执行分段计算逻辑
        if (计算完成) {
            return result;
        }
    }
    throw new InterruptedException("任务被中断");
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 21:09:52