Java 允许多个线程对一个数据集进行操作,而一个线程对数据集进行汇总

Java 允许多个线程对一个数据集进行操作,而一个线程对数据集进行汇总,java,multithreading,concurrency,thread-safety,locking,Java,Multithreading,Concurrency,Thread Safety,Locking,我正在尝试实施一个银行系统,在那里我有一套账户。有多个THRED试图在账户之间转账,而一个线程连续地或更确切地说,在随机时间尝试将所有账户余额的银行总金额相加 解决这个问题的方法一开始听起来很明显;对执行事务的线程使用REENTRANDREADWRITELOCK和readLock,对执行求和的线程使用writeLock。然而,在以这种方式实现后(参见下面的代码),我看到了性能/事务吞吐量的巨大下降,即使与只使用一个线程进行事务相比也是如此 上述实施的代码: public class Accoun

我正在尝试实施一个银行系统,在那里我有一套账户。有多个THRED试图在账户之间转账,而一个线程连续地或更确切地说,在随机时间尝试将所有账户余额的银行总金额相加

解决这个问题的方法一开始听起来很明显;对执行事务的线程使用REENTRANDREADWRITELOCK和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。如果我只删除标有a//的行!!而且也不要调用求和方法,突然之间,使用多个线程的传输吞吐量要比使用单个线程的传输吞吐量高得多,这也是我们的目标

我现在的问题是,如果我从来没有尝试获得一个写库,为什么简单的引入读写库会让整个事情慢那么多,我在这里做错了什么,因为我无法找到问题所在

旁注: 我已经问了一个关于这个问题的问题,但还是问错了问题。然而,对于我提出的那个问题,我得到了一个令人惊讶的答案。我决定不立即降低问题的质量,为了让那些需要帮助的人能够得到这个伟大的答案,我不会再编辑这个问题。相反,我打开了这个问题,坚信这不是一个重复,而是一个完全不同的问题。 锁定代价很高,但在您的情况下,我假设在运行测试时可能会出现某种几乎死锁的情况:如果某个线程位于代码的synchronizedfrom{}块中,而另一个线程希望解锁其synchronizedto{}块中的from实例,然后它将无法:第一个同步将阻止线程2进入synchronizedto{}块,因此锁不会很快释放

这可能会导致大量线程挂在锁队列中,从而导致获取/释放锁的速度变慢

更多注意事项:当第二部分转到.setBalanceto.getBalance+amount时,您的代码将导致问题;由于某种原因未执行异常、死锁。您需要找到一种方法来围绕这两个操作创建一个事务,以确保它们要么被执行,要么不被执行

执行此操作的一个好方法是创建平衡值对象。在您的代码中,您可以创建两个新的设置,更新两个余额,然后只调用两个setter-因为setter不能失败,要么更新两个余额,要么在调用任何setter之前代码将失败。

锁定是昂贵的,但在您的情况下,我假设在运行测试时可能会出现某种几乎死锁的情况:如果某个线程位于代码的synchronizedfrom{}块中,而另一个线程希望解锁其synchronizedto{}块中的from实例,那么它将无法解锁:第一个已同步线程将阻止线程2进入synchronizedto{}阻塞,因此锁不会很快释放

这可能会导致大量线程挂在锁队列中,从而导致获取/释放锁的速度变慢

更多注意事项:当第二部分转到.setBalanceto.getBalance+amount时,您的代码将导致问题;由于某种原因未执行异常、死锁。您需要找到一种方法来围绕这两个操作创建一个事务,以确保它们要么被执行,要么不被执行


执行此操作的一个好方法是创建平衡值对象。在您的代码中,您可以创建两个新的设置,更新两个余额,然后只调用两个设置器-因为设置器不能失败,要么更新两个余额,要么在调用任何设置器之前代码将失败。

首先,将更新放入自己的同步块是正确的,即使getter和setter是单独同步的,也要避免检查,然后反模式操作

但是,从性能的角度来看,这并不是最优的,因为您为from帐户三次四次获得相同的锁。JVM或热点优化器知道同步原语,并且能够优化嵌套同步的模式,但是现在我们不得不猜测,如果在这两者之间获得另一个锁,它可能会阻止这些优化

正如在另一个问题中已经提到的,您可以使用无锁更新,但当然您必须完全理解它。无锁更新以一个特殊操作为中心,该操作仅在变量具有预期的旧值时执行更新,换句话说,在这两个操作之间没有执行并发更新,而检查 d更新作为一个原子操作执行。该操作不是使用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操作之外组合操作都将失败。 您有三种选择:

将每个受支持的更新操作作为专用方法提供,如safedraw方法 提供compareAndSet方法,以允许调用方使用该方法编写自己的更新操作 提供以更新函数作为参数的更新方法,如; 当然,这在使用Java8时尤其方便,在Java8中,您可以使用lambda表达式来实现实际的更新函数。
请注意,AtomicInteger本身使用所有选项。对于常见的操作,有专门的更新方法,例如,还有允许组合任意更新操作的方法。

首先,将更新放入其自己的同步块是正确的,即使getter和setter是单独同步的,因此您可以避免检查然后反模式操作

但是,从性能的角度来看,这并不是最优的,因为您为from帐户三次四次获得相同的锁。JVM或热点优化器知道同步原语,并且能够优化嵌套同步的模式,但是现在我们不得不猜测,如果在这两者之间获得另一个锁,它可能会阻止这些优化

正如在另一个问题中已经提到的,您可以使用无锁更新,但当然您必须完全理解它。无锁更新以一个特殊操作为中心,该操作仅在变量具有预期的旧值时执行更新,换句话说,在这两个操作之间没有执行并发更新,而检查和更新作为一个原子操作执行。该操作不是使用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操作之外组合操作都将失败。 您有三种选择:

将每个受支持的更新操作作为专用方法提供,如safedraw方法 提供compareAndSet方法,以允许调用方使用该方法编写自己的更新操作 提供以更新函数作为参数的更新方法,如; 当然,这在使用Java8时尤其方便,在Java8中,您可以使用lambda表达式来实现实际的更新函数。 请注意,AtomicInteger本身使用所有选项。对于常见的操作,有专门的更新方法,例如,有一种方法允许编写任意的更新操作。

您通常会使用锁或同步,同时使用这两种方法是不常见的

为了管理您的场景,您通常会对每个帐户使用细粒度锁,而不是像您这样使用粗糙的锁。您还可以使用侦听器实现合计机制

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..
}
请注意,您可以在同一线程中使用写锁两次,也可以使用第二次。锁只排除来自其他线程的访问。

您通常会使用锁或同步,但同时使用这两个是不常见的

为了管理您的场景,您通常会对每个帐户使用细粒度锁,而不是像您这样使用粗糙的锁。您还可以使用侦听器实现合计机制

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

请注意,您可以在同一线程中使用写锁两次,也可以使用第二次。锁仅排除来自其他线程的访问。

该锁是共享锁。只要没有写锁,就不会有死锁。@Holger:synchronized是一个

写锁。我可以看到你对两个同步语句互相阻塞的建议,谢谢你的提示!但是,问题不可能真的存在,因为如果我删除ReadWriteLock,我就不会有性能问题。@Phil:如果删除同步块,它也应该消失,但这样一来,平衡就会出现错误。问题是同步块会干扰锁。尝试将锁移到外部。但是没有嵌套的同步块,没有循环,也没有阻塞操作,因此您关于没有及时释放任何锁的解释是完全错误的。该锁是共享锁。只要没有写锁,就不会有死锁。@Holger:synchronized是一个写锁。我可以看到你对两个同步语句互相阻塞的建议,谢谢你的提示!但是,问题不可能真的存在,因为如果我删除ReadWriteLock,我就不会有性能问题。@Phil:如果删除同步块,它也应该消失,但这样一来,平衡就会出现错误。问题是同步块会干扰锁。尝试将锁移到外部。但是没有嵌套的同步块,没有循环,也没有阻塞操作,因此您关于没有及时释放任何锁的解释完全是错误的。非常感谢您的解释,我现在将尝试相应地更改我的实现,看看这会给我带来什么!我改变了类Account的实现以适应现在的需求,使用了一个不同步但原子的safeDeposit和SafeDraw方法。而这在没有读写器的情况下也能很好地工作。transfer方法现在也不再包含同步语句。。但是,当我再次尝试锁定/解锁ReadWriteLock时,性能下降再次出现。这似乎首先需要更多地使用ReadWriteLock,而不是一个实现问题,也许?如果您现在配置文件会发生什么?与您的原始代码不同,线程现在不能被synchronized阻止……没错,它们不能,它们也不是。如果我对其进行分析,一切看起来都很正常,没有相互阻碍,但引入readWriteLock会花费大量的时间。这就是为什么我说,我开始觉得问题更多地在于锁的选择,而不是锁的实现,这也是很难想象的。还没有,到目前为止,我只尝试过用Java 8运行/编译。我应该尝试Java7吗?非常感谢您的解释,我现在将尝试相应地更改我的实现,看看这会给我带来什么!我改变了类Account的实现以适应现在的需求,使用了一个不同步但原子的safeDeposit和SafeDraw方法。而这在没有读写器的情况下也能很好地工作。transfer方法现在也不再包含同步语句。。但是,当我再次尝试锁定/解锁ReadWriteLock时,性能下降再次出现。这似乎首先需要更多地使用ReadWriteLock,而不是一个实现问题,也许?如果您现在配置文件会发生什么?与您的原始代码不同,线程现在不能被synchronized阻止……没错,它们不能,它们也不是。如果我对其进行分析,一切看起来都很正常,没有相互阻碍,但引入readWriteLock会花费大量的时间。这就是为什么我说,我开始觉得问题更多地在于锁的选择,而不是锁的实现,这也是很难想象的。还没有,到目前为止,我只尝试过用Java 8运行/编译。我应该试试Java7吗?还有细粒度和粗粒度的问题;在接受霍尔格的建议后,我的账户本身不再有任何锁定,因此唯一的锁定将是总和的锁定。然而,在阅读了您的答案之后,这当然也可能是一个摆脱该锁的解决方案,从而再次提高吞吐量。我会考虑用这种方式来求和,谢谢。lot@Phil-我也为您添加了一个示例银行系统,以便您可以查看如何锁定两个转账帐户。不要锁定两个帐户;这是造成僵局的秘诀。如果一个线程尝试从a转移到b,而另一个线程尝试从b转移到a,会发生什么情况?顺便说一下,没有必要持有两把锁。全局锁的全部目的是在对所有帐户求和时避免挂起的传输,而不是避免并发传输。@Holger-很好的调用-我已将其更改为tryLock,这样可以避免死锁。根据您所说的意图,其他技术可能是合适的。那么,当tryLock返回false时,您会怎么做?简单地忽略你不拥有锁不是一个合适的解决方案;在接受霍尔格的建议后,我自己的账户不再有任何锁定
s、 所以唯一的锁就是求和的锁。然而,在阅读了您的答案之后,这当然也可能是一个摆脱该锁的解决方案,从而再次提高吞吐量。我会考虑用这种方式来求和,谢谢。lot@Phil-我也为您添加了一个示例银行系统,以便您可以查看如何锁定两个转账帐户。不要锁定两个帐户;这是造成僵局的秘诀。如果一个线程尝试从a转移到b,而另一个线程尝试从b转移到a,会发生什么情况?顺便说一下,没有必要持有两把锁。全局锁的全部目的是在对所有帐户求和时避免挂起的传输,而不是避免并发传输。@Holger-很好的调用-我已将其更改为tryLock,这样可以避免死锁。根据您所说的意图,其他技术可能是合适的。那么,当tryLock返回false时,您会怎么做?简单地忽略您不拥有锁不是一个合适的解决方案。