Java 内存模型:重新排序和并发锁

发布于 2024-08-27 20:33:04 字数 1336 浏览 6 评论 0原文

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。所以这个例子看起来像这样:

编辑:这个例子被破坏了,因为我错误地转换了代码,正如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 内部易失性变量的访问(至少对于 sun 实现而言)。然而,这不是任何规范的一部分,而且对非易失性变量的访问并不真正被视为由这些变量给出的内存屏障覆盖,不是吗?

所以,我的问题是:

  • 假设与“旧”同步块相同的顺序是否安全?
  • 这有记录在某处吗?
  • 访问任何易失性变量是否会成为任何其他变量的内存屏障?

问候, Steffen

-

对 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 变量有何不同呢?

The java meomry model mandates that synchronize blocks that synchronize on the same monitor enforce a before-after-realtion on the variables modified within those blocks. Example:

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

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

In this case it is garanteed that thread B will see x==true as long as thread A already passed that synchronized-block. Now I am in the process to rewrite lots of code to use the more flexible (and said to be faster) locks in java.util.concurrent, especially the ReentrantReadWriteLock. So the example looks like this:

EDIT: The example was broken, because I incorrectly transformed the code, as noted by matt b. Fixed as follows:

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

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

However, I have not seen any hints within the memory model specification that such locks also imply the nessessary ordering. Looking into the implementation it seems to rely on the access to volatile variables inside AbstractQueuedSynchronizer (for the sun implementation at least). However this is not part of any specification and moreover access to non-volatile variables is not really condsidered covered by the memory barrier given by these variables, is it?

So, here are my questions:

  • Is it safe to assume the same ordering as with the "old" synchronized blocks?
  • Is this documented somewhere?
  • Is accessing any volatile variable a memory barrier for any other variable?

Regards,
Steffen

--

Comment to Yanamon:

Look at the following code:

// 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 );

From what I understood, the memory barrier enforces the second output to show 2, but has no guaranteed affect on the other variables...? So how can this be compared to accessing a volatile variable?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

口干舌燥 2024-09-03 20:33:05

来自 API 文档 :

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

* 成功的锁定操作与成功的锁定操作具有相同的内存同步效果。
* 成功的解锁操作与成功的解锁操作具有相同的内存同步效果。

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

From the API-doc:

All Lock implementations must enforce
the same memory synchronization
semantics as provided by the built-in
monitor lock, as described in The
Java Language Specification, Third
Edition (17.4 Memory Model):

* 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.

Unsuccessful locking and unlocking
operations, and reentrant
locking/unlocking operations, do not
require any memory synchronization
effects.

厌味 2024-09-03 20:33:05

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

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

您应该使用类似于以下内容的 Lock

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

Beyond the question of what the semantics of the memory model guarantees, I think there are a few problems with the code you are posting.

  1. You are synchronizing twice on the same lock - this is unnecessary. When using a Lock implementation, you don't have need to use the synchronized block.
  2. The standard idiom for using a Lock is to do so in a try-finally block to prevent accidental unlocking of the lock (since the lock is not automatically released when entering whatever block you are in, as with the synchronized block).

You should be using a Lock with something resembling:

lock.lock();
try {
    //do stuff
}
finally { 
    lock.unlock();
}
撧情箌佬 2024-09-03 20:33:05

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

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.readLock().lock();
try {
  // Do work
} finally {
  lock.readLock.unlock();
}

Reading and writing volatile variables now enforces happens before and happens after operation ordering. Writing to a volatile variable has the same effect as releasing a monitor and reading a variable has the effect as acquiring a monitor. The following example makes it a little more clear:

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;
}

But that all being said the sample code you provided did not look like it was really using the ReentrantLock as it was designed to be used.

  1. Surrounding the use of a Lock with the the Java's built in syncronized keyword effectively makes access to the lock already single threaded so it doesn't give the Lock a chance to do any real work.
  2. Acquiring a releasing a Lock should be done following the pattern below, this is outlined in the java docs of Lock

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

浅沫记忆 2024-09-03 20:33:05

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

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

“只要该线程内无法检测到重新排序,就不能保证一个线程中的操作将按照程序给定的顺序执行 - 即使重新排序对于其他线程

Java 并发实践,Brian Goetz,p34

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

Yanamon, I am not sure you are correct - but for different reasons than the argument you are making.

The unguardedVariable variable may be re-ordered in thread "a" such that its value is set to 10 after memoryBarrier is set to true.

"There is no guarantee that operations in one thread will be performed in the order given by the program, as long as the reordering is not detectable within that thread - even if the reordering is apparent to other threads"

Java Concurrency in Practise, Brian Goetz, p34

UPDATED: what I said is true in the case of the old memory model. So, if you want write-once-run-anywhere then my argument stands. However, in the new memory model, it is not the case as the semantics surrounding re-ordering of non-volatile variables in the presence of volatile access has become stricter (see http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文