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

时间:2015-03-26 14:20:03

标签: java multithreading

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

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 - 因此锁定共享对象效率不高。

11 个答案:

答案 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。对于每次转移,您只需锁定AB,具体取决于参与转帐的帐户。由于您使用相同类型进行传输,因此最终只需要很少的额外代码,因此维护成本较低。

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

答案 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;比系统会说请等待

供参考,您可以看到以下代码

请注意:

  1. 不要通过从LockHolder类调用freeAccount(BigDecimal accId)来从锁定地图中删除您的帐户ID

  2. 我使用了列表的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)

感谢所有人提出疑问。

我在https://www.securecoding.cert.org/confluence/display/java/LCK07-J.+Avoid+deadlock+by+requesting+and+releasing+locks+in+the+same+order

中找到了几个解决方案

在这里删除了一个链接答案,这是在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);
           }
        }
    }
}
相关问题