CoreJava 第 11 版关于客户端锁定(同步块)的线程问题
CoreJava 11th Ed Threading question on client-side locking (synchronized block)
我正在阅读以下关于为什么不推荐客户端锁定的部分:
"有时,程序员会使用对象的锁来实现额外的
原子操作——一种称为客户端锁定的做法。考虑一下,对于
例如,Vector class,它是一个列表,其方法是同步的。
现在假设我们将银行余额存储在 Vector 中。这里是
传输方法的简单实现:
public void transfer(Vector accounts, int from, int to, int amount) //
{
accounts.set(从, accounts.get(从) - 金额);
accounts.set(至, accounts.get(至) + 数量);
System.out.println(...);
}
Vectorclass的get和set方法是同步的,但是那个
对我们没有帮助。
中一个线程完全有可能被抢占
在第一次调用 get 完成后调用 transfer 方法。另一个
然后线程可以将不同的值存储到相同的位置。然而,我们
可以劫持锁:
public void transfer(Vector accounts, int from, int to, int amount)
{
同步(帐户)
{
accounts.set(从, accounts.get(从) - 金额);
accounts.set(至, accounts.get(至) + 数量);
}
System.out.println(...);
}
这种方法有效,但它完全取决于 Vector
class 对其所有修改器方法使用内在锁。然而,这是
真的是事实吗? Vector class 的文档没有这样的
承诺。你要认真研究源码,希望以后
版本不引入不同步的突变体。可以看到,client-
侧面锁定非常脆弱,一般不推荐使用。"
问题:
既然transfer方法中的synchronized(accounts)已经获取了accounts内在锁,那为什么那个依赖vectorclass的mutator方法全部使用内在锁(粗斜体高亮?
如果 accounts
Vector
的唯一突变发生在 transfer
方法中,那么 Vector
同步其突变体就没有关系了。
但是通过锁定与现有增变器方法相同的对象(即 Vector
),我们可以防止 任何 对 [=12= 的其他变更操作].
不仅仅是另一个线程完成的传输可能会破坏我们的数据,但是,例如,在我们读取余额之后和之前在 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);
}
}
返回我们数据的副本执行两个功能:
- 我们不提供对内部数据的引用,因此客户无法在不使用我们提供的 API 的情况下直接修改
ArrayList
中的值。
- 客户获得一致的账户余额快照,因此他们可以对所有账户求和并获得在他们调用
getAccountsSnapshot
[=37= 时有效的总计 ].否则,在计算总和时进行修改可能意味着他们得到了 'real life'. 中从未发生过的总数
我正在阅读以下关于为什么不推荐客户端锁定的部分:
"有时,程序员会使用对象的锁来实现额外的
原子操作——一种称为客户端锁定的做法。考虑一下,对于
例如,Vector class,它是一个列表,其方法是同步的。
现在假设我们将银行余额存储在 Vector 中。这里是
传输方法的简单实现:
public void transfer(Vector accounts, int from, int to, int amount) //
{
accounts.set(从, accounts.get(从) - 金额);
accounts.set(至, accounts.get(至) + 数量);
System.out.println(...);
}
Vectorclass的get和set方法是同步的,但是那个
对我们没有帮助。
中一个线程完全有可能被抢占
在第一次调用 get 完成后调用 transfer 方法。另一个
然后线程可以将不同的值存储到相同的位置。然而,我们
可以劫持锁:
public void transfer(Vector accounts, int from, int to, int amount)
{
同步(帐户)
{
accounts.set(从, accounts.get(从) - 金额);
accounts.set(至, accounts.get(至) + 数量);
}
System.out.println(...);
}
这种方法有效,但它完全取决于 Vector
class 对其所有修改器方法使用内在锁。然而,这是
真的是事实吗? Vector class 的文档没有这样的
承诺。你要认真研究源码,希望以后
版本不引入不同步的突变体。可以看到,client-
侧面锁定非常脆弱,一般不推荐使用。"
问题:
既然transfer方法中的synchronized(accounts)已经获取了accounts内在锁,那为什么那个依赖vectorclass的mutator方法全部使用内在锁(粗斜体高亮?
如果 accounts
Vector
的唯一突变发生在 transfer
方法中,那么 Vector
同步其突变体就没有关系了。
但是通过锁定与现有增变器方法相同的对象(即 Vector
),我们可以防止 任何 对 [=12= 的其他变更操作].
不仅仅是另一个线程完成的传输可能会破坏我们的数据,但是,例如,在我们读取余额之后和之前在 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);
}
}
返回我们数据的副本执行两个功能:
- 我们不提供对内部数据的引用,因此客户无法在不使用我们提供的 API 的情况下直接修改
ArrayList
中的值。 - 客户获得一致的账户余额快照,因此他们可以对所有账户求和并获得在他们调用
getAccountsSnapshot
[=37= 时有效的总计 ].否则,在计算总和时进行修改可能意味着他们得到了 'real life'. 中从未发生过的总数