为什么一般不推荐客户端锁定?

时间:2013-08-22 23:57:46

标签: java multithreading locking synchronized

请解释以下有关Hortsmann,Cornell(第9版,第865页)中“Core Java”的同步块的说法:

  

Vector类的get和set方法是同步的,但这对我们没有帮助   ...
  但是,我们可以劫持锁:

public void transfer(Vector<Double> accounts, int from, int to, int amount)
{
    synchronized (accounts)
    {
        accounts.set(from, accounts.get(from) - amount);
        accounts.set(to, accounts.get(to) + amount);
    }
    //...
}
     

这种方法有效,但它完全取决于Vector类对其所有mutator方法使用内部锁的事实。

为什么同步取决于上述事实?如果一个线程A. 拥有对帐户的锁定,没有其他线程可以获得相同的锁定。它不依赖于Vector用于其mutator方法的锁。

我能想到的唯一可能的解释是以下一个。让线程A拥有对账户的锁定。如果Vector对其set / get使用另一个锁,那么线程A必须获得一个额外的锁以继续进行set / get,这是不可能的(由于某个原因,一个线程可以同时保存2个不同的锁吗?)。

这个解释对我来说看起来不合理,但我没有别的。 我错过了什么?

3 个答案:

答案 0 :(得分:6)

  

如果线程A拥有对账户的锁定,则其他任何线程都不能获得相同的锁定。它不依赖于Vector用于其mutator方法的锁。

但是如果Vector使用一个完全不相关的锁来进行自己的同步,那么你的锁对象将毫无意义。这样的代码不会同步:

x.transfer(vector, 100, 100, 100);  // uses your lock

vector.add(100);   // uses Vector's own, unrelated lock

如果您的所有代码都通过您自己的方法(使用您的锁),并且没有人直接访问vector的方法,那么您没问题。 (但是你根本不需要使用Vector的内置同步,并且可以使用ArrayList)。

这些锁只有在所有相关代码路径都使用它们时才有效。通常涉及多种方法,他们需要使用同一组锁来正确地“互相交谈”。程序员应该确保这一点。

  

让线程A拥有对账户的锁定。如果Vector对其set / get使用另一个锁,那么线程A必须获得一个额外的锁以继续进行set / get,这是不可能的(由于某个原因,一个线程可以同时保存2个不同的锁吗?)。

这不可能,线程A可以容纳任意数量的锁。但是,在上面的访问模式中,线程A保持第一个锁是没有意义的,因为当线程B仅使用内置的Vector锁时,它甚至不会尝试锁定它。

答案 1 :(得分:2)

  

这种方法有效,但它完全取决于Vector类对其所有mutator方法使用内部锁的事实。

这是试图解释您锁定accounts并且Vector锁定在同一个对象上。这意味着对accounts Vector进行更改的其他主题将被锁定,您将不会遇到竞争条件。

如果您没有共享此锁定,那么您将遇到竞争条件,因为synchronized阻止内部有4个操作:

  1. 获取来自帐户的当前值
  2. 使用递减值设置来自帐户
  3. 获取帐户的值
  4. 使用递增值设置帐户
  5. 由于存在锁定,我假设其他线程正在后台修改其他帐户。如果Vector假设改变了他们的内部锁定策略,那么其他线程可以在此过程中间对来自或来自帐户进行更改并搞砸会计。例如,如果from帐户在#1和#2之间递增,则由于传输,该值将被覆盖。

    依赖于像这样的类的内部锁定范例是非常糟糕的形式。这意味着如果Vector决定改变它的锁定机制(是的,我知道它不会),那么你的代码将具有竞争条件。更可能的是,另一个程序员(或将来你)决定将accounts更改为使用不同锁定机制的不同Collection,代码会中断。除非特别记录,否则不应该依赖类的内部行为。

    如果你需要防止这种竞争条件,那么你应该synchronized锁定Vector。{/ p> 不过,你不应该再使用Vector了。如果需要同步列表,请使用Collections.synchronizedList(new ArrayList<Double>);或Java 5中引入的新并发类之一。

答案 2 :(得分:1)

我没有读过这本书,但我认为他们想说的是每个Vector方法都是同步的(独立),即使这可以保护Vector免受损坏,也不会保护可以存储在其中的信息(特别是因为业务规则,数据模型,数据结构设计,或者你想要调用它们)。

示例:方法transfer是天真实现的,相信如果Vector的{​​{1}}方法同步,那么一切都很好。

set

如果2个主题(T2和T3)同时调用public void transfer(Vector<Double> accounts, int from, int to, int amount) { accounts.set(from, accounts.get(from) - amount); accounts.set(to, accounts.get(to) + amount); } ,例如同一个transfer帐户(A1),余额为1,500美元且不同from,则会发生什么情况账户(A2和A3)余额为0美元?比如说100美元到A2和1,200美元到A3?

  1. T2:检索to并减去100。结果= 1,400,但尚未存储
  2. T3:检索accounts.get('A1')(仍为1,500)并减去1,200。结果= 300
  3. T3:存储结果:accounts.get('A1')(如果执行)将产生300。
  4. T2:存储先前计算的结果。如果执行,accounts.get('A1')将产生1,400。
  5. T2:检索accounts.get('A1')(值0),添加100,然后存储回来。 accounts.get('A2')(如果已执行)将产生100。
  6. T3:检索accounts.get('A2')(值0),添加1,200并存储回来。 accounts.get('A3')(如果执行)将产生1,200。
  7. 所以,我们从accounts.get('A3') + A1 + A2 = 1,500 + 0 + 0 = 1,500开始,在完成这些内部转移后,我们有A3 + {{ 1}} + A1 = 1,400 + 100 + 1,200 = 2,700。显然有些东西在这里不起作用。什么?在步骤1和步骤4之间.T2保持A2的平衡,并且没有(不能)检测到A3也在减少它,至少在概念层面上。

    当然,每次运行都不会发生这种情况。但这是伪装的邪恶,因为如果隐藏在几十万行代码中,那么复制(以及查找和重新测试)这个问题将是困难的。

    但请注意,即使A1方法从未同时调用,上述情况也会发生。甚至不是尝试。事实上,T3并未作为accounts损坏,但它已作为我们的数据结构损坏。

    这就是短语

      

    Vector类的get和set方法是同步的,但这对我们没有帮助。

    指的是。

    要尊重提议的解决方案

      

    这种方法有效,但它完全取决于Vector类对其所有mutator方法使用内部锁的事实。

    ,作者肯定认为除了转移方法之外,accounts还有其他用途。示例:添加帐户,删除帐户等。从这个意义上说,幸运的是这些方法也在向量上同步。如果它们在内部对象中同步,则系统的开发人员需要将Vector包装在辅助同步层中,这次是基于account本身或任何其他常见对象。

    最后,关于

      

    如果线程A拥有对账户的锁定,则其他任何线程都不能获得相同的锁定。它不依赖于Vector用于其mutator方法的锁。

    这可能正是重点:所有需要互相访问数据结构的类需要就它们将要进入​​的对象达成一致accounts。这个案子非常简单。在更复杂的情况下,选择这个对象并非易事。在许多情况下,如果将一个对象特权优先于其他对象,则会创建一个特殊的“锁定”对象。但这有一个限制:整个数据结构一次只能更新一次。在这是一个问题的应用程序中,需要定义更复杂的锁定策略,并且开发人员可能难以确定哪些对象可以被锁定,以及它们应该锁定在每种可能的情况中。这些策略还需要注意死锁和竞争条件的可能性。