4

请解释以下有关来自康奈尔 Hortsmann 的“Core Java”同步块的陈述(第 9 版,第 865 页):

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 个不同的锁吗?)。

这个解释对我来说似乎不合理,但我没有其他任何东西。我错过了什么?

4

3 回答 3

6

如果线程 A 拥有帐户锁,则没有其他线程可以获取相同的锁。它不依赖于 Vector 用于其 mutator 方法的锁。

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

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

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

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

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

让线程 A 拥有帐户锁定。如果 Vector 为其 set/get 使用另一个锁,则线程 A 必须获得一个额外的锁才能继续执行 set/get,这由于某种原因是不可能的(一个线程可以同时持有 2 个不同的锁吗?)。

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

于 2013-08-23T00:02:44.210 回答
2

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

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

synchronized如果您没有共享此锁,那么您将遇到竞争条件,因为该块内正在进行 4 个操作:

  1. 获取 from 账户的当前值
  2. 使用递减的值设置来自帐户
  3. 获取账户的价值
  4. 使用增量值设置帐户

由于有锁,我假设其他线程正在后台修改其他帐户。如果Vector假设更改了它们的内部锁定策略,其他线程可能会在此过程的中间更改 from 或 to 帐户并搞砸会计。例如,如果 from 帐户在 #1 和 #2 之间增加,则该值将因转账而被覆盖。

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

如果您需要防止这种竞争条件,那么您应该synchronized在访问Vector.

顺便说一句,你不应该再使用Vector了。如果您需要同步列表,请使用Collections.synchronizedList(new ArrayList<Double>);Java 5 中引入的新并发类之一。

于 2013-08-23T00:04:32.220 回答
1

我没有读过这本书,但我认为他们想说的是每个Vector方法都是同步的(独立的),即使这样可以防止Vector损坏,它也不能保护它可以存储在其中的信息(尤其是因为业务规则、数据模型、数据结构设计,或者你想怎么称呼它们)。

示例:方法transfer的实现很天真,相信如果它Vectorset方法是同步的,那么一切都很好。

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

如果 2 个线程(T2 和 T3)同时调用transfer相同的from帐户(A1),余额为 1,500 美元,不同的to帐户(A2 和 A3),余额为 0 美元,会发生什么情况?对 A2 说 100 美元,对 A3 说 1,200 美元?

  1. T2: accounts.get('A1')检索并减去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('A2')被检索(值 0),添加 100,然后存储回来。 accounts.get('A2')(如果执行)将产生 100。
  6. T3: accounts.get('A3')检索(值 0),添加 1,200,然后存储回来。 accounts.get('A3')(如果执行)将产生 1,200。

所以,我们从A1+ A2+ A3= 1,500 + 0 + 0 = 1,500 开始,在进行这些内部传输之后,我们有A1+ A2+ A3= 1,400 + 100 + 1,200 = 2,700。显然有些东西在这里不起作用。什么?在步骤 1 和 4 之间。T2 保持A1平衡,并且没有(无法)检测到这T3也在减少它,至少在概念上是这样。

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

但是,请注意,即使accounts从未同时调用方法,也会发生上述情况。甚至没有尝试。事实上,accounts它并没有被破坏为Vector,而是被破坏为我们的数据结构。

这就是这句话

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

指。

说尊重提出的解决方案

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

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

最后,关于

如果线程 A 拥有帐户锁,则没有其他线程可以获取相同的锁。它不依赖于 Vector 用于其 mutator 方法的锁。

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

于 2013-08-23T00:08:00.343 回答