【问题标题】:Are there concurrency concerns when one threads initializes the value and other threads just read it当一个线程初始化值而其他线程只读取它时是否存在并发问题
【发布时间】:2017-01-03 23:13:14
【问题描述】:

假设我有一个这样的共享对象:

class Shared {

   private Value val;

   //synchronized set
   public synchronized void setValue(Value val) {
      if (this.val == null) {
         this.val = val;
      } else {
          throw new IllegalStateException();
      }
   }

   //unsynchronized get
   public Value getValue() {
        return this.val;
   }
}

如果我有一个线程在应用程序初始化期间设置了值,在任何其他线程有机会读取它之前,并且没有任何东西再次更改该值,那么读取非同步值是否安全,或者我是否冒着其他线程永远看不到设置值的风险(因为变量不是volatile 并且不能保证被刷新到主内存)? 想象一下在设置发生在 Servlet 初始化期间的 Web 应用程序上下文中。我不知道此时是否已经创建了其他线程,因为这是容器的工作。但我认为届时将创建一个处理未来请求的线程拉取。

如果不安全,有没有一种方法可以安全地初始化值,而无需为每次读取永远付出代价,即使该值永远不会改变? IE。有没有办法只刷新一次值?

另外,这不就是这样吗?春天总是这样吗?在初始化容器时,单例上的各种不同步设置正在发生:通过 setter 注入 bean,@PostConstruct 初始化器触发等。一旦完成,请求就会被接受并且不会发生任何修改。如果这是不安全的,不是每个单例方法都需要同步吗?

【问题讨论】:

  • 这不是一个愚蠢的问题,不,这不安全,除非Value 是不可变的。
  • @shmosel 是因为我列出的原因还是其他原因?即使这个值再也不会被触及?
  • 描述在任何其他线程有机会阅读它之前。如何防止其他线程读取它?
  • 好的,这样的 Spring 字段应该标记为 volatile,如果您需要它们是线程安全的:stackoverflow.com/a/23992532/360211
  • @weston 查看您链接的答案中的更新。他说 Spring bean 通常是开箱即用的线程安全的,并解释了原因。

标签: java spring concurrency synchronization thread-safety


【解决方案1】:

视情况而定。

在您写入值和读取它的线程之间有很多操作可能会创建发生之前的关系,您需要保证该值存在。

我将讨论 Spring MVC(以及任何 Servlet 应用程序)的情况。 Spring MVC 通常创建两个ApplicationContext 实例:一个在ContextLoaderListener 中,一个在DispatcherServlet 中。

在典型的 Servlet 容器上,ContextLoaderListenerDispatcherServlet 将在同一个线程上按顺序初始化。然后,容器将启动线程来监听连接并服务请求。

在非典型容器上,您仍然可以依赖 ContextLoaderListenerDispatcherServlet 必须完全初始化(因此您的Spring 上下文)在它可以接收请求之前。

初始化完成后,容器将启动服务线程,and since

在线程上调用start() 发生在启动线程中的任何操作之前。

您的价值将变得可见。否则,初始化线程将通过一些其他机制(可能是CountDownLatch)通知服务线程,这些机制提供了自己的happens-before关系。

假设您只从一个线程设置该值并且您不再更改它,您甚至不需要设置器上的synchronized


寻找这些发生之前的关系,你会没事的。显然,如果您不想这样做,那么volatile 解决方案就可以了。如果您的应用程序逻辑类似于 Servlet 容器(或者是 Spring MVC 应用程序),则不需要 synchronized 或额外的 volatile。关键是

在任何其他线程有机会读取它之前,没有任何东西会再次更改该值

为了防止其他线程读取该值,您可能有一个通知机制,该机制已经添加了将正确发布您的值的发生前关系。

【讨论】:

  • 最后。真实的解释!非常感谢您,好先生!
  • 令人兴奋的部分是 Servlet 的 init()service() 之间的 Servlet 规范没有明确的发生前发生。尽管如此,正如stackoverflow.com/questions/11719916/… 中很好解释的那样,即使是GenericServlet 实现似乎也只是假设它就在那里。
【解决方案2】:

这不安全。

你需要把字段设为volatile:

private volatile Value val;

允许其他线程缓存自己的非易失性字段副本,因此一个线程对其进行的更改可能不会被其他线程“看到”。

volatile 强制所有线程立即“看到”对字段的更改(实际上,它强制线程不使用缓存副本)。

【讨论】:

  • 某种建议 volatile 是唯一的方法(它们是 synchronized 解决方案的一半)。另外,我认为它过度简化了volatiles 的角色。例如,它不会阻止编译器和 VM 重新排序操作吗?
  • @weston 它强制“发生在之前”关系:一个线程在写入 volatile 之前对任何内容所做的所有写入都会在其他线程读取 volatile 之后被其他线程看到。
【解决方案3】:

这是一场数据竞赛,所以不安全。

这是一场数据竞争,因为您有两个线程,一个写入一个值,另一个读取一个值,它们之间没有同步1。当您使用synchronized 关键字时,同步发生在第一个线程释放后一个线程获取锁2

由于getValue() 方法不同步(因此永远不会获取锁),它与任何setValue(...) 调用之间没有同步。您还可以通过将字段标记为volatile 来提供数据同步:从易失性字段读取与之前对其的任何写入同步,3 以及其他技术。对线程安全发布的完整说明过于宽泛,无法在此处给出答案。

因为您有数据竞争,所以值会以非原子方式从一个线程发布到另一个线程。假设你做到了:

Value v = new Value();
v.setName("Bond");
v.setId(7);
Shared.setValue(v);

然后有人可能会调用getValue() 并看到一个名称为“Bond”但其 id 仍然是默认字段值 (0) 的对象。它甚至有可能看到一个空名称和一个 7 的 id,即使代码似乎表明如果设置了 id 则名称必须是。数据竞赛会给您留下许多微妙的、不直观的错误。


1。 JLS 17.4.5:“当一个程序包含两个冲突的访问(第 17.4.1 节),这些访问没有按发生前的关系排序时,就说它包含数据竞争。”

2。 JLS 17.4.4 "监视器上的解锁操作 m 同步 m 上的所有后续锁定操作"。

3。还有 17.4.4

【讨论】:

  • 我想我明白了,这是另一种说法,即编译器和虚拟机都被允许在内存屏障之外重新排序操作(用于优化等),setValue 可能发生在@之前987654330@?
  • 保证读取比写入晚很多。该设置在应用程序接受请求之前完成(框架初始化代码)。两者不可能同时发生。这有什么改变吗?
  • @kaqqao 在实践中,您更有可能看到完整的价值。但不能保证“更有可能”。 保证的是,如果您不太可能看到这样的部分更新,那么调试将非常困难。 :) 例如,如果setValue() 调用发生在读取器线程甚至开始之前(如果您使用的是 ExecutorService,这将很难或不可能保证),那么您可以保证看到完整的 Value。但是你进入了相当微妙的领域,收获相对较小。正如 Bohemian 所说,您也可以使该字段不稳定。
  • @weston 谢谢 :) 我错过了你对我的回答的第一个问题,但是是的,就是这样。在重新排序和内存缓存刷新之间,您可以看到操作几乎以任何顺序发生,除非您另外告诉 java —— 而同步(包括易失性字段等)就是您这样做的方式。
  • 我对你的回答的问题是它忽略了在任何其他线程有机会阅读它之前,并且没有任何东西会再次更改值,这在原始版本中是粗体的。您正在重新散列有关发生之前和可见性的一般信息,但没有使其适应他们的问题。 OP 如何在任何机会阅读它之前执行?这很关键。有很多事情可以在不控制线程何时启动的情况下导致正确的程序顺序。
【解决方案4】:

如果有办法在构造函数中初始化元素,并使其既 final 又不可变,那么构造函数的结尾与所有启动的线程建立 happens-before 关系在构造对象之后,读取将是线程安全的。如果您在构造后的任何时间初始化元素,或者如果它不是final 或不是不可变的,那么您必须同步访问。 volatile 将保护您免受不可见指针更改的影响,但这不足以使引用的对象线程安全。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多