首页 > 解决方案 > 关于客户端锁定(同步块)的 CoreJava 第 11 版线程问题

问题描述

我正在阅读以下关于为什么不建议使用客户端锁定的部分:

“有时,程序员使用对象的锁来实现额外的
原子操作——这种做法称为客户端锁定。
例如,考虑 Vector 类,它是一个方法同步的列表。
现在假设我们存储了银行余额在 Vector 中。这是
传输方法的简单实现:

public void transfer(Vector accounts, int from, int to, int amount) //
{
accounts.set(from, accounts.get(from) - amount);
account.set(to, accounts.get(to) + amount);
System.out.println(...);
Vector
类的 get 和 set 方法是同步的,但这对
我们没有帮助。
在第一次调用 get 完成后,线程完全有可能在 transfer 方法中被抢占。然后另一个
线程可以将不同的值存储到相同的位置。但是,我们
可以劫持锁:

公共无效转移(矢量帐户,int from,int to,int amount)
{
同步(帐户)
{
accounts.set(from,accounts.get(from)-金额);
account.set(to, accounts.get(to) + amount);
}
System.out.println(...);
}

这种方法有效,但它完全取决于 Vector
类对其所有 mutator 方法使用内部锁
这一事实。然而,这
真的是事实吗?Vector 类的文档没有做出这样的
承诺。您必须仔细研究源代码,并希望将来的
版本不要引入不同步的mutators。如您所见,客户
端锁定非常脆弱,通常不推荐使用。”

问题:
既然 transfer 方法中的 synchronized(accounts) 已经获得了 accounts 的内在锁,那为什么依赖于 vector 类的所有 mutator 方法都使用了内在锁(以粗斜体突出显示?

标签: javajava-8java-11

解决方案


如果 的唯一突变accounts Vector发生在方法中,那么同步它的突变器transfer就无关紧要了。Vector

但是通过锁定与现有mutator方法(即 )相同的对象Vector,我们可以防止Vector.

这不仅仅是由另一个线程完成的传输,它可能会破坏我们的数据,而是,例如,to在我们读取余额之后和设置之前在账户上执行的存款。

正如 Holger 指出的那样,一旦您在一个线程中发生突变并在另一个线程中读取,如果您想要一致的数据视图,即使读取操作也需要同步。

正如Core Java所建议的那样,最好封装要保护的数据,例如(玩具示例)

public class Accounts {
    private final List<Integer> accounts = new ArrayList();

    public synchronized void transfer(int from, int to, int amount) {
        accounts.set(from, accounts.get(from) - amount);
        accounts.set(to, accounts.get(to) + amount);
    }
    
    public synchronized void deposit(int to, int amount) {
        accounts.set(to, accounts.get(to) + amount);
    }
    
    public synchronized List<Integer> getAccountsSnapshot() {
        // don't return our internal data structure, make a defensive copy
        return new ArrayList(accounts);
    }
}

返回我们的数据副本执行两个功能:

  • 我们不提供对内部数据的引用,因此客户端无法ArrayList直接修改其中的值,而不使用我们提供的 API。
  • 客户获得账户余额的一致快照,因此他们可以汇总所有账户并获得在他们调用时getAccountsSnapshot有效的总数。否则,在他们取总时进行修改可能意味着他们得到了一个在“现实生活”中从未发生过的总数。

推荐阅读