【问题标题】:Storing object reference into a volatile field将对象引用存储到 volatile 字段中
【发布时间】:2016-04-28 21:00:37
【问题描述】:

我正在使用以下字段:

private DateDao dateDao;

private volatile Map<String, Date> dates;

public Map<String, Date> getDates() {
    return Collections.unmodifiableMap(dates);
}

public retrieveDates() {
     dates = dateDao.retrieveDates();
}

在哪里

public interface DateDao {
    //Currently returns HashMap instance
    public Map<String, Date> retrieveDates();
}

以这种方式发布日期地图是否安全?我的意思是,易失性字段意味着对字段的引用不会被缓存在 CPU 寄存器中,并且不会在任何时候被访问时从内存中读取。

因此,我们不妨读取state of the map 的陈旧值,因为HashMap 不进行任何同步。

这样做安全吗?

UPD:例如假设DAo方法以如下方式实现:

public Map<String, Date> retrieveDates() {
    Map<String, Date> retVal = new HashMap<>();
    retVal.put("SomeString", new Date());
    //ad so forth...
    return retVal;
}

可以看出,Dao 方法不做任何同步,HashMapDate 都是可变的,不是线程安全的。现在,我们已经创建并发布了它们,如上所示。是否保证从另一个线程对dates 的任何后续读取不仅会观察到对Map 对象的正确引用,还会观察到它的“新鲜”状态。

我不确定线程​​是否不能观察到一些陈旧的值(例如 dates.get("SomeString") 返回 null

【问题讨论】:

  • “安全”在什么意义上?对getDates 的调用总是会看到来自retrieveDates 的最新分配的地图?还是……?
  • @T.J.Crowder 调用 getDates 总是会看到最近分配的地图你为什么这么认为?没有同步JIT 允许执行任何与内存模型兼容的优化。在 Dao 中创建和填充 Map 不涉及任何同步,而且 HashMap 不是线程安全的。我有点担心。
  • @T.J.Crowder 我的问题实际上是。仅存储对不会被修改为volatile 字段的对象的引用以安全地发布它真的就足够了吗?即使在发布之前对对象进行创建和其他一些操作也不需要同步。
  • 我在问你在问什么,而不是在断言什么。要澄清您的问题,请使用问题上的“编辑”链接,而不是 cmets。
  • 只是一种英语 仅供参考:“你能看一下吗?”是要求某人看某物的请求。 “不能你看看吗?”是他们没有抱怨。 :-)(疯狂的语言,嗯?)我会看一下,但我可能没有足够的知识来回答(只是为了确定需要更多细节)。

标签: java multithreading volatile


【解决方案1】:

我想你在问两个问题:

  1. 鉴于该 DAO 代码,您的代码是否可以使用它在此处获得的对象引用:

     dates = dateDao.retrieveDates();
    

之前引用的dateDao.retrieveDates 方法已添加到该对象。例如,内存模型的语句重新排序语义是否允许retrieveDates 方法在最后一个put(等)完成之前返回引用?

  1. 一旦您的代码具有dates 引用,您的代码中是否存在对dates 的非同步访问以及您从getDates 返回的只读视图的问题。

您的领域是否为volatile 与这些问题中的任何一个都无关。使您的字段volatile 所做的唯一事情是防止调用getDates 的线程获取您的dates 字段的过时值。那就是:

线程 A 线程 B ---------- -------- 1. 从 dateDao.retrieveDates 更新 `dates` 2. 再次从“”更新`dates` 3. getDates 返回只读 #1 中的“日期”视图

没有volatile,上述情况是可能的(但无害)。 volatile 不是这样,线程 B 会从 #2 而非 #1 中看到 dates 的值。

但这与我认为您要问的任何一个问题都无关。

问题 1

不,在dateDao.retrieveDates 完成填充该映射之前,您在retrieveDates 中的代码无法看到dateDao.retrieveDates 返回的对象引用。 memory model 允许重新排序语句,但是:

...允许编译器重新排序任一线程中的指令,如果这不会影响单独执行该线程

(我的重点。)dateDao.retrieveDates 之前返回对您代码的引用显然会影响线程的孤立执行。

问题 2

您显示的 DAO 代码永远不会修改它返回给您的映射,因为它不保留它的副本,所以我们不需要担心 DAO。

您的代码中,您没有显示任何修改dates 内容的内容。如果您的代码没有修改dates 的内容,那么就不需要同步,因为地图是不变的。您可能希望通过在获取而不是在返回它时将dates 包装在只读视图中来保证这一点:

dates = Collection.unmodifiableMap(dateDao.retrieveDates());

如果您的代码确实修改了dates 某个您没有显示的地方,那么是的,这可能会出现问题,因为Collections.unmodifiableMap 对同步地图操作没有任何作用。它只是创建一个只读视图。

如果您想确保同步,您需要将dates 包装在Collections.synchronizedMap 实例中:

dates = Collections.synchronizedMap(dateDao.retrieveDates());

然后在你的代码中对它的所有访问都会被同步,所有通过你返回的只读视图对它的访问也会被同步,因为它们都经过同步的映射。

【讨论】:

  • 遵守线程内语义的概念相当模糊...... 在dateDao.retrieveDates之前返回对你代码的引用显然会影响线程的孤立执行。 是不是因为 volatile 引用?
  • @Alupkers 这真的取决于。例如,如果 JIT 想要在 retireveDates 之外重新排序 dates = dateDao.retrieveDates(); 的分配,那么 JIT 违反了线程内语义,因为在初始 retireveDates 调用之前的 getDates 调用应该会失败,但是在那种情况下不会。
  • @Alupkers: “是因为 volatile 引用吗?” 不,这是因为如果我们只看那个线程,而忽略其他线程,它显然是一个如果您的代码收到dates 并且可能在dateDao.retrieveDates 仍在写入地图时继续访问地图,则该线程的操作会有很大差异。由于这是一个很大的区别(并且可能会引入ConcurrentModificationException),因此不允许 JIT 这样做。而且由于不允许在线程内这样做,所以在有多个线程的情况下也不能这样做。
【解决方案2】:

据我所知,声明地图 volatile 不会同步其访问(即读者可以在 dao 更新地图时阅读地图)。但是,它保证映射存在于共享内存中,因此每个线程在每个给定时间都会在其中看到相同的值。当我需要同步和新鲜时,我通常会使用锁定对象,类似于以下内容:

private DateDao dateDao;
private volatile Map<String, Date> dates;
private final Object _lock = new Object();

public Map<String, Date> getDates() {
   synchronized(_lock) {
     return Collections.unmodifiableMap(dates);
   }
}

public retrieveDates() {
   synchronized(_lock) {
     dates = dateDao.retrieveDates();
   }
}

这提供了读取器/写入器同步(但请注意,写入器没有优先级,即如果读取器正在获取地图,写入器将不得不等待)和通过volatile 提供的“数据新鲜度”。此外,这是一种非常基本的方法,还有其他方法可以实现相同的功能(例如 Locks 和 Semaphores),但大多数时候这对我来说很有效。

【讨论】:

  • “但是,它保证映射存在于共享内存中,因此每个线程在每个给定时间都会在其中看到相同的值。” 不,它保证在dates 中对其的reference 将在线程间保持一致。对于引用所指对象的状态,它根本不做任何事情。
  • 嘿@T.J.Crowder 感谢您的澄清!所以基本上 volatile 只保证引用的对象没有被替换,而不是它的状态没有改变?我想为了保证跨线程的状态是一致的,我需要一个不可变的对象,这是实现它的唯一方法吗?
  • 对,volatile 指的是变量,而不是它所指的东西。
  • 好吧,你能解释一下锁声明中的静态吗?例如,如果我们有 1000 个对象...
  • 你是对的,在这种情况下不需要静态。抱歉,我的错误(static 存在是因为我也将这种方法用于单例,在这种情况下需要它,因为 getInstance 方法必须是静态的)。
猜你喜欢
  • 2010-09-30
  • 1970-01-01
  • 2014-08-11
  • 2016-07-09
  • 1970-01-01
  • 2018-12-25
  • 2021-10-04
  • 2016-07-30
  • 1970-01-01
相关资源
最近更新 更多