【问题标题】:Why do I need to use synchronized for multiple threads over volatile?为什么我需要在 volatile 上对多个线程使用同步?
【发布时间】:2017-03-27 20:35:41
【问题描述】:

有人说如果多个线程在读/写,那么你需要使用同步,如果一个线程在读/写而另一个线程只在读,那么你必须使用 volatile。我不明白这两种情况之间的区别。

基本上,在对 volatile 字段的写入操作完成后,所有读取器(尤其是其他线程)都可以看到该字段的值。

然后,如果我将变量定义为 volatile,首先 threadA 将读取其值,threadA 将更新其值并将其写入内存。之后该变量将对 threadB 可见。那我为什么需要同步块呢?

【问题讨论】:

  • 我建议你做一些independent reading。这个问题比你意识到的要复杂得多。但举个简单的例子——如果我想以原子方式设置 两个 变量怎么办?
  • 另外推荐:《Java并发实战》
  • @dnault 鉴于当时的问题,这似乎有点太高级了......
  • @BoristheSpider 当您尝试以原子方式设置两个变量时,您将使用 volatile 定义这两个变量,并且 volatile 字段的值将对所有其他线程变得不可见,直到第一个线程完成其工作。顺便说一句,您的链接支持我的想法。如果将 c 变量定义为 volatile,则不再需要同步方法。
  • @hellzone 非常非常错误和非常危险的话。 c++c-- 不是原子操作,volatile 不会帮助你。欢迎来到并发世界...

标签: java multithreading


【解决方案1】:

有人说如果多个线程在读/写,那么你需要使用同步,如果一个线程在读/写而另一个线程只在读,那么你必须使用 volatile。我不明白这两种情况之间的区别。

这确实没有硬性规定。选择是否使用synchronizedvolatile 更多地与对象的更新方式有关,而不是有多少读者或作者。

例如,您可以使用包含volatile longAtomicLong 来实现多个读取器和写入器。

  private AtomicLong counter = new AtomicLong();
  ...
  // many threads can get/set this counter without synchronized
  counter.incrementAndGet();

在某些情况下,即使只有一个读取器/写入器,您也需要 synchronized 块。

synchronized (status) {
   status.setNumTransactions(dao.getNumTransactions());
   // we don't want the reader thread to see `status` partially updated here
   status.setTotalMoney(dao.getTotalMoney());
}

在上面的示例中,由于我们多次调用以更新status 对象,因此我们可能需要确保在更新 num-transactions 而不是总金额时其他线程看不到它。是的,AtomicReference 处理其中一些情况,但不是全部。

需要明确的是,标记字段volatile 可确保内存同步。当您读取volatile 字段时,您会越过读取内存屏障,而当您写入它时,您会越过写入内存屏障。 synchronized 块在开始时有一个读内存屏障,在块结束时有一个写屏障并且具有互斥锁以确保一次只有一个线程可以进入该块。

有时您只需要内存屏障来实现线程之间正确共享数据,有时您需要锁定。

【讨论】:

    【解决方案2】:

    正如 cmets 所建议的,您可以进一步阅读。但是为了给你一个想法,你可以看看这个stackoverflow question,例如考虑以下场景:

    您有几个变量需要处于正确的状态。但是,尽管您将它们全部设置为 volatile,您仍需要时间通过执行一些代码来更新它们。

    恰好这段代码几乎可以同时被不同的线程执行。第一个变量可能是“OK”并且以某种方式同步,但其他一些变量可能依赖于第一个变量并且还不正确。因此在这种情况下你需要一个同步块。

    再添加一篇文章以进一步阅读有关 volatile look here

    【讨论】:

    • volatile 变量的值将对所有其他线程变得不可见。如何“代码几乎可以同时被不同的线程执行”?
    • @hellzone 但在示例中,需要更新两个变量。假设我有一个class ConcurrentList,现在我有一个data 和一个size。我需要 1) 写入 data 和 2) 原子地更新 size。如果我首先更新size,并允许其他线程读取它,它们将读取尚未存在的数据。如果我首先更新data,那么其他线程可能会覆盖我的数据,因为size 未设置。 volatile 是一个非常专业的并发构造。
    • @hellzone 也许我遗漏了一些东西,但为什么这些变量会对所有其他线程变得不可见?我对volatile 的理解是读写直接进入主内存(无缓存)——仅此而已——无阻塞/无隐藏。也针对第二个问题。线程 A 调用函数 x() 而线程 B 调用函数 x() - 为什么不可能呢?
    • @BoristheSpider 谢谢你的例子!
    • @BoristheSpider 然后 synchronized 扩展了 volatile,我们总是可以使用 synchronized 代替 volatile。
    【解决方案3】:

    volatile 和 synchronized 之间的主要区别在于 volatile 只保证可见性,而 synchronized 保证可见性和锁定。

    如果有多个读线程和一个写线程,那么使用 volatile 可以确保写线程对 volatile 变量的更改立即对其他线程可见。但是您看到在这种情况下锁定不是问题,因为您只有 1 个写入线程。

    对于 volatile 有一些经验法则:

    1. 当 volatile 的值取决于之前的值时,不要使用它
    2. 当 volatile 参与与其他不变量的交互时,不要使用它
    3. 当有多个写入线程更新 volatile 变量的值时,不要使用 volatile。

    一般来说,volatile 的使用应仅限于那些相对容易推断其状态的情况,例如状态标志。

    在您共享可变状态的所有其他情况下,无论何时触及共享可变状态,始终使用同步,除非声明为 final 并且仅在构造函数中修改而没有不安全的发布。如我的 3 点所述,Volatile 仅在特殊情况下才替代同步。

    【讨论】:

    • AtomicLong 和其他对象成功使用volatile 但仍然支持incrementAndGet(),因此违反了您的#1 和#2。它们还允许多个作者。
    猜你喜欢
    • 1970-01-01
    • 2020-08-29
    • 1970-01-01
    • 2014-05-16
    • 2012-11-21
    • 2021-12-06
    • 2020-03-15
    • 2016-03-20
    • 1970-01-01
    相关资源
    最近更新 更多