Atomically grab multiple locks

3
假设我们需要在交易中转移两个账户(其中有数百个),并且通常会有多个类似的事务在典型的多线程环境中同时运行。按照预先设计的惯例,通常如下维护锁定顺序:
lock account A
lock account B
transfer(A,B)
release B
release A

有没有一种方法可以将锁定和释放作为原子操作尝试?

听起来对我来说像是一个 XY 问题。 - David Haim
这是经典的死锁情况,当其他人以相反的顺序锁定时,无论是原子操作还是非原子操作。 - edharned
3个回答

4

是的,这是可能的:你需要在一个锁下锁住另一个锁。换句话说,你需要创建一个锁层次结构。但是,这种解决方案效率不高,因为它降低了锁粒度。

看起来在你的情况下,始终按照相同的顺序获取锁就足够了。例如,总是先锁定ID较小的用户。


0
你可以尝试使用以下代码。 注意:它仅适用于两个锁,我不确定如何将其扩展到更多的锁。 思路是先获取第一个锁,然后尝试获取第二个锁。 如果失败,我们知道现在有1个锁是空闲的,但另一个锁正在被占用。 因此,我们释放第一个锁并反转它们,这样我们将锁定那个被占用的锁,并尝试获取那个(曾经!)空闲的锁,如果它仍然是空闲的。 反复执行此操作。 从统计学上讲,这段代码几乎不可能出现StackOverflow错误, 我认为处理它并给出错误提示比使其循环更好,因为这表明某些地方出了严重问题。
public static void takeBoth(ReentrantLock l1,ReentrantLock l2) {
    l1.lock();
    if(l2.tryLock()) {return;}
    l1.unlock();
    try{takeBoth(l2,l1);}
    catch(StackOverflowError e) {throw new Error("??");}
  }
  public static void releaseBoth(ReentrantLock l1,ReentrantLock l2){
    if(!l1.isHeldByCurrentThread()) {l1.unlock();}//this will fail: IllegarMonitorState exception
    l2.unlock();//this may fail, in that case we did not touch l1.
    l1.unlock();    
    }

0

根据ACID定义,事务是原子性的(A代表原子性)。隔离级别(至少为READ_COMMITED)保证了在同一时间可能发生在账户A上的其他交易将等待前一个已启动的交易完成。因此,实际上,您不需要显式锁定它们,因为它们将由内部实现(例如数据库)锁定,并且这些锁定将更有效,因为它们可以使用乐观锁定技术。

但是,仅当它们都参与一个事务上下文(例如在JTA环境中)时才成立。在这种环境中,您只需在转移方法开始时启动事务,无需锁定帐户A和帐户B。

如果它们不在同一个事务上下文中,则可以引入另一个锁定对象,但这将显着降低性能,因为线程将被锁定,即使一个线程正在处理帐户A和B,另一个线程正在处理帐户C和D。有一些技术可以避免这种情况(例如,查看ConcurentHashMap,其中锁定在篮子上而不是整个对象上)。

但是对于您的特定示例,答案只能是一些一般性的想法,因为示例太短而无法进行更多的检查。我认为,在特定顺序中锁定帐户A和帐户B的变体(应该非常小心 - 因为这可能导致潜在的死锁。并且假设不仅有可以使用它们的转移方法 - 这真的很高风险)对于给定的情况是正常的。


我非常确定OP使用“transaction”一词来表示转账。这个问题与数据库或分布式事务无关。 - Oleg
1
@Oleg 是的,它不是关于事务的。它是关于在某个方法上获取原子锁。我只是指出,在事务中实际上甚至不需要这样做(数据库和分布式事务只是例子,而不是答案的主题)。在非事务环境中,唯一的方法是引入另一个锁定对象,因此答案已经给出了。只是提供了可能与作者或其他人有关的额外信息。那有什么问题吗? - Izbassar Tolegen
不知道,可能什么都没有,只是感觉有些不相关。你说服我移除了踩票。 - Oleg
显然,除非您编辑答案,否则我无法这样做。 - Oleg
@Oleg 我明白这似乎与问题无关 - 下次会考虑到这一点。我的回答是因为问题中提到了交易而被触发的。(我已经编辑了我的回答 :) - Izbassar Tolegen

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