2013-08-22 38 views
3

请解释下面的语句有关Hortsmann,康奈从“核心Java”的同步块(9版,p.865。):为什么不推荐客户端锁定?

GET和Vector类的设置方法是同步的,但没有按帮不了我们。
...
但是,我们可以劫持锁:

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用于其增变器方法的锁。

我能想到的唯一可能的解释是以下一种。让线程A拥有锁定帐户。如果Vector为它的set/get使用另一个锁,那么线程A必须获得一个额外的锁来继续set/get,并且由于某种原因这是不可能的(线程可以同时持有2个不同的锁吗?)。

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

回答

5

如果线程A拥有账号上的锁定,则其他线程无法获得相同的锁定。它不依赖于Vector用于其增变器方法的锁。

但是,如果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只使用内置的Vector锁时甚至不会尝试锁定它。

2

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

这是试图解释你锁定在accountsVector锁定在同一个对象上。这意味着其他线程将更改为accountsVector将被锁定,您将不会有竞争状态。

如果你不同意这个锁,那么你将有一个竞争条件,因为有4个操作是​​块里想的是:

  1. 从帐户获取的当前值
  2. 设置从帐户与递减值
  3. 获取的价值占
  4. 设置考虑到具有增加的值

由于存在锁定,我假设其他线程正在修改后台中的其他帐户。如果Vector假设性地改变了他们的内部锁定策略,其他线程可以在这个过程的中间对来自或去往账户进行更改,并搞砸会计。例如,如果发件人账号在#1和#2之间递增,那么由于转账,该值将被覆盖。

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

如果您需要防止出现这种竞争状况,那么您应该执行​​左右的锁定,以便访问Vector

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

+0

“如果向量决定改变它的锁定机制”虽然这是不可能的。它如何同步显然是API的一部分,他们不会打破这一点。 – Thilo

+0

在Vector的Javadoc中,它提到了你可以依赖所有被同步的方法。如果将'Vector'切换到使用内部的'private final Object lock',它们不会相同吗?我知道他们不会改变它,但这是我反对的模式。 @Thilo。 – Gray

+1

@Thilo实际上,他们在书中进一步提到Vector并未做出这样的承诺。 –

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?说出100美元到A2和1200美元到A3?

  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')(如果执行)将产生1200。

于是,我们开始A1 + A2 + A3 = 1500 + 0 + 0 = 1500,而做这些内部转账后,我们有A1 + A2 + A3 = 1400 + 100 + 1200 = 2,700元。显然有些东西在这里不起作用。什么?在步骤1和步骤4之间.T2保持A1的平衡,并且没有(不能)检测到T3也在减少它,至少在概念层面。

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

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

这就是那句

get和Vector类的设置方法是同步的,但这并不能帮助我们。

指的是。

如果说对于提出的解决方案是

这种方法的工作原理,但它是完全依赖于Vector类使用其所有mutator方法的内部锁的事实。

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

最后,关于

如果一个线程A拥有的账户锁定,没有其他线程可以获取同样的锁。它不依赖于Vector用于其增变器方法的锁。

这可能恰恰是这样一个观点:所有需要对数据结构进行互斥访问的类需要就他们将要访问的对象达成一致synchronize。这个案例非常简单。在更复杂的情况下,选择这个对象不是微不足道的。在许多情况下,相反,如果将某个对象放在其他对象上,则会创建一个特殊的“锁定”对象。但是这有一个限制:整个数据结构一次只能更新一次。在出现这种问题的应用程序中,需要定义更详细的锁定策略,开发人员可能难以确定哪些对象可能被锁定,以及哪些对象应锁定在每种可能的情况下。这些策略还需要关注死锁和竞态条件的可能性。

相关问题