我正在尝试实施一个银行系统,我有一组帐户。有多个试图在账户之间转账,而一个线程连续(或者更确切地说,在随机时间)试图总结银行的总金额(所有账户余额的总和)。
解决这个问题的方法起初听起来很明显;对于执行事务的线程使用ReentrantReadWriteLocks
和readLock
,对执行求和的线程使用writeLock
。然而,在以这种方式实现之后(参见下面的代码),我看到性能/“事务吞吐量”大幅下降,甚至只与一个线程进行交易。
上述实施的代码:
public class Account implements Compareable<Account>{
private int id;
private int balance;
public Account(int id){
this.id = id;
this.balance = 0;
}
public synchronized int getBalance(){ return balance; }
public synchronized setBalance(int balance){
if(balance < 0){ throw new IllegalArgumentException("Negative balance"); }
this.balance = balance;
}
public int getId(){ return this.id; }
// To sort a collection of Accounts.
public int compareTo(Account other){
return (id < other.getId() ? -1 : (id == other.getId() ? 0 : 1));
}
}
public class BankingSystem {
protected List<Account> accounts;
protected ReadWriteLock lock = new ReentrantReadWriteLock(); // !!
public boolean transfer(Account from, Account to, int amount){
if(from.getId() != to.getId()){
synchronized(from){
if(from.getBalance() < amount) return false;
lock.readLock().lock(); // !!
from.setBalance(from.getBalance() - amount);
}
synchronized(to){
to.setBalance(to.getBalance() + amount);
lock.readLock().unlock(); // !!
}
}
return true;
}
// Rest of class..
}
请注意,这甚至没有使用求和方法,因此不会获取writeLock
。如果我只删除标有// !!
的行并且也不调用求和方法,那么突然使用多个线程的“传输吞吐量”比使用单个线程要高很多,这是目标。
我现在的问题是,为什么这个简单的readWriteLock
介绍会减慢整个事情的速度,如果我从不尝试获取writeLock
,以及我在这里做错了什么,因为我无法找到问题。
答案 0 :(得分:2)
锁定是昂贵的,但在你的情况下,我认为可能会出现某种类似的“死锁”#34;当你运行测试时:如果某个线程在代码的 synchronized(from){}
块中,而另一个线程想要解锁其from
块中的synchronized(to){}
实例,那么它赢了& #39; t能够:第一个synchronized
将阻止第2个线程进入synchronized(to){}
块,因此锁定不会很快被释放。
这可能导致很多线程挂在锁的队列中,这使得获取/释放锁的速度变慢。
更多注意事项:当第二部分(to.setBalance(to.getBalance() + amount);
)由于某种原因(例外,死锁)未执行时,您的代码将导致问题。您需要找到一种方法来围绕这两个操作创建一个事务,以确保它们既可以执行也可以不执行。
执行此操作的好方法是创建Balance
值对象。在你的代码中,你可以创建两个新的,更新两个余额,然后只调用两个setter - 因为setter不能失败,要么两个余额都会更新,否则代码会在调用任何setter之前失败。 / p>
答案 1 :(得分:2)
首先,将更新放入其自己的synchronized
块是正确的,即使getter和setter本身是synchronized
,所以你要避免 check-then-行为反模式。
但是,从性能的角度来看,它并不是最佳的,因为您获得了相同的锁三次(from
帐户的四次)。 JVM或HotSpot优化器知道同步原语并且能够优化嵌套同步的这种模式,但是(现在我们必须猜测一下)如果你在中间获得另一个锁,它可能会阻止这些优化。
正如在另一个问题中已经提到的那样,您可以转向无锁更新,但当然您必须完全理解它。无锁更新以一个特殊操作compareAndSet
为中心,仅当变量具有预期的旧值时才执行更新,换句话说,没有在其间执行并发更新,而执行检查和更新作为一个原子操作。并且该操作不是使用synchronized
实现的,而是直接使用专用的CPU指令。
使用模式总是像
缺点是更新可能会失败,这需要重复这三个步骤,但如果计算不是太重,并且由于更新失败表明另一个线程必须成功完成其中间的更新,那么它将是可接受的,总会有进步。
这导致帐户的示例代码:
static void safeWithdraw(AtomicInteger account, int amount) {
for(;;) { // a loop as we might have to repeat the steps
int current=account.get(); // 1. read the current value
if(amount>current) throw new IllegalStateException();// 2. possibly reject
int newValue=current-amount; // 2. calculate new value
// 3. update if current value didn’t change
if(account.compareAndSet(current, newValue))
return; // exit on success
}
}
因此,为了支持无锁访问,提供getBalance
和setBalance
操作永远不足以完成get
和set
操作之外的所有操作锁定将失败。
您有三种选择:
safeWithdraw
方法compareAndSet
方法,允许调用者使用该方法撰写自己的更新操作AtomicInteger
does in Java 8;
当然,这在使用Java 8时特别方便,您可以使用lambda表达式来实现实际的更新功能。请注意AtomicInteger
本身使用所有选项。有increment等常见操作的专用更新方法,并且compareAndSet
方法允许组合任意更新操作。
答案 2 :(得分:2)
您通常会使用 一个锁或synchronized
,一次使用两者都是不常见的。
要管理您的方案,您通常会在每个帐户上使用细粒度锁定而不是粗略锁定。您还可以使用侦听器实现总计机制。
public interface Listener {
public void changed(int oldValue, int newValue);
}
public class Account {
private int id;
private int balance;
protected ReadWriteLock lock = new ReentrantReadWriteLock();
List<Listener> accountListeners = new ArrayList<>();
public Account(int id) {
this.id = id;
this.balance = 0;
}
public int getBalance() {
int localBalance;
lock.readLock().lock();
try {
localBalance = this.balance;
} finally {
lock.readLock().unlock();
}
return localBalance;
}
public void setBalance(int balance) {
if (balance < 0) {
throw new IllegalArgumentException("Negative balance");
}
// Keep track of the old balance for the listener.
int oldValue = this.balance;
lock.writeLock().lock();
try {
this.balance = balance;
} finally {
lock.writeLock().unlock();
}
if (this.balance != oldValue) {
// Inform all listeners of any change.
accountListeners.stream().forEach((l) -> {
l.changed(oldValue, this.balance);
});
}
}
public boolean lock() throws InterruptedException {
return lock.writeLock().tryLock(1, TimeUnit.SECONDS);
}
public void unlock() {
lock.writeLock().unlock();
}
public void addListener(Listener l) {
accountListeners.add(l);
}
public int getId() {
return this.id;
}
}
public class BankingSystem {
protected List<Account> accounts;
public boolean transfer(Account from, Account to, int amount) throws InterruptedException {
if (from.getId() != to.getId()) {
if (from.lock()) {
try {
if (from.getBalance() < amount) {
return false;
}
if (to.lock()) {
try {
// We have write locks on both accounts.
from.setBalance(from.getBalance() - amount);
to.setBalance(to.getBalance() + amount);
} finally {
to.unlock();
}
} else {
// Not sure what to do - failed to lock the account.
}
} finally {
from.unlock();
}
} else {
// Not sure what to do - failed to lock the account.
}
}
return true;
}
// Rest of class..
}
请注意,可以在同一个线程中进行两次写锁定 - 第二个也是允许的。锁只会排除其他主题的访问权限。