Java:同步操作与波动性究竟有何关系?

标签 java multithreading thread-safety volatile

抱歉,这个问题太长了。

我最近对多线程进行了大量研究,因为我慢慢地将它实现到个人项目中。然而,可能由于大量稍微不正确的示例,在某些情况下同步块(synchronized block)和波动率的使用对我来说仍然有点不清楚。

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

  1. 如果是 同步一个简单的 getter 方法的目的是什么? (参见示例 1) 此外,只要线程已同步任何内容,所有更改都会发送到主内存吗?例如,如果它被发送出去在一个非常高级别的同步中到处做大量工作,那么每次更改都会对主内存进行,并且不会缓存任何内容,直到它再次解锁?
  2. 如果不是 更改是否必须显式地在同步块(synchronized block)内进行,或者 java 是否真的可以使用 Lock 对象? (参见示例 3)
  3. 如果是 同步对象是否需要与以任何方式更改的引用/基元相关(例如,包含它的直接对象)?如果安全的话,我可以通过在一个对象上同步写入并与另一个对象读取吗? (见示例 2)

(请注意以下示例,我知道同步方法和同步(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:

happens-before 关系只是一个 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;
  }
}

最佳答案

是的,读取也必须同步。 This page说:

The results of a write by one thread are guaranteed to be visible to a read by another thread only if the write operation happens-before the read operation.

[...]

An unlock (synchronized block or method exit) of a monitor happens-before every subsequent lock (synchronized block or method entry) of that same monitor

同一页说:

Actions prior to "releasing" synchronizer methods such as Lock.unlock, Semaphore.release, and CountDownLatch.countDown happen-before actions subsequent to a successful "acquiring" method such as Lock.lock

因此锁提供与同步块(synchronized block)相同的可见性保证。

无论您使用同步块(synchronized block)还是锁,只有当读取线程使用与写入线程相同的监视器或锁时才能保证可见性。

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

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

  • 您的示例 3 没问题。如果 getter 没有锁定,您会看到一个较旧的计数值。 happens-before 是由 JVM 保证的。 JVM 必须遵守指定的规则,例如将缓存刷新到主内存。

关于Java:同步操作与波动性究竟有何关系?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10781079/

相关文章:

java - java中如何比较两个整数

java - 暂停所有线程而不等待

c# - await Task.Run(() => semaphore.WaitOne()) 有什么问题吗?

java - meteor 和java应用程序数据推送

java - 输入流仅返回 1 个字节

java - 动态查找文件

multithreading - 在进程的线程之间共享信号量与在进程之间共享信号量有什么区别?

跨子类的 C++ 互斥体和 STL 列表

ios - 正确使用 NSThread 和 NSRunLoop

ios - 这个 UIImage 数据读取器线程安全吗?