【问题标题】:Java Memory Model: reordering and concurrent locksJava 内存模型:重新排序和并发锁
【发布时间】:2011-02-04 07:24:27
【问题描述】:

java meomry 模型要求在同一监视器上同步的synchronize 块对在这些块中修改的变量强制执行前后处理。示例:

// in thread A
synchronized( lock )
{
  x = true;
}

// in thread B
synchronized( lock )
{
  System.out.println( x );
}

在这种情况下,只要线程 A 已经通过了 synchronized 块,线程 B 就会看到 x==true。现在我正在重写大量代码以使用java.util.concurrent 中更灵活(据说更快)的锁,尤其是ReentrantReadWriteLock。所以这个例子看起来像这样:

EDIT:该示例已损坏,因为我错误地转换了代码,如 matt b 所述。修正如下:

// in thread A
lock.writeLock().lock();
{
  x = true;
}
lock.writeLock().unlock();

// in thread B
lock.readLock().lock();
{
  System.out.println( x );
}
lock.readLock().unlock();

但是,我在内存模型规范中没有看到任何暗示这种锁也暗示着必要的排序。查看实现,它似乎依赖于对 AbstractQueuedSynchronizer 中的 volatile 变量的访问(至少对于 sun 实现)。然而,这不是任何规范的一部分,而且对非易失性变量的访问并没有真正考虑由这些变量给出的内存屏障覆盖,是吗?

所以,这是我的问题:

  • 假设与“旧”synchronized 块相同的顺序是否安全?
  • 这是否记录在某处?
  • 访问任何 volatile 变量是否是任何其他变量的内存屏障?

问候, 史蒂芬

--

对 Yanamon 的评论:

看下面的代码:

// in thread a
x = 1;
synchronized ( a ) { y = 2; }
z = 3;

// in thread b
System.out.println( x );
synchronized ( a ) { System.out.println( y ); }
System.out.println( z );

据我了解,内存屏障强制第二个输出显示 2,但不能保证对其他变量产生影响......?那么这如何与访问 volatile 变量相比呢?

【问题讨论】:

  • 关于您添加的代码的注释,如果线程 b 在线程 a 之前获得了锁,则线程 b 只会打印 2... 这有点暗示,但我只是想澄清一下。但要回答你 volatile 的问题,请按以下方式使用 volatile 来强制可见性:-------- volatile boolean memoryBarrier = false; int unguardedValue = 0; //线程a: unguardedValue = 10;记忆屏障=真; // thread b if (memoryBarrier) { // unguardedValue 保证读取为 10; }
  • 好吧,我猜在 cmets 中编写代码并不能很好地工作,我用一个例子更新了我的答案

标签: java concurrency locking synchronized


【解决方案1】:

来自API-doc:

所有 Lock 实现都必须强制执行 相同的内存同步 内置提供的语义 监视器锁定,如 Java 语言规范,第三 版本(17.4 内存模型):

* A successful lock operation has the same memory synchronization effects as a successful Lock action.
* A successful unlock operation has the same memory synchronization effects as a successful Unlock action.

锁定和解锁不成功 操作和可重入 锁定/解锁操作,不要 需要任何内存同步 效果。

【讨论】:

  • 你是绝对正确的。看了很多东西,但似乎完全错过了Lock界面本身......
  • 你能评论一下这个不稳定的问题吗?
  • @Steffen Heil:如果我没记错的话,对 volatile 变量的任何访问都只会与对同一变量的其他访问同步效果,即不能保证提供某种通用内存屏障。对 JLS 的快速扫描似乎证实了这一回忆。但是,请谨慎对待这一点,因为自从我上次近距离接触记忆模型及其含义以来已经有一段时间了......
  • 查看java.sun.com/docs/books/jls/third_edition/html/… 了解如何读取和写入 volatile 强制同步顺序,这更像是一般的内存屏障,而不仅仅是 volatile 变量的最新读/写。跨度>
【解决方案2】:

除了内存模型的语义保证什么的问题之外,我认为您发布的代码存在一些问题。

  1. 您在同一个锁上同步两次 - 这是不必要的。使用Lock 实现时,您无需使用synchronized 块。
  2. 使用Lock 的标准习惯用法是在try-finally 块中这样做,以防止意外解锁锁(因为在进入您所在的任何块时,锁不会自动释放,就像@987654324 @块)。

你应该使用Lock 类似的东西:

lock.lock();
try {
    //do stuff
}
finally { 
    lock.unlock();
}

【讨论】:

  • 你是对的,我的例子被打破了。我解决了这个问题。是的,我通常使用 try/finally,只是为了简短而将其留在这里。
【解决方案3】:

现在,读取和写入 volatile 变量强制发生在操作排序之前和之后。写入 volatile 变量与释放监视器具有相同的效果,读取变量具有获取监视器的效果。下面的例子让它更清楚一点:

volatile boolean memoryBarrier = false;
int unguardedValue = 0;

//thread a:
unguardedValue = 10;
memoryBarrier = true;

// thread b
if (memoryBarrier) {
  // unguardedValue is guaranteed to be read as 10;
}

但话虽如此,您提供的示例代码看起来并没有真正使用ReentrantLock,因为它被设计为使用。

  1. 使用Lock 和Java 内置的syncronized 关键字可以有效地使访问锁已经是单线程的,因此它不会给Lock 做任何实际工作的机会。李>
  2. 获取释放Lock 应该按照下面的模式完成,这在Lock 的java 文档中进行了概述

lock.readLock().lock();
try {
  // Do work
} finally {
  lock.readLock.unlock();
}

【讨论】:

  • 请查看有问题的评论。
  • 我知道线程 b 不会看到 unguardedValue,因为排序不会影响可见性并且 unguardedValue 不是易失性的,因此线程 b 不一定可见。是这样吗?
【解决方案4】:

Yanamon,我不确定您是否正确 - 但原因与您提出的论点不同。

unguardedVariable 变量可以在线程“a”中重新排序,使其值设置为 10memoryBarrier 设置为 true。

“不保证一个线程中的操作将按照程序给定的顺序执行,只要在那个线程中检测不到重新排序 - 即使重新排序对其他线程很明显"

Java 并发实践,Brian Goetz,p34

更新:我所说的在旧内存模型的情况下是正确的。所以,如果你想在任何地方写一次运行,那么我的论点就成立了。然而,在新的内存模型中,情况并非如此,因为在存在 volatile 访问的情况下,围绕非易失性变量重新排序的语义变得更加严格(参见 http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-02
    • 2017-02-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-19
    • 1970-01-01
    相关资源
    最近更新 更多