Java同步:通过帐户对以原子方式移动资金?

gav*_*koa 43 java multithreading

如何将资金从一个账户转移到另一个账户?对于:

public class Account {
    public Account(BigDecimal initialAmount) {...}
    public BigDecimal getAmount() {...}
    public void setAmount(BigDecimal amount) {...}
}
Run Code Online (Sandbox Code Playgroud)

我希望伪代码:

public boolean transfer(Account from, Account to, BigDecimal amount) {
    BigDecimal fromValue = from.getAmount();
    if (amount.compareTo(fromValue) < 0)
         return false;
    BigDecimal toValue = to.getAmount();
    from.setAmount(fromValue.add(amount.negate()));
    to.setAmount(toValue.add(amount));
    return true;
}
Run Code Online (Sandbox Code Playgroud)

在多线程环境中安全地更新帐户,我看到危险情况如下:

acc1 --> acc2  ||  acc2 --> acc1
acc1 --> acc2  ||  acc2 --> acc3  ||  acc3 --> acc1
...
Run Code Online (Sandbox Code Playgroud)

最简单的解决方案是对共享对象进行阻止,但对于以下情况来说效率低下:

acc1 --> acc2  ||  acc3 --> acc4  and  acc1 != acc3 and acc2 != acc4
Run Code Online (Sandbox Code Playgroud)

我希望独立移动是并行执行的.

更新似乎建议的解决方案:

synchronize (acc1) {
   synchronize (acc2) {
     ....
   }
}
Run Code Online (Sandbox Code Playgroud)

导致死锁,因为顺序获得2个锁...

更新2 你对"在多线程环境中安全地更新帐户"的意思是什么?是唯一担心帐户不会最终有减去资金还是有其他问题?

如果acc1(2); acc2(3)acc1 --1--> acc2acc2 --2--> acc1我预期的一致性:(acc1, acc2)具有(3, 2)价值,而不是(4, 2)(3, 4).总数应为5,而不是1 + 3 = 4或4 + 3 = 7.

你一次期望多少并发交易?1000-10000 - 因此锁定共享对象效率不高.

Pet*_*etr 45

一个简单的解决方案可能是每个帐户使用一个锁,但为了避免死锁,您必须始终以相同的顺序获取锁.因此,您可以拥有最终帐户ID,并首先使用较少的ID获取该帐户的锁定:

public void transfer(Account acc1, Account acc2, BigDecimal value) {
    Object lock1 = acc1.ID < acc2.ID ? acc1.LOCK : acc2.LOCK;
    Object lock2 = acc1.ID < acc2.ID ? acc2.LOCK : acc1.LOCK;
    synchronized (lock1) {
       synchronized (lock2) {
          acc1.widrawal(value);
          acc2.send(value);
       }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 在帐户内部拥有锁定对象(或者,同样地,锁定帐户对象本身)意味着需要确保JVM中仅存在特定帐户的一个实例. (4认同)
  • 您可以使用帐户对象本身作为锁,而不是使用 `LOCK` 字段。 (2认同)
  • @tcooc - 这仍然只需要一个帐户对象的副本. (2认同)
  • 作为一种更通用的解决方案,您可以根据需要使用尽可能多的粒度锁,只要始终以相同的顺序获取任何和所有锁,就不必担心死锁或活锁. (2认同)

Lie*_*yan 12

一种方法是使用事务日志.在转移资金之前,您需要写入您想要做的每个帐户的交易日志.日志应包含:从帐户中取出/取出的金额,以及在日志对之间共享的锁定.

最初锁定应处于阻塞状态.您创建了一个日志对,一个数量为X,另一个数量为-X,两者共享一个锁.然后将日志条目发送到相应帐户的收件箱,从中取出资金的帐户应保留该金额.一旦您确认他们已安全送达,然后释放锁.锁定被释放的那一刻,如果没有回复,你就处于某一点.然后帐户应该自行解决.

如果任何一方想要在锁定释放之前的任何时间使事务失败,那么只需删除日志并将保留金额返回到主余额.

这种方法可能有点沉重,但它也可以在分布式场景中工作,其中帐户实际上在不同的机器中,并且实际上必须保留收件箱,以确保如果任何机器崩溃/去,钱永远不会丢失意外离线.其一般技术称为两相锁定.


Chr*_*ian 8

我建议创建一个方法Account.withdraw(amount),如果它没有足够的资金,它会抛出异常.此方法需要在帐户本身上同步.

编辑:

还需要一个在接收帐户实例上同步的Account.deposit(amount)方法.

基本上这将导致在撤销时锁定第一个帐户,然后在存款时锁定接收帐户.所以两个锁但不是同时.

代码示例:假设撤销/存储是同步的并返回布尔成功状态而不是抛出异常.

public boolean transfer(Account from, Account to, BigDecimal amount) {
    boolean success = false;
    boolean withdrawn = false;
    try {
        if (from.withdraw(amount)) {
            withdrawn = true;
            if (to.deposit(amount)) {
                success = true;
            }
        }
    } finally {
        if (withdrawn && !success) {
            from.deposit(amount);
        }
    }

    return success;
}
Run Code Online (Sandbox Code Playgroud)


Spa*_*ker 7

您可以创建一个Account T仅用于转移资金的额外存款.因此,如果你想转移AB你实际转移AT然后转移TB.对于每个转移,您只能锁定AB取决于参与转移的帐户.由于您使用相同类型进行传输,因此最终只需要很少的额外代码,因此维护成本较低.

要减少额外帐户的数量,您可以将它们保存在池中.如果您有一个正在处理传输的线程池,那么您可以为每个线程分配它自己的额外帐户.因此,您不需要经常从池中请求和释放这些额外的帐户.

  • @gavenkoa不完全是因为Petr没有使用额外的帐户,我的解决方案不需要帐户"A"和"B"之间的特定锁定顺序. (2认同)

Vic*_*kin 6

一种方法是使用一种"条带锁",其中锁定/解锁方法在几个锁上运行.帐户映射到锁使用hashCode,您分配的锁越多,您获得的并行性就越多.

这是代码示例:

public class StripedLock {

    private final NumberedLock[] locks;

    private static class NumberedLock {
        private final int id;
        private final ReentrantLock lock;

        public NumberedLock(int id) {
            this.id = id;
            this.lock = new ReentrantLock();
        }
    }


    /**
     * Default ctor, creates 16 locks
     */
    public StripedLock() {
        this(4);
    }

    /**
     * Creates array of locks, size of array may be any from set {2, 4, 8, 16, 32, 64}
     * @param storagePower size of array will be equal to <code>Math.pow(2, storagePower)</code>
     */
    public StripedLock(int storagePower) {
        if (!(storagePower >= 1 && storagePower <= 6)) { throw new IllegalArgumentException("storage power must be in [1..6]"); }

        int lockSize = (int) Math.pow(2, storagePower);
        locks = new NumberedLock[lockSize];
        for (int i = 0; i < locks.length; i++)
            locks[i] = new NumberedLock(i);
    }

    /**
     * Map function between integer and lock from locks array
     * @param id argument
     * @return lock which is result of function
     */
    private NumberedLock getLock(int id) {
        return locks[id & (locks.length - 1)];
    }

    private static final Comparator<? super NumberedLock> CONSISTENT_COMPARATOR = new Comparator<NumberedLock>() {
        @Override
        public int compare(NumberedLock o1, NumberedLock o2) {
            return o1.id - o2.id;
        }
    };


    public void lockIds(@Nonnull int[] ids) {
        Preconditions.checkNotNull(ids);
        NumberedLock[] neededLocks = getOrderedLocks(ids);
        for (NumberedLock nl : neededLocks)
            nl.lock.lock();
    }

    public void unlockIds(@Nonnull int[] ids) {
        Preconditions.checkNotNull(ids);
        NumberedLock[] neededLocks = getOrderedLocks(ids);
        for (NumberedLock nl : neededLocks)
            nl.lock.unlock();
    }

    private NumberedLock[] getOrderedLocks(int[] ids) {
        NumberedLock[] neededLocks = new NumberedLock[ids.length];
        for (int i = 0; i < ids.length; i++) {
            neededLocks[i] = getLock(i);
        }
        Arrays.sort(neededLocks, CONSISTENT_COMPARATOR);
        return neededLocks;
    }
}

    // ...
    public void transfer(StripedLock lock, Account from, Account to) {
        int[] accountIds = new int[]{from.getId(), to.getId()};
        lock.lockIds(accountIds);
        try {
            // profit!
        } finally {
            lock.unlockIds(accountIds);
        }
    }
Run Code Online (Sandbox Code Playgroud)


Jes*_*num 5

不要使用内置同步,使用 Lock 对象。使用 tryLock() 同时获取两个账户的排他锁。如果其中一个失败,则释放两个锁并等待随机时间,然后重试。


Leo*_*aga 5

如前所述,您应该锁定两个帐户,始终以相同的顺序锁定。然而,关键部分是确保整个 VM 实例的高粒度和单一性。这可以使用String.intern()

public boolean transfer(Account from, Account to, BigDecimal amount) {
    String fromAccountId = from.id.toString().intern();
    String toAccountId = to.id.toString().intern();
    String lock1, lock2;

    if (from.id < to.id) {
       lock1 = fromAccountId;
       lock2 = toAccountId;
    } else {
       lock1 = toAccountId;
       lock2 = fromAccountId;
    }

    // synchronizing from this point, since balances are checked
    synchronized(lock1) {
        synchronized(lock2) {
            BigDecimal fromValue = from.getAmount();
            if (amount.compareTo(fromValue) < 0)
                 return false;

            BigDecimal toValue = to.getAmount();
            from.setAmount(fromValue.add(amount.negate()));
            to.setAmount(toValue.add(amount));
            return true;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)