用Java锁定类的所有实例

问题描述 投票:1回答:3

我正在实施一个并行银行系统,所有操作都可以同时运行。我已经实现了一个线程安全的transferMoney方法,它将amount从Account from转移到to

transferMoney使用以下代码实现:

public boolean transferMoney(Account from, Account to, int amount) {
        if (from.getId() == to.getId()){
            return false;
        }else if(from.getId() < to.getId()) {
            synchronized(to) {
                synchronized(from) {
                    if(from.getBalance() >= amount) {
                        from.setBalance(from.getBalance()-amount);
                        to.setBalance(to.getBalance()+amount);
                    }else {
                        return false;
                    }
                }
            }
        }else {
            synchronized(from) {
                synchronized(to) {
                    if(from.getBalance() >= amount) {
                        from.setBalance(from.getBalance()-amount);
                        to.setBalance(to.getBalance()+amount);
                    }else {
                        return false;
                    }
                }
            }
        }

        return true;
    }

为了防止死锁,我已经指定始终以相同的顺序获取锁。为了确保以相同的顺序获取锁,我使用ID的独特Account

另外,我已经实施了一种方法,用以下代码总结银行的总金额:

public int sumAccounts(List<Account> accounts) {
    AtomicInteger sum = new AtomicInteger();

    synchronized(Account.class) {
        for (Account a : accounts) {
            sum.getAndAdd(a.getBalance());
        }
    }

    return sum.intValue();
}

Problem

当我与sumAccounts()同时运行transferMoney()时,我会在银行之前获得更多(有时更少)的资金,即使没有增加资金。从我的理解,如果我通过Account锁定所有synchronized(Account.class)对象,我不应该得到正确的银行总和,因为我阻止执行transferMoney()

What I have tried this far

我试过以下的事情:

  • 像上面一样同步Account.class(不起作用)
  • 同步for each循环中的特定帐户(当然,这不是线程安全的,因为事务同时发生)
  • 通过ReentrantLock对象同步两种方法。这样可行,但它会对性能产生巨大影响(需要三倍于顺序代码)
  • 在类级别同步两种方法。这也有效,但是再次比按顺序运行操作要长三倍。

不应该锁定Account.class阻止任何进一步的transferMoney()处决?如果没有,我该如何解决这个问题?

编辑:getBalance()的代码:

public int getBalance() {
        return balance;
}
java multithreading thread-safety synchronized reentrantlock
3个回答
1
投票

您可以在这种情况下使用ReadWriteLock。 transferMoney方法将使用读锁定,因此可以同时执行。 sumAccounts方法将使用写锁定,因此当它执行时,没有transferMoney(或sumAccounts)可以从其他线程执行。

使用ReentrantLock并在类级别同步这两种方法,其行为与您所声明的相同,因为它们不会让transferMoney方法并发执行。

示例代码:

final ReadWriteLock rwl = new ReentrantReadWriteLock();

public boolean transferMoney(Account from, Account to, int amount) {
  rwl.readLock().lock();
  try{
    .... Your current code here
  }
  finally {
       rwl.readLock().unlock();
  }
}

public int sumAccounts(List<Account> accounts) {
  rwl.writeLock().lock();
  try{
    // You dont need atomic integer here, because this can be executed by one thread at a time
    int sum = 0;
    for (Account a : accounts) {
        sum += a.getBalance();
    }
    return sum;
  }
  finally {
       rwl.writeLock().unlock();
  }
}

此外,Reentrant锁的公平模式往往比非公平模式执行速度慢。查看文档了解详细信息。

https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html


1
投票

如注释中所述,对类对象进行锁定不会对该类的所有实例进行锁定,只会对表示Account类的Class对象进行锁定。该锁与Account对象上的锁不兼容,因此您根本没有同步。

对各个Account对象进行锁定可以在for循环中完成(在sumAccounts中),但它不会阻止这样的计划发生:

- sumAccounts locks 'first' Account and reads balance (and releases lock again at end of the synchronized block taking the lock)
- system schedules a moneyTransfer() from 'first' to 'last'
- sumAccounts locks 'last' Account and reads balance, which includes the amount that was just transferred from 'first' and was already included in the sum

因此,如果你想要防止这种情况,你也需要在Account.class上同步moneyTransfer()处理(这样就不再需要锁定单个对象)。


0
投票

查看代码非常困难,因为我们无法知道您同步的对象帐户是否是所有函数中完全相同的实例。 首先,我们必须同意余额和金额转移的总和是两个应该同时运行的操作。 我希望在金额转移之前和之后余额的总和是相同的。 另外,你在余额的总和中使用synchronized(Account.class)这是错误的。您应该在循环的对象上进行同步。 现在,即使您确实在完全相同的实例中进行协调,您仍然可以拥有以下计划:

Thread-1 (transfer)  
  locks from  
Thread-2 (sum balance)  
  locks first object in the list and adds the balance to the running sum and moves to next object
Thread-1  
   locks to (which is the object Thread-2) processed
   moves money from => to  

您已经将to与增加前的金额相加,您可以在扣除后添加from,具体取决于时间安排。

问题是您要在传输中更新2个对象,但只能在总和中锁定1。 我建议的是:

  1. 要么在同一个锁上同步两个方法,要使它们串行运行
  2. 当对象进入transfer方法时设置一些脏标志,如果设置了,则在余额之和中跳过它们,并在完成所有更新后完成总和
  3. 为什么你甚至用Java做这个?这应该使用具有ACID属性的事务在数据库中进行。
© www.soinside.com 2019 - 2024. All rights reserved.