线程锁定顺序与简单同步块的比较

3

在阅读《Java并发实践》一书时,遇到了这段代码,其中“fromAccount”和“toAccount”对象被依次锁定,以防止动态锁顺序死锁。

public void transferMoney(Account fromAccount,Account toAccount) {
    **synchronized (fromAccount) {**
        **synchronized (toAccount) {**
               ........
        }
    }
}

我对为什么需要这种锁定顺序感到困惑。如果我们只是想确保两个对象同时被锁定,那么如果存在一个常规的同步块,在该块内访问fromAccount和toAccount对象,那么不是会得到相同的效果吗?我确定我在这里忽略了一些基本概念。谢谢你的帮助。

public void transferMoney(Account fromAccount,Account toAccount) {
    synchronized (this) {
        fromAccount.someMethod();
        toAccount.someMethod();        
    }
}

2
这个锁在账户之外,因此如果其他线程正在fromAccount对象中执行某些操作,它将不会被锁定。 - RealSkeptic
顺便问一下,你是指的第208页上的例子吗? - RealSkeptic
@RealSkeptic,它在第10章清单10.2中。 - seeker
3个回答

2
您要避免的是锁定顺序示例的替代方案:使用一个中央锁,因为这样您就无法进行并发传输,一切都在等待那一个锁,只有一个传输可以同时进行。不清楚“this”是什么或其范围可能是什么,但如果存在多个此传输服务实例,则锁定没有任何好处,因为涉及一个帐户的一个传输可以通过一个实例进行,而涉及该帐户的另一个传输可以通过另一个实例进行。因此,似乎只能有一个实例,这将使您的并发性降至一次传输。您不会死锁,但也不会快速处理大量传输。
这个玩具示例背后的想法(您不应将其误认为任何人转移资金的方式)是通过锁定涉及到的各个帐户来获得更好的并发性,因为对于许多传输,涉及的帐户并不涉及其他并发传输,您希望能够并发处理它们,并通过最小化正在进行的锁定的范围来最大化并发性。但是,如果某个帐户涉及多个并发传输,并且某些传输以不同的顺序获取锁定,则此方案将遇到问题。

例如,想象一下,如果您试图向朋友转账,网页上显示“转账已排队;您前面有239,341笔转账——预计需要4.5小时。”您可能会感到有点恼火,并合理地问道:“这些转账都与我或我的朋友无关,为什么我还要等它们?” - yshavit

1
首先,需要注意的是您提供的示例(根据您的评论,它在第208页,列表10.2)是一个坏的示例-会导致死锁。这些对象没有按顺序锁定,以防止动态锁定顺序死锁,它们是动态锁定顺序发生的示例!
现在,您建议在this上进行锁定,但是这个this到底是什么,锁定的范围是什么?
  • 很明显,所有操作都必须使用相同的对象-取款、存款、转账。如果对它们使用不同的对象,则一个线程可以在帐户A上进行存款,而另一个线程则从帐户A转移到帐户B,他们将不使用相同的锁,因此余额将受到损害。因此,所有访问同一帐户的锁对象应该是相同的。
  • 正如Nathan Hughes所解释的那样,需要本地化锁定。我们不能为所有帐户使用一个中央锁对象,否则尽管实际上没有在同一资源上工作,但它们都将互相等待。因此,使用中央锁定对象也行不通。
看起来我们需要对锁进行本地化,以便每个账户的余额都有自己的锁,这样可以允许不相关的账户之间进行并行操作,但是这个锁必须用于所有操作——提款、存款和转账。
问题在于,当只有提款或存款时,您只操作一个账户,因此您只需要锁定该账户。但是当您进行转账时,涉及到两个对象。因此,如果有其他线程想要操作其中任何一个账户,您需要锁定它们两个的余额。
任何持有两个或更多账户单个锁的对象都将破坏上述两点中的一点。它要么不会用于所有操作,要么就不足够本地化。
这就是为什么他们尝试按顺序锁定两个锁的原因。他们的解决方案是使Account对象本身成为账户的锁——这既满足了“所有操作”条件,又满足了“本地性”条件。但是,在转移资金之前,我们仍然需要确保我们拥有两个账户的锁。

但是,这个源代码又是一个死锁易发生的例子。这是因为一个线程可能想要从账户A转账到账户B,而另一个线程则想要从账户B转账到账户A。在这种情况下,第一个线程锁定了A账户,第二个线程锁定了B账户,然后它们陷入了死锁,因为它们以相反的顺序执行了锁定。


0

这里的基本原则是要避免竞态条件。在您的情况下,如果任何其他类中有另一种方法也将资金转移至toAccount,则可能会在toAccount中更新不正确的金额。例如,有两个执行资金转移的类。

一个类有一个方法:

public void transferMoney(Account fromAccount,Account toAccount) {
    synchronized (this) {
        fromAccount.someMethod();
        toAccount.someMethod();        
    }
}

还有其他类包含:

public void transferMoneyNow(Account fromAccount1,Account toAccount) {
    synchronized (this) {
        fromAccount1.someMethod();
        toAccount.someMethod();        
    }
}

如果两种方法同时进行,由于竞态条件,可能会将错误的金额更新到toAccount中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接