更好的解决方案而不是 Java 中的嵌套同步块?
我有一个包含 Account
列表的 Bank
类。银行有一个 transfer()
方法将金额从一个帐户转移到另一个帐户。这个想法是在转账中锁定from
和to
帐户。
为了解决这个问题,我有以下代码(请记住,这是一个非常简单的示例,因为它只是一个示例):
public class Account {
private int mBalance;
public Account() {
mBalance = 0;
}
public void withdraw(int value) {
mBalance -= value;
}
public void deposit(int value) {
mBalance += value;
}
}
public class Bank {
private List<Account> mAccounts;
private int mSlots;
public Bank(int slots) {
mAccounts = new ArrayList<Account>(Collections.nCopies(slots, new Account()));
mSlots = slots;
}
public void transfer(int fromId, int toId, int value) {
synchronized(mAccounts.get(fromId, toId)) {
synchronized(mAccounts.get(toId)) {
mAccounts.get(fromId).withdraw(value);
mAccounts.get(toId).deposit(value);
}
}
}
}
这可以工作,但不能防止死锁。为了解决这个问题,我们需要将同步更改为以下内容:
synchronized(mAccounts.get(Math.min(fromId, toId))) {
synchronized(mAccounts.get(Math.max(fromId, toId))) {
mAccounts.get(fromId).withdraw(value);
mAccounts.get(toId).deposit(value);
}
}
但是编译器警告我有关嵌套同步块的信息,我相信这是一件坏事?另外,我不太喜欢最大/最小解决方案(我不是提出这个想法的人),如果可能的话,我想避免这种情况。
如何解决上述两个问题?如果我们可以锁定多个对象,我们就会锁定 from
和 to
帐户,但我们不能这样做(据我所知)。那有什么办法解决呢?
I have a Bank
class with a list of Account
. The bank has a transfer()
method to transfer a value from one account to another. The idea is to lock both the from
and to
accounts within a transfer.
To solve this issue I have the following code (please bear in mind that this is a very trivial example because it's just that, an example):
public class Account {
private int mBalance;
public Account() {
mBalance = 0;
}
public void withdraw(int value) {
mBalance -= value;
}
public void deposit(int value) {
mBalance += value;
}
}
public class Bank {
private List<Account> mAccounts;
private int mSlots;
public Bank(int slots) {
mAccounts = new ArrayList<Account>(Collections.nCopies(slots, new Account()));
mSlots = slots;
}
public void transfer(int fromId, int toId, int value) {
synchronized(mAccounts.get(fromId, toId)) {
synchronized(mAccounts.get(toId)) {
mAccounts.get(fromId).withdraw(value);
mAccounts.get(toId).deposit(value);
}
}
}
}
This works, but does not prevent deadlocks. To fix that, we need to change the synchronization to the following:
synchronized(mAccounts.get(Math.min(fromId, toId))) {
synchronized(mAccounts.get(Math.max(fromId, toId))) {
mAccounts.get(fromId).withdraw(value);
mAccounts.get(toId).deposit(value);
}
}
But the compiler warns me about nested synchronization blocks and I trust that that is a bad thing to do? Also, I'm not very fond of the max/min solution (I was not the one who came up with that idea) and I would like to avoid that if possible.
How would one fix those 2 problems above? If we could lock on more than one object, we would lock both the from
and to
account, but we can't do that (as far as I know). What's the solution then?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
我个人更喜欢避免除了最琐碎的同步场景之外的任何情况。在像您这样的情况下,我可能会使用同步队列集合将存款和取款汇集到一个单线程进程中,该进程操作不受保护的变量。这些队列的“有趣”之处在于,当您将所有代码放入放入队列的对象中时,因此从队列中提取对象的代码绝对是微不足道且通用的(commandQueue.getNext().execute();) ——然而,正在执行的代码可以任意灵活或复杂,因为它有一个完整的“Command”对象来实现——这是 OO 风格编程所擅长的模式。
这是一个很棒的通用解决方案,可以解决相当多的线程问题,而无需显式同步(同步仍然存在于队列中,但通常是最小的并且无死锁,通常只需要同步“put”方法,并且这是内部的)。
某些线程问题的另一个解决方案是确保您可能写入的每个共享变量只能由单个进程“写入”,然后您通常可以完全放弃同步(尽管您可能需要分散一些瞬态)
I personally prefer to avoid any but the most trivial synchronization scenario. In a case like yours I would probably use a synchronized queue collection to funnel deposits and withdraws into a single-threaded process that manipulates your unprotected variable. The "Fun" thing about these queues is when you put all the code into the object that you drop into the queue so the code pulling the object from the queue is absolutely trivial and generic (commandQueue.getNext().execute();)--yet the code being executed can be arbitrarily flexible or complex because it has an entire "Command" object for it's implementation--this is the kind of pattern that OO-style programming excels at.
This is a great general-purpose solution and can solve quite a few threading problems without explicit synchronization (synchronization still exists inside your queue but is usually minimal and deadlock-free, often only the "put" method needs to be synchronized at all, and that's internal).
Another solution to some threading problems is to ensure that every shared variable you might possibly write to can only be "Written" to by a single process, then you can generally leave off synchronization altogether (although you may need to scatter a few transients around)
锁定顺序确实是解决方案,所以你是对的。编译器会向您发出警告,因为它无法确保您的所有锁定都是有序的 - 它不够聪明,无法检查您的代码,也不够聪明,无法知道可能还有更多锁定。
另一种解决方案可以是锁定封闭对象,例如,对于一个用户帐户内的转账,您可以锁定用户。用户之间的传输并非如此。
话虽如此,您可能不会依赖 Java 锁定来进行传输:您需要一些数据存储,通常是数据库。如果使用数据库,锁定将转移到存储。尽管如此,同样的原则仍然适用:您订购锁以避免死锁;您升级锁以使锁定更简单。
Lock ordering is indeed the solution, so you're right. The compiler warns you because it cannot make sure all your locking is ordered—it's not smart enough to check your code, and smart enough to know there may be more.
An alternative solution could be locking on an enclosing object, e.g. for transfers within one user's account you could lock on user. Not so with transfers between users.
Having said that, you are not probably going to rely on Java locking in order to make a transfer: you need some data storage, usually a database. In case of using a database, the locking moves to the storage. Still, the same principles apply: you order locks to avoid deadlocks; you escalate locks to make locking simpler.
我建议你研究一下java中的锁定对象。也看看条件对象。每个帐户对象都可以公开线程等待的条件。一旦事务完成,条件对象await或notify就会被调用。
I would advise you to look into Lock Objects in java. Have a look at condition objects too. Each of your account object can expose a condition on which a thread waits. Once a transaction is complete, condition objects await or notify is called.
如果您还没有,您可能想查看 java.util.concurrent。
虽然您仍然需要注意避免死锁,但 ReadWriteLock 特别适用于允许多线程读取访问,同时仍锁定对象修改。
If you haven't already you may want to look at the more advanced locking packages in java.util.concurrent.
While you still have to take care to avoid with deadlock, the ReadWriteLocks in particular are useful to allow multi-thread read access while still locking for object modification.
通过多语言编程让这一切变得简单,使用软件事务内存 与 Clojure 但在Java。
示例解决方案
Account.java
Bank.java
依赖项
Make this easy with Polyglot programming, use Software Transactional Memory with Clojure but in Java.
Example solution
Account.java
Bank.java
Dependencies