【问题标题】:Better solution instead of nested synchronized blocks in Java?更好的解决方案,而不是 Java 中的嵌套同步块?
【发布时间】:2026-02-06 10:50:01
【问题描述】:

我有一个带有Account 列表的Bank 类。银行有一个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);
    }
}

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

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

【问题讨论】:

  • 你肯定知道你在做什么。我会忽略编译器。
  • 大声笑,我只是在学习进程并发......我不会称之为“知道我在做什么”,我只是了解这个基本示例的工作原理。不过谢谢。
  • 完全没有,你很好地识别并解决了死锁问题。
  • 我假设方法本身不同步的原因是因为示例过于简化?
  • 其实我没有,我得到了帮助 :P 正如我所说,这个想法不是来自我。

标签: java concurrency synchronized


【解决方案1】:

使用Polyglot programming 使这变得简单,使用Software Transactional MemoryClojure 但在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();
    }
}

银行.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>

【讨论】:

    【解决方案2】:

    我个人更愿意避免任何最琐碎的同步场景。在像您这样的情况下,我可能会使用同步队列集合将存款和取款汇集到一个单线程进程中,该进程操纵您的未受保护的变量。这些队列的“有趣”之处在于,当您将所有代码放入放入队列的对象中时,从队列中拉出对象的代码绝对是微不足道和通用的(commandQueue.getNext().execute();) ——然而,正在执行的代码可以任意灵活或复杂,因为它有一个完整的“命令”对象来实现——这是面向对象编程擅长的那种模式。

    这是一个很好的通用解决方案,无需显式同步即可解决相当多的线程问题(同步仍然存在于队列中,但通常很少且无死锁,通常只有“put”方法需要在全部,这是内部的)。

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

    【讨论】:

    • 他们可能应该首先教授这项技术——只要有可能,就有一个单一的总订单。在那个世界里,生活要简单得多。
    • 虽然拆分消费者/生产者等通常是个好主意,但实际上要获得良好的扩展性,您需要 commandQueue 的多个消费者,我们再次使用锁定 :-)跨度>
    • @Voo 我已经这样做了——你可以做的是用一些东西标记每个任务,表明它想要的不可共享/稀缺资源,当某些东西已经“消耗”(正在运行)一个不可共享的资源和队列中的下一个资源需要相同的资源,您跳过它并从队列中获取下一个资源。然后,唯一的“同步”再次简单、快速并包含在队列结构中。
    • 有趣的想法。假设我们几乎没有争用,这不会增加太多开销(理论上我们得到 O(N) 用于 get() + 额外的查找,但在实践中应该可以忽略不计)并且看起来很简单。我担心你只是给了我一把锤子,我将不得不找到一个钉子来在实践中测试该解决方案;-)
    • 如果它成为一个问题(如果许多任务可能竞争),您可以将它分成多个队列,每个队列用于不同的资源(只有在每个任务只需要一个资源时才有效)。另一种可能性是从内部优化队列——但这种逻辑会变得更棘手——尽管搞砸会很有趣。
    【解决方案3】:

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

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

    【讨论】:

      【解决方案4】:

      我建议您研究一下 Java 中的锁定对象。也看看条件对象。您的每个帐户对象都可以公开线程等待的条件。事务完成后,将调用条件对象 await 或 notify。

      【讨论】:

        【解决方案5】:

        锁定排序确实是解决方案,所以你是对的。编译器会警告您,因为它无法确保所有您的锁定是有序的——它不够聪明,无法检查您的代码,而且聪明到知道可能还有更多。

        另一种解决方案可能是锁定封闭对象,例如对于一个用户帐户内的转账,您可以锁定用户。用户之间的转移并非如此。

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

        【讨论】: