1

我认为我对 java 中的关键字有一个很好的想法volatile,但我正在考虑重构一些代码,我认为使用它是一个好主意。

我有一个基本上用作数据库缓存的类。它拥有一堆从数据库中读取的对象,为这些对象提供请求,然后偶尔刷新数据库(基于超时)。这是骨架

public class Cache
{
    private HashMap mappings =....;
    private long last_update_time;
    private void loadMappingsFromDB()
    {
        //....
    }
    private void checkLoad()
    {
        if(System.currentTimeMillis() - last_update_time > TIMEOUT)
            loadMappingsFromDB();
    }
    public Data get(ID id)
    {
        checkLoad();
        //.. look it up
    }
}

所以担心 loadMappingsFromDB 可能是一个高延迟操作,这是不可接受的,所以最初我认为我可以在缓存启动时启动一个线程,然后让它休眠,然后在后台更新缓存。但后来我需要同步我的班级(或地图)。然后我只是为了让每个缓存访问变慢而偶尔暂停一下。

然后我想为什么不使用volatile

我可以将地图参考定义为 volatile

private volatile HashMap mappings =....;

然后在get(或使用映射变量的任何其他地方)我将只制作引用的本地副本:

public Data get(ID id)
{
    HashMap local = mappings;
    //.. look it up using local
}

然后后台线程将加载到临时表中,然后交换类中的引用

HashMap tmp;
//load tmp from DB
mappings = tmp;//swap variables forcing write barrier

这种方法有意义吗?它实际上是线程安全的吗?

4

4 回答 4

2

这个问题的现有答案中有一些错误信息。使用volatile实际上是确保线程安全的一个很好的步骤。请参阅IBM 的 Peter HaggarDispelling Java 编程语言神话中的第 3 项。Haggar 提供了一些背景知识和示例,但重点是:

那么,原子操作怎么可能不是线程安全的呢?要点是它们可能确实是线程安全的,但不能保证它们是线程安全的。Java 线程允许将变量的私有副本与主内存分开。

通过使用volatile,您将保证线程正在引用主内存,而不是使用您不知道或不期望的变量的私有副本。

那么回答你的问题:是的,你的策略是安全的。

编辑:
作为对另一篇文章的回应,这里是关于volatilefields的 JLS 部分。

于 2010-06-10T17:06:16.463 回答
1

这种方法有意义吗?它实际上是线程安全的吗?

它确实有意义,而且它是线程安全的。在某种程度上,无论如何。需要考虑的一些事情:

  • 更新时,您让应用程序读取旧的、过时的值。这是你想要的吗?对于某些应用程序来说这很好,在其他情况下,您可能希望在缓存更新之前阻塞(FutureTask使这种行为相当容易)。
  • 启动时loadMappingsFromDB(),最初调用的线程get(ID)将阻塞,直到更新完成。
  • 多个线程可能checkLoad()同时调用,这意味着如果重新加载很慢并且您有多个线程调用get(ID),您最终可能会遇到大量并发更新。虽然结果是一样的,但这会浪费系统资源。在当前代码中修复它的一种简单方法是AtomicBoolean在更新之前检查它:

    private final AtomicBoolean isUpdating = new AtomicBoolean(false);
    private void checkLoad()
    {
        if (System.currentTimeMillis() - last_update_time <= TIMEOUT) return;
        if (!isUpdating.compareAndSet(false, true)) return; // already updating
        try {
            loadMappingsFromDB();
        } finally {
            isUpdating.set(false);
        }
    }
    
于 2010-06-10T16:58:39.693 回答
0

我认为这种通用方法是有效的(在后台线程上重新加载缓存,而不是通过将数据加载到单独的实例来阻止在加载过程中访问缓存),但我不确定将引用声明为volatile真的给你。

您可以轻松地让get(id)方法及其loadMappingsFromDB()覆盖引用的部分(不是来自 DB 的整个负载,而只是重新分配mappings)在同一个锁上同步。

老实说,虽然我会考虑重新使用已建立的缓存库,如 EhCache,它具有后台加载或启动时加载的功能,因为这些库很可能很久以前就解决了所有同步问题,你可以回到担心应用程序的逻辑而不是自制缓存的低级安全性。

于 2010-06-10T16:29:00.700 回答
-1

实际上,即使不使用 'volatile' 关键字,您提出的方法也是有意义的。因为引用分配 ( mappings = tmp;) 是原子操作(转换为一个机器命令),所以不会出现内存不一致:http:
//java.sun.com/docs/books/tutorial/essential/concurrency/atomic.html
读写是atomic 用于参考变量和大多数原始变量(除了 long 和 double 之外的所有类型)。

'volatile' 关键字背后的想法是,如果您在时间 T 更改此类变量,则在时间 T + x (x > 0) 访问它的任何其他线程都将看到新值。否则,即使更改了值,仍然有一段时间其他线程可能会看到旧值。但这对你来说似乎不是问题。
编辑
上面的链接中描述了相同的想法。

于 2010-06-10T16:53:46.070 回答