当一个线程对它进行求和时,让多个线程对数据集进行操作

时间:2015-04-15 09:44:52

标签: java multithreading concurrency thread-safety locking

我正在尝试实施一个银行系统,我有一组帐户。有多个试图在账户之间转账,而一个线程连续(或者更确切地说,在随机时间)试图总结银行的总金额(所有账户余额的总和)。

解决这个问题的方法起初听起来很明显;对于执行事务的线程使用ReentrantReadWriteLocksreadLock,对执行求和的线程使用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,以及我在这里做错了什么,因为我无法找到问题。

  • 旁注: 我已经问了一个关于这个问题的问题here,但设法提出错误的问题。然而,我确实为这个问题得到了一个惊人的答案。我决定不会毫无疑问地降低问题质量,并且为那些需要帮助的人保留那个伟大的答案,我不会再编辑这个问题。相反,我打开了这个问题,坚信这是重复,而是完全不同的事情。

3 个答案:

答案 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指令。

使用模式总是像

  1. 读取当前值
  2. 计算新值(或拒绝更新)
  3. 尝试执行更新,如果当前值仍然相同,则会更新
  4. 缺点是更新可能会失败,这需要重复这三个步骤,但如果计算不是太重,并且由于更新失败表明另一个线程必须成功完成其中间的更新,那么它将是可接受的,总会有进步。

    这导致帐户的示例代码:

    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
        }
    }
    

    因此,为了支持无锁访问,提供getBalancesetBalance操作永远不足以完成getset操作之外的所有操作锁定将失败。 您有三种选择:

    1. 将所有受支持的更新操作作为safeWithdraw方法
    2. 等专用方法提供
    3. 提供compareAndSet方法,允许调用者使用该方法撰写自己的更新操作
    4. 提供一种更新方法,将更新函数作为参数,如AtomicInteger does in Java 8; 当然,这在使用Java 8时特别方便,您可以使用lambda表达式来实现实际的更新功能。
    5. 请注意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..
}

请注意,可以在同一个线程中进行两次写锁定 - 第二个也是允许的。锁只会排除其他主题的访问权限。