如何将资金从一个账户转移到另一个账户?为:
public class Account {
public Account(BigDecimal initialAmount) {...}
public BigDecimal getAmount() {...}
public void setAmount(BigDecimal amount) {...}
}
我希望伪代码:
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;
}
在多线程环境中安全地更新帐户,我将危险情况视为:
acc1 --> acc2 || acc2 --> acc1
acc1 --> acc2 || acc2 --> acc3 || acc3 --> acc1
...
最简单的解决方案是在共享对象上进行阻止,但对于以下情况来说效率很低:
acc1 --> acc2 || acc3 --> acc4 and acc1 != acc3 and acc2 != acc4
我希望独立移动是并行进行的。
更新似乎建议的解决方案:
synchronize (acc1) {
synchronize (acc2) {
....
}
}
导致死锁,因为顺序获得2个锁...
更新2 您对“在多线程环境中安全地更新帐户”的意思是什么?是唯一担心账户不会最终有负资金还是还有其他问题?
如果acc1(2); acc2(3)
和acc1 --1--> acc2
以及acc2 --2--> acc1
我希望保持一致:(acc1, acc2)
的值为(3, 2)
,但不是(4, 2)
或(3, 4)
}。总数应为5,而不是1 + 3 = 4或4 + 3 = 7.
您一次预期会有多少并发事务? 1000-10000 - 因此锁定共享对象效率不高。
答案 0 :(得分:44)
一个简单的解决方案可能是对每个帐户使用锁定,但为了避免死锁,您必须始终以相同的顺序获取锁定。因此,您可以拥有最终帐户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);
}
}
}
答案 1 :(得分:12)
执行此操作的一种方法是拥有事务日志。在转移资金之前,您需要写入您想要做的每个帐户的交易日志。日志应包含:从帐户中取出的金额,以及在日志对之间共享的锁。
最初锁定应处于阻塞状态。您创建了一个日志对,一个数量为X,另一个数量为-X,两者共享一个锁。然后将日志条目发送到相应帐户的收件箱,从中取出资金的帐户应保留该金额。一旦您确认他们已安全送达,然后释放锁。锁定被释放的那一刻,如果没有回复,你就处于某一点。然后帐户应该自行解决。
如果任何一方想要在锁定被释放之前的任何时间使交易失败,那么只需删除日志并将预留金额返回到主余额。
这种方法可能有点沉重,但它也可以在分布式场景中工作,其中帐户实际上在不同的机器中,并且实际上必须保留收件箱,以确保如果任何机器的钱永远不会丢失崩溃/意外脱机。它的一般技术称为两相锁定。
答案 2 :(得分:8)
我建议创建一个方法Account.withdraw(amount),如果它没有足够的资金,则抛出异常。需要在帐户本身上同步此方法。
编辑:
还需要一个在接收帐户实例上同步的Account.deposit(金额)方法。
基本上,这将导致第一个帐户锁定,同时撤销,然后在存款时锁定接收帐户。所以两个锁,但不是同时。
代码示例:假设撤消/存储已同步并返回布尔成功状态而不是抛出异常。
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;
}
答案 3 :(得分:7)
您可以创建一个仅用于转移资金的额外Account
T
。因此,如果您想从A
转移到B
,实际上是从A
转移到T
,然后从T
转移到B
。对于每次转移,您只需锁定A
或B
,具体取决于参与转帐的帐户。由于您使用相同类型进行传输,因此最终只需要很少的额外代码,因此维护成本较低。
减少可以在池中保留的额外帐户数量。如果您有一个正在处理传输的线程池,那么您可以为每个线程分配它自己的额外帐户。因此,您不需要经常从池中请求和释放这些额外的帐户。
答案 4 :(得分: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);
}
}
答案 5 :(得分:4)
不要使用内置同步,请使用Lock对象。使用tryLock()同时获取两个帐户的独占锁定。如果其中任何一个失败,则释放两个锁并等待一段随机时间再试一次。
答案 6 :(得分:4)
正如您所提到的,您一次预期会有1000-10000个并发事务,而不是存储某些事务正在进行并处理并发的帐户
一个解决方案是允许系统只创建一个具有微粒账户ID的对象,这意味着如果您想在账户之间进行交易&#34; 123&#34;和&#34; 456&#34;你的线程将创建帐户对象,并且在帐户类的构造函数中,我们将检查是否有任何其他帐户对象具有微粒帐户ID,如果其他帐户对象具有相同的帐户ID则意味着某些事务正在进行微粒化帐户ID,因此您必须等待获取帐户对象。
所以我们可以在&#34; 123&#34;之间进行交易。和&#34; 456&#34;同时我们可以在&#34; abc&#34;之间进行交易。和&#34; xyz&#34;但如果同时某个其他线程会尝试创建帐户对象&#34; 123&#34;比系统会说请等待
供参考,您可以看到以下代码
请注意:
不要通过从LockHolder类调用freeAccount(BigDecimal accId)来从锁定地图中删除您的帐户ID
我使用了列表的HasMap instand,因为当你从中随机删除元素时(或经常更新它时)列表不是一个好的选择
package test;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
public class T {
public static void main(String[] args) {
Account ac, ac2;
try {
ac = new Account(new BigDecimal("123"));
} catch (Exception e) {
e.printStackTrace();
}
try {
ac2 = new Account(new BigDecimal("123"));
} catch (Exception e) {
System.out.println("Please Wait");
}
}
}
class Account {
public Account(BigDecimal accId) throws Exception {
if (LockHolder.isLocked(accId)) {
throw new Exception();
} else {
LockHolder.setLock(accId);
}
}
}
class LockHolder {
public static Map<BigDecimal, Integer> locks = new HashMap<BigDecimal, Integer>();
public synchronized static boolean isLocked(BigDecimal accId) {
return LockHolder.locks.containsKey(accId);
}
public synchronized static void setLock(BigDecimal accId) {
LockHolder.locks.put(accId , 1);
}
public synchronized static void freeAccount(BigDecimal accId) {
LockHolder.locks.remove(accId);
}
}
答案 7 :(得分:4)
如前所述,您应该锁定两个帐户,始终以相同的顺序。然而,关键部分是确保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;
}
}
}
答案 8 :(得分:2)
即使线程可能被任意镶嵌,一种保持强大的方法是让每个帐户维护一个请求或发布的事务列表。要请求从一个帐户转移到另一个帐户,请创建一个定义请求的事务对象,并将其添加到源帐户的请求队列中。如果该帐户可以执行该事务,则应将其移至其已发布事务列表并将其添加到目标的请求队列中。使用AtomicReference
可以确保从事务被放入第一个帐户的队列的那一刻起,系统的状态将始终使事务处于暂挂,完成或中止,即使某些或所有的线程都得到了解决方案,检查交易清单可以确定哪些钱属于哪里。
相比之下,当使用锁时,意外延迟一个线程的事件可以任意阻碍许多其他线程的执行,如果一个线程在持有锁时被杀死,则可能无法确定它究竟具有什么或没有完成在那之前。
答案 9 :(得分:0)
感谢所有人提出疑问。
中找到了几个解决方案在这里删除了一个链接答案,这是在cert.org失败时帮助任何人的必备代码。件很长,所以我没有任何优点/缺点。
私有静态最终锁定对象:
final class BankAccount {
private double balanceAmount; // Total amount in bank account
private static final Object lock = new Object();
BankAccount(double balance) {
this.balanceAmount = balance;
}
// Deposits the amount from this object instance
// to BankAccount instance argument ba
private void depositAmount(BankAccount ba, double amount) {
synchronized (lock) {
if (amount > balanceAmount) {
throw new IllegalArgumentException(
"Transfer cannot be completed");
}
ba.balanceAmount += amount;
this.balanceAmount -= amount;
}
}
public static void initiateTransfer(final BankAccount first,
final BankAccount second, final double amount) {
Thread transfer = new Thread(new Runnable() {
@Override public void run() {
first.depositAmount(second, amount);
}
});
transfer.start();
}
}
有序锁定:
final class BankAccount implements Comparable<BankAccount> {
private double balanceAmount; // Total amount in bank account
private final Object lock;
private final long id; // Unique for each BankAccount
private static long NextID = 0; // Next unused ID
BankAccount(double balance) {
this.balanceAmount = balance;
this.lock = new Object();
this.id = this.NextID++;
}
@Override public int compareTo(BankAccount ba) {
return (this.id > ba.id) ? 1 : (this.id < ba.id) ? -1 : 0;
}
// Deposits the amount from this object instance
// to BankAccount instance argument ba
public void depositAmount(BankAccount ba, double amount) {
BankAccount former, latter;
if (compareTo(ba) < 0) {
former = this;
latter = ba;
} else {
former = ba;
latter = this;
}
synchronized (former) {
synchronized (latter) {
if (amount > balanceAmount) {
throw new IllegalArgumentException(
"Transfer cannot be completed");
}
ba.balanceAmount += amount;
this.balanceAmount -= amount;
}
}
}
public static void initiateTransfer(final BankAccount first,
final BankAccount second, final double amount) {
Thread transfer = new Thread(new Runnable() {
@Override public void run() {
first.depositAmount(second, amount);
}
});
transfer.start();
}
}
合规解决方案(ReentrantLock):
final class BankAccount {
private double balanceAmount; // Total amount in bank account
private final Lock lock = new ReentrantLock();
private final Random number = new Random(123L);
BankAccount(double balance) {
this.balanceAmount = balance;
}
// Deposits amount from this object instance
// to BankAccount instance argument ba
private void depositAmount(BankAccount ba, double amount)
throws InterruptedException {
while (true) {
if (this.lock.tryLock()) {
try {
if (ba.lock.tryLock()) {
try {
if (amount > balanceAmount) {
throw new IllegalArgumentException(
"Transfer cannot be completed");
}
ba.balanceAmount += amount;
this.balanceAmount -= amount;
break;
} finally {
ba.lock.unlock();
}
}
} finally {
this.lock.unlock();
}
}
int n = number.nextInt(1000);
int TIME = 1000 + n; // 1 second + random delay to prevent livelock
Thread.sleep(TIME);
}
}
public static void initiateTransfer(final BankAccount first,
final BankAccount second, final double amount) {
Thread transfer = new Thread(new Runnable() {
public void run() {
try {
first.depositAmount(second, amount);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Reset interrupted status
}
}
});
transfer.start();
}
}
答案 10 :(得分:0)
可能只是重新排序锁?
请批评该解决方案是否不好,为什么?
public void transfer(Account acc1, Account acc2, BigDecimal value) {
synchronized (acc1) {
acc1.widrawal(value);
}
boolean success = false;
try {
synchronized (acc2) {
acc2.send(value);
success = true;
}
} finally {
synchronized (acc1) {
if (!success) {
// revert transaction back if we had any exceptions,
// Thread interruptions and so one
acc1.send(value);
}
}
}
}