问题 Java:同步操作与波动性的关系究竟如何?


对不起,这是一个很长的问题。

我最近在多线程中进行了大量研究,因为我慢慢将其应用到个人项目中。然而,可能由于存在大量略微不正确的例子,在某些情况下使用同步块和波动对我来说仍然有点不清楚。

我的核心问题是:当一个线程在同步块内部时,对引用和原语的更改是否自动易失(即,在主内存而不是缓存上执行),或者读取是否也必须同步才能使其工作正常吗?

  1. 如果是这样 同步简单的getter方法的目的是什么? (见例1) 此外,只要线程已同步到任何内容,是否所有更改都发送到主内存?例如,如果它被发送到一个非常高级别同步的地方做大量的工作,那么每次改变都会发生在主存储器中,并且没有任何东西可以缓存,直到它再次被解锁?
  2. 如果不 更改是否必须显式位于同步块内,或者java实际上是否可以接受,例如,使用Lock对象? (见例3)
  3. 如果是 同步对象是否需要与以任何方式更改的引用/原语相关(例如,包含它的直接对象)?我可以通过同步一个对象来写,如果它安全吗,可以用另一个对象阅读吗? (见例2) 

(请注意以下示例,我知道同步方法和synchronized(this)是不赞成的,为什么,但讨论超出了我的问题的范围)

例1:

class Counter{
  int count = 0;

  public synchronized void increment(){
    count++;
  }

  public int getCount(){
    return count;
  }
}

在此示例中,increment()需要同步,因为++不是原子操作。因此,同时递增的两个线程可能导致计数总体上增加1。 count原语需要是原子的(例如,不是long / double / reference),并且它很好。

getCount()需要在这里同步吗?为什么呢?我听到的最多的解释是,我不能保证返回的计数是增量前还是后增量。然而,这似乎是对某些略有不同的解释,那就是错误的地方。我的意思是,如果我要同步getCount(),那么我仍然看不到保证 - 它现在归结为不知道锁定顺序,不知道实际读取是在实际写入之前/之后。

例2:

以下示例线程是否安全,如果您假设通过此处未显示的技巧,这些方法中的任何一个都不会同时被调用?如果每次都使用随机方法完成,那么计数会以预期的方式递增,然后被正确读取,或者进行锁定  成为同一个对象? (顺便说一下,我完全意识到这个例子有多么荒谬,但我对理论比对实践更感兴趣)

class Counter{
  private final Object lock1 = new Object();
  private final Object lock2 = new Object();
  private final Object lock3 = new Object();
  int count = 0;

  public void increment1(){
    synchronized(lock1){
      count++;
    }
  }

  public void increment2(){
    synchronized(lock2){
      count++;
    }
  }

  public int getCount(){
    synchronized(lock3){
      return count;
    }
  }

}

例3:

之前发生的关系只是一个java概念,还是内置于JVM中的实际内容?即使我可以保证下一个例子的概念发生 - 之前的关系,如果它是一个内置的东西,java是否足够聪明地接受它?我假设它不是,但这个例子实际上是线程安全的吗?如果它的线程安全,那么如果getCount()没有锁定呢?

class Counter{
  private final Lock lock = new Lock();
  int count = 0;

  public void increment(){
    lock.lock();
    count++;
    lock.unlock();
  }

  public int getCount(){
    lock.lock();
    int count = this.count;
    lock.unlock();
    return count;
  }
}

2164
2018-05-28 07:46


起源



答案:


是的,读取也必须同步。 这一页 说:

一个线程写入的结果保证对a可见   只有在写入操作发生之前,才由另一个线程读取   读操作。

[...]

监视器的解锁(同步块或方法退出)   发生在每个后续锁定之前(同步块或方法)   那个监视器的入口)

同一页说:

“释放”同步器方法之前的操作,例如Lock.unlock,   Semaphore.release和CountDownLatch.countDown发生在操作之前   在成功的“获取”方法之后,例如Lock.lock

因此,锁提供与同步块相同的可见性保证。

无论您使用同步块还是锁,只有读取器线程使用时,才能保证可见性 相同 监视或锁定作者线程。

  • 示例1不正确:如果要查看计数的最新值,则必须同步getter。

  • 您的示例2不正确,因为它使用不同的锁来保护相同的计数。

  • 你的例子3没问题。如果getter没有锁定,您可以看到较旧的计数值。之前发生的事情是由JVM保证的。例如,JVM必须通过将缓存刷新到主存储器来遵守指定的规则。


8
2018-05-28 07:55



我认为我从中获得的是,如果一个线程A同步一个对象,然后线程B同步同一个对象 - 那时,线程B将拥有所有“内存视图”线程A,并且该变量在锁定期间改变与它没有任何关系?当发生这种情况时,线程A缓存是否会与线程B追溯同步,还是单向? - Numeron
这是一种方式。当B进入同步块时,它肯定会看到A中发生的所有变化 在A发布监视器之前。在A发布监视器后,B不一定会看到A中发生的所有变化。 - JB Nizet
谢谢,为我点击了很多东西。 - Numeron


答案:


是的,读取也必须同步。 这一页 说:

一个线程写入的结果保证对a可见   只有在写入操作发生之前,才由另一个线程读取   读操作。

[...]

监视器的解锁(同步块或方法退出)   发生在每个后续锁定之前(同步块或方法)   那个监视器的入口)

同一页说:

“释放”同步器方法之前的操作,例如Lock.unlock,   Semaphore.release和CountDownLatch.countDown发生在操作之前   在成功的“获取”方法之后,例如Lock.lock

因此,锁提供与同步块相同的可见性保证。

无论您使用同步块还是锁,只有读取器线程使用时,才能保证可见性 相同 监视或锁定作者线程。

  • 示例1不正确:如果要查看计数的最新值,则必须同步getter。

  • 您的示例2不正确,因为它使用不同的锁来保护相同的计数。

  • 你的例子3没问题。如果getter没有锁定,您可以看到较旧的计数值。之前发生的事情是由JVM保证的。例如,JVM必须通过将缓存刷新到主存储器来遵守指定的规则。


8
2018-05-28 07:55



我认为我从中获得的是,如果一个线程A同步一个对象,然后线程B同步同一个对象 - 那时,线程B将拥有所有“内存视图”线程A,并且该变量在锁定期间改变与它没有任何关系?当发生这种情况时,线程A缓存是否会与线程B追溯同步,还是单向? - Numeron
这是一种方式。当B进入同步块时,它肯定会看到A中发生的所有变化 在A发布监视器之前。在A发布监视器后,B不一定会看到A中发生的所有变化。 - JB Nizet
谢谢,为我点击了很多东西。 - Numeron


尝试用两个截然不同的简单操作来查看它:

  1. 锁定(互斥),
  2. 内存屏障(缓存同步,指令重新排序屏障)。

进入一个 synchronized 块需要锁定和记忆障碍;离开了 synchronized 块需要解锁+内存屏障;读/写 volatile 字段只需要内存屏障。用这些术语思考我认为你可以自己澄清上面的所有问题。

对于示例1,读取线程将不具有任何类型的存储器屏障。它不仅仅是在读取之前/之后看到的值之间的关系 决不 观察 任何 在线程启动后更改为var。

示例2.是您提出的最有趣的问题。在这种情况下,JLS确实没有给出任何保证。在实践中,您将不会获得任何订购保证(就像是 锁定 方面根本不存在),但你仍然会受益于记忆障碍,所以你会观察到变化,这与第一个例子不同。基本上,这与删除完全相同 synchronized 和标记 int 如 volatile (除了获取锁的运行时成本)。

关于示例3,通过“只是一个Java事物”,我觉得你有一些关于擦除的泛型,只有静态代码检查才能知道。这不是那样的 - 锁和内存屏障都是纯运行时工件。实际上,编译器根本无法对它们进行推理。


6
2018-05-28 07:52



我在示例2中不同意您的观点:您没有任何可见性保证,因为读者不会在同一个监视器上与作者同步。看到我的回答。 - JB Nizet
@JBNizet JLS并没有给你任何保证(我在回答中说了很多),但是我没有看到你在实践中如何构建一个与我所说的不同的系统。它必须专门设计为故障。 - Marko Topolnik
我不知道,因为我对硬件,处理器缓存等一无所知。但我猜想Sun / Oracle工程师会这样做,如果他们坚持使用“同一台显示器”,那么就有充分的理由。如果完全不可能,他们可以提供更好的可见性保证。但他们没有。 - JB Nizet
@JBNizet我不同意对Sun工程师的评估。 JLS经过精心编写,以尽量减轻实施者的负担,仍然提供有用的保证。他们没有理由用硬保证来掩盖我所说的内容,因为依赖这种行为将是对线程协调原语的恶意滥用。然而,恰恰相反,在满足强制保证的同时,您会将此行为视为副作用。 - Marko Topolnik