2010-06-10 72 views
1

我觉得我有在Java中volatile关键字一个不错的主意,但我想重新分解了一些代码,我认为这将是使用它是一个好主意。正确使用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

我可以定义绘图参照挥发性

private volatile HashMap mappings =....; 

,然后在get(或其他地方使用该映射变量)我只想让本地参考副本:

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

然后后台线程会加载到临时表中,然后交换r类别中的推理

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

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

回答

2

有在现有的回答这个问题的一些误传。使用volatile实际上是确保线程安全性的一个很好的步骤。参见IBM的Peter Haggar的item 3 in Dispelling Java programming language myths。 Haggar给出了一些背景和示例,但是坚果是这样的:

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

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

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

编辑:
为了应对另一篇文章,这里是the JLS section about volatile fields

+0

多数民众赞成在一篇不错的文章,谢谢 – luke 2010-06-10 17:11:18

0

我认为这种一般的方法是有效的(重新加载缓存在后台线程,而不是阻止访问缓存,而负载是通过将数据装入进步分离实例),但我不知道什么声明参考为volatile真的给你。

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

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

+0

我不想使用同步块,因为它们会导致性能开销。 – luke 2010-06-10 16:47:41

+0

块的开销像'synchronized(lockObject){mappings = temp; }“远远低于你的想象。并且使用volatile还具有开销。 – 2010-06-10 17:31:54

+0

易失性仅在写入后立即产生开销,因为缓存已失效(这实际上取决于底层架构,但这通常是屏障的工作原理)。虽然同步,施加相同的惩罚加上一些方法调用和其他cheks以及潜在的阻塞,而挥发性总是会立即读取值。 – luke 2010-06-10 19:39:20

-1

实际上,你提出的做法是有道理的,即使没有使用“挥发性”关键字。由于参考分配(mappings = tmp;)是一个原子操作(转换为一个机器指令)没有记忆不一致的机会:
http://java.sun.com/docs/books/tutorial/essential/concurrency/atomic.html
读取和写入原子参考变量和最原始的变量(所有类型除了长和双)。

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

+0

如果映射不是“易失性”,它将不会是“安全发布”。引用不会立即更新,并且值可能会过时。 – gustafc 2010-06-10 16:57:07

+0

原子操作不能保证是线程安全的:http://www.ibm.com/developerworks/java/library/it/it-1001art24/index。html#3 – Pops 2010-06-10 16:58:30

+0

@gustafc如果我正确理解海报,几秒钟的延迟(这不太可能)不会成为一个问题。这并不像价值会在一年内“过时”。但我同意在许多情况下这是不可取的。我刚才描述了这个行为,这是作者决定是否使用它。 – 2010-06-10 17:00:02

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); 
        } 
    } 
    
+0

@gustfc这些都是一般的优点,但它们对我的情况并不重要。只有一个线程会调用get,因此只有该线程才会一次调用checkUpdate,问题更多的是修改此共享变量的安全性。此外,它也可以,如果有'陈旧'的读取,因为我们只是在轮询更改,所以数据可能已经过时了一段时间,然后我们尝试了更新(对于我的用例来说,这没问题)。但感谢代码片段,我没有使用AtomicBoolean之前,所以+1 :) – luke 2010-06-10 19:36:35