Java中有更好的解决方案替代嵌套的同步代码块吗?

9

我有一个Bank类,其中包含一系列Account。该银行有一个transfer()方法,用于将资金从一个帐户转移到另一个帐户。想法是在转账过程中锁定fromto两个帐户。

为了解决这个问题,我有以下代码(请注意,这只是一个非常简单的示例):

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

但编译器警告我关于嵌套同步块,我相信那是不好的事情? 而且,我不太喜欢max/min解决方案(那不是我想出来的),如果可能的话,我想避免使用它。

如何解决上述两个问题? 如果我们可以锁定多个对象,我们将同时锁定fromto账户,但我们不能这样做(据我所知)。 那么解决方案是什么呢?


我假设方法本身不同步的原因是因为示例过于简化了? - Dave Newton
其实我没有,我得到了帮助 :P 正如我所说的,这个想法不是来自我。 - rfgamaral
@Dave 转移方法?从未想过……也许这个例子过于简单了,我需要花点时间思考一下,但现在我得离开了。 - rfgamaral
说了这么多,在现实世界中,所有这些都会在数据库中发生,因此问题是相当无关紧要的。 - user207421
此链接讨论了类似的问题:http://download.oracle.com/javase/tutorial/essential/concurrency/newlocks.html - dmh2000
显示剩余3条评论
5个回答

5
我个人更喜欢避免非常微不足道的同步场景。在像你这样的情况下,我可能会使用一个同步队列集合来将存款和提款引导到单线程处理程序中,该处理程序操作您没有保护的变量。关于这些队列的“有趣”之处在于,当您将所有代码放入对象中并将其放入队列时,从队列中拉出对象的代码绝对是简单的和通用的(commandQueue.getNext().execute();) - 但是要执行的代码可以是任意灵活或复杂的,因为它具有整个“命令”对象来实现--这是面向对象编程擅长的模式之一。
这是一个很好的通用解决方案,可以解决许多线程问题而无需显式同步(同步仍然存在于您的队列内部,但通常最小化且无死锁,通常只需要在“put”方法上同步,而这是内部的)。
另一种解决某些线程问题的方法是确保您可能写入的每个共享变量只能由单个进程“写入”,然后通常可以完全省略同步(尽管您可能需要散布一些transients)。

他们可能应该首先教授这种技术 - 尽可能使用单一的总排序。在那个世界里,生活会简单得多。 - irreputable
尽管通常将消费者/生成者等进行分离是个好主意,但在实践中为了获得良好的扩展性,您需要多个命令队列的消费者,这又涉及到锁定问题 :-) - Voo
@Voo 我已经做到了 - 你可以为每个任务打上标记,指示它需要的不可共享/稀缺资源。当某个任务已经“消耗”(正在操作)一个不可共享资源,并且队列中的下一个任务也需要该资源时,您可以跳过该任务并从队列中获取上一个任务。然后,唯一的“同步”是,再次简单、快速且包含在队列结构中。 - Bill K
有趣的想法。假设我们很少遇到争用,这不会增加太多开销(理论上我们会得到O(N)的get()+额外的查找,但实际上应该是可以忽略的),而且似乎足够简单。我担心你只是给了我一把锤子,我必须找到一个钉子来测试这个解决方案;-) - Voo
如果出现问题(如果有许多任务可能会争夺资源),您可以将其分成多个队列,每个队列针对不同的资源(仅当每个任务只需要单个资源时才有效)。另一个可能性是从内部优化队列-但这个逻辑变得更加棘手-尽管这很有趣。 - Bill K
似乎这个解决方案会在处理其中两个账户时锁住所有账户。 - x__dos

2
锁定顺序确实是解决方案,所以你是正确的。编译器警告你是因为它不能确保所有锁定都有序——它不够聪明,无法检查你的代码,并且足够聪明,知道可能会有更多情况。
另一种解决方案可能是在封闭对象上进行锁定,例如对于一个用户账户内的转账,可以锁定该用户。但是对于用户之间的转账则不行。
话虽如此,你可能不会仅依靠Java锁定来进行转账:你需要一些数据存储,通常是数据库。如果使用数据库,则锁定移到存储中。仍然适用相同的原则:按顺序锁定以避免死锁;升级锁定以使锁定更简单。

1
我建议您研究Java中的锁对象。也可以看一下条件对象。每个账户对象都可以公开一个条件,线程在其上等待。一旦交易完成,就会调用条件对象的await或notify方法。

1

如果您还没有这样做,您可能想要查看java.util.concurrent中更高级的锁定包。

虽然您仍然需要注意避免死锁,但特别是ReadWriteLocks非常有用,可以允许多线程读取访问,同时仍然锁定对象修改。


0

使用多语言编程,在Java中使用Clojure软件事务内存,让这变得容易。

软件事务内存(STM)是一种并发控制技术,类似于数据库事务,用于控制并发计算中共享内存的访问。它是锁定同步的替代方案。

示例解决方案

Account.java

import clojure.lang.Ref;

public class Account {
    private Ref mBalance;

    public Account() {
        mBalance = new Ref(0);
    }

    public void withdraw(int value) {
        mBalance.set(getBalance() - value);
    }

    public void deposit(int value) {
        mBalance.set(getBalance() + value);
    }

    private int getBalance() {
        return (int) mBalance.deref();
    }
}

Bank.java

import clojure.lang.LockingTransaction;

import java.util.*
import java.util.concurrent.Callable;

public class Bank {
    private List<Account> mAccounts;
    private int mSlots;

    public Bank(int slots) {
        mAccounts = new ArrayList<>(Collections.nCopies(slots, new Account()));
        mSlots = slots;
    }

    public void transfer(int fromId, int toId, int value) {
        try {
            LockingTransaction.runInTransaction(
                    new Callable() {
                        @Override
                        public Object call() throws Exception {
                            mAccounts.get(fromId).withdraw(value);
                            mAccounts.get(toId).deposit(value);
                            return null;
                        }
                    });
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

依赖项

<dependency>
    <groupId>org.clojure</groupId>
    <artifactId>clojure</artifactId>
    <version>1.6.0</version>
</dependency>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接