更好的解决方案而不是Java中的嵌套同步块?

时间:2011-10-19 23:04:02

标签: java concurrency synchronized

我有一个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);
    }
}

但编译器警告我嵌套同步块,我相信这是一件坏事吗?另外,我不太喜欢最大/最小解决方案(我不是那个提出这个想法的人),如果可能的话我想避免这种情况。

如何修复上述2个问题?如果我们可以锁定多个对象,我们会锁定fromto帐户,但我们不能这样做(据我所知)。那么解决方案是什么?

5 个答案:

答案 0 :(得分:5)

我个人更喜欢避免任何但最简单的同步方案。在像你这样的情况下,我可能会使用同步的队列集合来汇集存款并退回到一个操作未受保护的变量的单线程进程中。关于这些队列的“有趣”的事情是当你把所有代码放入你放入队列的对象中时,所以从队列中拉出对象的代码绝对是微不足道的和通用的(commandQueue.getNext()。execute();) - 正在执行的代码可以是任意灵活或复杂的,因为它有一个完整的“Command”对象用于它的实现 - 这是OO风格编程擅长的那种模式。

这是一个很棒的通用解决方案,可以在没有显式同步的情况下解决相当多的线程问题(队列中仍然存在同步,但通常是最小且无死锁的,通常只需要在“put”方法中同步所有,这是内部的。

某些线程问题的另一个解决方案是确保您可能写入的每个共享变量只能由单个进程“写入”,然后您通常可以完全取消同步(尽管您可能需要分散一些周围的瞬变)

答案 1 :(得分:2)

锁定顺序确实是解决方案,所以你是对的。编译器警告你,因为它无法确保所有你的锁被命令 - 它不够智能来检查你的代码,并且足够聪明地知道可能有更多。

另一种解决方案是锁定封​​闭物体,例如对于一个用户帐户内的转移,您可以锁定用户。用户之间的转移不是这样。

话虽如此,您可能不会依赖Java锁定来进行传输:您需要一些数据存储,通常是数据库。在使用数据库的情况下,锁定移动到存储。尽管如此,相同的原则仍然适用:您订购锁以避免死锁;升级锁以使锁定更简单。

答案 2 :(得分:1)

我建议你在java中查看Lock Objects。看看条件对象也是如此。您的每个帐户对象都可以公开线程等待的条件。一旦事务完成,就会调用条件对象等待或通知。

答案 3 :(得分:1)

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

虽然你仍然需要注意避免死锁,但ReadWriteLocks特别适用于允许多线程读访问,同时仍然锁定对象修改。

答案 4 :(得分:0)

使用Polyglot programming轻松实现此目标,Software Transactional Memory使用Clojure Java

  

软件事务内存(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>