正确使用volatile关键字

时间:2010-06-10 16:23:48

标签: java thread-safety volatile

我想我对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 个答案:

答案 0 :(得分:2)

在这个问题的现有答案中存在一些错误信息。使用volatile实际上是确保线程安全的一个很好的步骤。请参阅IBM的Peter Haggar的item 3 in Dispelling Java programming language myths。 Haggar给出了一些背景和一个例子,但坚果是这样的:

  

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

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

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

编辑:
在回复另一篇文章时,这里是the JLS section about volatile fields

答案 1 :(得分: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);
        }
    }
    

答案 2 :(得分:0)

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

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

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

答案 3 :(得分:-1)

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

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