【问题标题】:Locking all instances of a class in Java在Java中锁定类的所有实例
【发布时间】:2019-04-06 14:52:54
【问题描述】:

我正在实施一个并行银行系统,所有操作都可以同时运行。我已经实现了一个线程安全的transferMoney 方法,它将amount 从帐户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;
    }

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

另外,我实现了一个方法,用下面的代码总结银行里的总金额:

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

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

    return sum.intValue();
}

问题

当我同时运行sumAccounts()transferMoney() 时,即使没有添加任何钱,我也会在银行之前更多(有时更少)钱。据我了解,如果我通过synchronized(Account.class) 锁定所有Account 对象,我是否应该在阻止transferMoney() 的执行时获得正确的银行金额?

到目前为止我已经尝试过什么

我尝试了以下方法:

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

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

编辑: getBalance()的代码:

public int getBalance() {
        return balance;
}

【问题讨论】:

  • 在 Account.class 上同步不会获取任何 Account 实例上的监视器;另见stackoverflow.com/questions/2056243/…
  • Account.getBalance()是同步方法吗?如果没有,您将得到之前的余额,如果减去一个帐户并加上新添加的帐户的余额
  • @Cratylus 不,它不同步,因为我在transferMoney() 中访问它们时锁定了对象。我添加了它以进行测试,但它仍然给了我错误的结果。
  • @pr0f3ss:你能把getBalance()的代码贴出来吗?
  • @Cratylus 刚刚将其添加为问题的编辑

标签: java multithreading thread-safety synchronized reentrantlock


【解决方案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();
  }
}

同样,可重入锁的公平模式往往比非公平模式执行得慢。详情请查看文档。

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

【讨论】:

  • 刚刚运行了这段代码。它实际上返回了一个不正确的总和(不知道为什么,尽管之前我只用一个普通的 ReentrantLock 对象实现了它并且它起作用了)。
  • @pr0f3ss 如果这给出了错误的结果,可能是因为 transferMoney 中的线程所做的操作对于执行 sumAccounts 方法的线程是不可见的。我认为锁定将保证这发生在关系之前。您可以尝试使 balance 字段不稳定。 (我假设您的代码的其他部分没有错误。
  • sumAccounts 确实在运行时在新的Thread 对象中执行。我最终让它工作了,但性能仍然很差,就像迄今为止所有工作一样。
  • @pr0f3ss 你做了什么让它发挥作用。这应该比 Reentrantlock 执行得更好。
  • 我以前都用过rwl.readLock.lock()(我知道是愚蠢的)。诀窍是在Account 类中创建一个锁,然后从那里锁定和解锁。因此,在每个循环的第一个循环中,您将锁定帐户,然后将其余额添加到总和中,最后在每个循环的后续循环中,您以相同的顺序解锁所有帐户。这保证不会同时发生任何事务。您认为回答我自己的问题来回答问题是否合适?
【解决方案2】:

正如评论中所述,锁定类对象不会锁定该类的所有实例,它只会锁定代表您的 Account 类的 Class 对象。该锁与 Account 对象上的锁并非不兼容,因此您根本不会进行同步。

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

- 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() 处理(这样就不再需要锁定单个对象)。

【讨论】:

  • 你是对的。但是通过同一个锁同步这两种方法,我得到的性能比顺序执行更差。 (见问题——到目前为止我已经尝试过什么——要点 3 和 4)
  • 当然你会得到“更糟糕的表现”(特别是车队综合症)。这只是为更好的完整性(以及更好的结果正确性保证)付出的代价。 synchronized() 块持有的 java 锁的主要问题是它们总是排他锁(java 没有任何概念,例如 DBMS 确实有的“共享”锁 - 这就是允许它们实现更高并行度的原因,同时保持更好的完整性保证)。与这里的空间相比,这件事要说的太多了。
  • 查看 ReadWriteLock 上的其他回复。 (请注意,如果您使用数据库技术完成所有这些工作,您将免费获得所有这些,而无需编写一行代码,而且通常无需花一秒钟时间思考“什么类型的锁”或此类问题。
  • 是的,我知道我的性能会变差,但即使我同时单独运行transferMoney()(没有sumAccounts()),我的性能也会差三到四倍。
【解决方案3】:

审查您的代码非常困难,因为我们无法知道您同步的对象帐户在所有函数中是否完全相同。
首先,我们必须同意余额总和和金额转移是否是两个应该同时运行的操作。
我希望在转移金额之前和之后的余额总和是相同的。
此外,您在余额总和中使用了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 属性的事务的数据库中发生。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多