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