【问题标题】:Do I need to add some locks or synchronization if there is only one thread writing and several threads reading?如果只有一个线程写入和多个线程读取,我是否需要添加一些锁或同步?
【发布时间】:2014-11-25 07:26:05
【问题描述】:

假设我有一个全局对象:

class Global {
   public static int remoteNumber = 0;
}

有一个线程定期运行以从远程获取新号码,并更新它(仅写入):

new Thread {
   @override
   public void run() {
       while(true) {
           int newNumber = getFromRemote();
           Global.remoteNumber = newNumber;
           Thread.sleep(1000);
       }
   }
}

并且有一个或多个线程随机使用这个全局remoteNumber(只读):

int n = Global.remoteNumber;
doSomethingWith(n);

您可以看到我没有使用任何锁或synchronize 来保护它,对吗?是否有任何可能导致问题的潜在问题?


更新:

就我而言,读取线程必须实时获取最新的新值并不重要。我的意思是,如果有任何问题(由于缺少锁定/同步)使一个读取线程错过了该值,那没关系,因为它很快就会有机会运行相同的代码(可能在循环中)

但不允许读取未确定的值(我的意思是,如果旧值为 20,则新更新值为 30,但读取线程读取不存在的值例如 33,我不确定是否可能)

【问题讨论】:

  • 是的,一个线程写入多个线程读取正是需要同步的情况。
  • 对于那个用例,你会遇到可见性问题:阅读线程可能看不到全局数字的新值。您至少需要使其 volatile 或更好,才能使用 AtomicInteger。
  • 阅读线程可能永远看到新值。为什么要使用不正确的代码而不是正确的代码,尤其是当您只需要使用 AtomicInteger 或 volatile int 来使其正确时?你真的认为这会导致任何性能问题吗?不会的。
  • @JBNizet,谢谢,我只是不明白为什么我们应该使用这些保护措施,而不仅仅是盲目地复制这些模式。但是感谢您的出色回答,现在我变得更加清晰了

标签: java multithreading locking synchronized


【解决方案1】:

您需要在此处进行同步(有一个警告,我将在稍后讨论)。

主要问题是读取线程可能永远看不到写入线程所做的任何更新。通常最终会看到任何给定的写入。但是在这里,您的更新循环非常简单,以至于写入可以很容易地保存在缓存中,并且永远不会进入主内存。所以你真的必须在这里同步。

EDIT 11/2017 我将对此进行更新,并说一个值可以在缓存中保存这么长时间可能是不现实的。我认为这是一个问题,尽管像这样的变量访问可以由编译器优化并保存在寄存器中。所以仍然需要同步(或volatile)来告诉优化器确保为每个循环实际获取一个新值。

因此您要么需要使用volatile,要么需要使用(静态)getter 和setter 方法,并且需要在这两种方法上使用synchronized 关键字。对于像这样的偶尔写入,volatile 关键字的重量要轻得多

需要注意的是,如果您确实不需要看到来自写入线程的及时更新,则不必进行同步。如果无限延迟不会影响您的程序功能,您可以跳过同步。但是在计时器上这样的事情看起来并不是一个省略同步的好用例。

编辑:根据 Brian Goetz 在Java Concurrency in Practice 中的说法,Java/a JVM 不允许向您显示“不确定”值——从未写入的值。这些在技术上被称为“无中生有”的值,它们被 Java 规范所禁止。您可以保证看到之前对全局变量进行的一些写入,或者是初始化时使用的零,或者是一些后续写入,但不允许使用其他值。

【讨论】:

  • 我想知道是否有任何运行示例可以在没有volatilesynchronization 的情况下演示此问题?
  • 如果你有多个核心,我想你可以做到。只有几个核心,进程将被交换得足够多,我认为您不会注意到更新滞后。但是如果你有更多的内核,我认为编写器线程实际上可以在单个 CPU 上停留足够长的时间以产生明显的效果。
  • 嗯,如果你有一个 volatile 和一个 non-volatile 字段,你也许可以演示它,但你必须小心:同步实际上会更新 ALL在 Java 中写入,因此对 volatile 字段的写入将更新其他非易失性字段。 (Java 中的同步基本上是一个内存栅栏,如果你知道那是什么的话。一条刷新缓存的机器指令,它会更新所有写入。)
  • 说明它很简单:启动一个线程,等待非易失性布尔值变为 false 停止。在另一个线程中更改其他变量,并注意您的程序永远不会停止。关于无中生有的值,请注意:如果您将 int 更改为 long,并且不使其易失或同步,您可能会看到一个从未分配过的 long 值,这仅仅是因为分配不是必须是原子的:它可以分配前 4 个字节,然后分配接下来的 4 个字节。再一次,一个 volatile 或(最好是 IMO)一个 AtomicLong 确保不会发生这种情况。
  • @JBNizet 不,这不是真的。启动一个等待非易失性布尔值变为假的线程停止,它将立即停止。有了这些东西,Java 变得更好了。实际上,如果一个线程正在写入和多个读取时不同步,则不会有任何问题。不过,Volatile 当然是更清洁的方式。
【解决方案2】:

读取线程可以读取未确定时间的旧值,但实际上没有问题。这是因为每个线程都有自己的这个变量的副本。有时它们会同步。您可以使用 volatile 关键字来删除此优化:

public static volatile int remoteNumber = 0;

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-04
    • 2012-08-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多