尽管在 Java 代码中使用了 lock(),但两个线程使用相同的方法?

Posted

技术标签:

【中文标题】尽管在 Java 代码中使用了 lock(),但两个线程使用相同的方法?【英文标题】:Two threads use same method despite using lock() in Java code? 【发布时间】:2020-04-28 18:55:42 【问题描述】:

我有一个简单的 Java 代码。目的是测试Java的entryLock()。有一个名为 Bank 的类(代码如下),其中包含一个 Account 对象数组(Account 类的代码如下)。

Bank 类可以使用名为withdraw end deposit 的Account 对象的方法将钱从一个Account 转移到另一个Account。 (其中一个数据成员是名为 accounts 的 Accounts 数组)。

AccountThread 扩展了 Thread,它将每个帐户作为一个线程运行。它包含对“银行”对象的引用。

TestBank 具有 (public static void main(String args[]) 方法并将运行对象。

它将初始化一个 Bank 对象,创建 10 个 AccountThread 对象,将对该 Bank 对象的引用传递给这些 AccountThread 对象。然后它将开始运行所有这 10 个线程(运行 AccountThread 对象)。

这 10 个帐户只相互转账。每个线程将运行自己的帐户。

这些线程将无限循环,随机选择一个帐户(其他 9 个帐户之一)并将随机金额转移到该帐户。

在每 10 000 笔交易后,程序会打印一些数据,其中之一是“总余额”(所有这 10 个帐户的余额之和)。

这笔金额永远不会改变,因为账户只是相互转账。

我在“存款”和“取款”方法中使用了 lock.lock() 和 lock.unlock(),这样当一个帐户中有存款或取款时,其他线程不应访问这些方法。

但是有一些问题:

但问题是,我确实得到了一些与总和的偏差,我不明白是什么原因造成的。

我尝试使用“AccountThread”作为类实现Runnable,但结果是一样的。

我试过了:

1) 而且我怀疑偏差可能是由于打印数据时线程仍在运行(这可能导致数据不一致)引起的。所以我在 Bank 类的 transfer 方法上做了一些改动,编写了一个代码来阻止所有线程在 test() 之前运行(并在运行 test() 之后解锁所有线程)。

for (Account account: accounts) account.getLock().lock(); 在调用 test() 和 toString() 之前

在 test() 和 toString() 之后我确实调用了

for (Account account: accounts) account.getLock().unlock(); 

但结果是一样的。

2) 在运行test()之前,我只锁定了刚才在transfer中使用的两个账号,这样其他线程就不会使用这些账号了。但它也不起作用。

我也尝试过其他组合,但结果不是预期的结果。

另外两个问题是:

另一个问题是,而 toString() 和 test() 方法是 相互调用,运行程序时这些方法得到 随机调用**

第三个问题是 test() 结果显示不是在每 10 000 次之后 交易,但通常超过 10 000 笔交易

尽管方法使用“lock.lock() 和“lock.unlock()”方法,但多个线程是否可以同时从同一个类访问相同的 deposit() 或 withdraw() 方法?

这里为什么会有偏差?如何确保总余额始终相同?

这是 Account 类:

import java.util.concurrent.locks.*;

class Account 
  private int balance;
  private int accountNumber;
  private Lock lock;
  private Condition lockCondition;

  public Account(int accountNumber, int balance) 
    this.accountNumber = accountNumber;
    this.balance = balance;
    this.lock = new ReentrantLock();
    this.lockCondition = lock.newCondition();
  


/* HERE IS THE WITHDRAW AND DEPOSIT METHODS THAT ARE LOCKED */
  void withdraw(int amount)  
      lock.lock(); // Acquire the lock
      try 
        while (balance < amount) 
          lockCondition.await();
        
        balance -= amount;
      
      catch (InterruptedException ex) 
        ex.printStackTrace();
      
      finally 
        lock.unlock(); // Release the lock
      
  

  void deposit(int amount) 
      lock.lock(); // Acquire the lock
      try 
        balance += amount;
        // Signal thread waiting on the condition
        lockCondition.signalAll();
      
      finally 
        lock.unlock(); // Release the lock
      
  

  int getAccountNumber() 
    return accountNumber;
  

  public int getBalance() 
    return balance;
  

  Lock getLock() 
      return lock;
  

这是 AccountThread 类:

import java.util.Random;

class AccountThread extends Thread 
  private Bank bank;
  private boolean debug;
  private int accountIndex;
  private int maxTransferAmount;
  private Random random;

  public AccountThread(Bank b, int index, int max, boolean debug) 
    this.bank = b;
    this.accountIndex = index;
    this.maxTransferAmount = max;
    this.debug = debug;
    this.random = new Random();
  

  public void run() 
    try 
      while (!interrupted()) 
        for (int i = 0; i < maxTransferAmount; i++) 
            int toAccount = random.nextInt(bank.nrOfAccounts());
            int amount = random.nextInt(maxTransferAmount);
            bank.transfer(accountIndex, toAccount, amount);
            sleep(2);
        
      
     catch (InterruptedException ignored) 
    
  

这是银行类:

import java.util.concurrent.locks.*;

class Bank 
  private static final int TEST_FREQUENCY = 10000;
  private static final int TO_STRING_FREQUENCY = 10000;
  private Lock lock;
  private int deviationCount;
  private int initialBalance;
  private Account[] accounts;
  private long transactionCount;
  private boolean debug;
  private int testCount;


  public Bank(int accountAmount, int initialBalance, boolean debug) 
    accounts = new Account[accountAmount];
    this.initialBalance = initialBalance;
    this.debug = debug;
    int i;
    for (i = 0; i < accounts.length; i++)
      accounts[i] = new Account(i, initialBalance);
    this.transactionCount = 0;
    this.deviationCount = 0;
    this.lock = new ReentrantLock();
  

  public void transfer(int fromAccount, int toAccount, int amount) 
      accounts[fromAccount].withdraw(amount);
      accounts[toAccount].deposit(amount);
      this.transactionCount++;
//    accounts[fromAccount].getLock().lock();
//    accounts[toAccount].getLock().lock();
    //  for (Account account: accounts) account.getLock().lock(); 
      lock.lock();
      try 
          if (transactionCount % TEST_FREQUENCY == 0) 
              test();
          
          if (transactionCount % TO_STRING_FREQUENCY == 0) 
              System.out.println(toString());
          

    //    accounts[fromAccount].getLock().unlock();
//        accounts[toAccount].getLock().unlock();
       finally 
          lock.unlock();
      
//    for (Account account: accounts) account.getLock().unlock();
  


  public void test() 
    int sum = 0;
    for (Account account : accounts) sum += account.getBalance(); 

    if (sum != nrOfAccounts()*initialBalance) deviationCount++; 

    System.out.println("Transactions:" + getTransactionCount() + " Balance: " + sum
            + " Deviation count: " + getDeviationCount());
    testCount++;
  

  @Override
  public String toString() 
      String string = String.format("\nTransactions; %d%n"
            + "Initial balance: %d%nNumber of accounts: %d%n"
            + "Deviation count: %d%nTestCount: %d%n"
            + "Error percentage: %.2f%n", 
            getTransactionCount(), initialBalance, nrOfAccounts(), 
            getDeviationCount(), testCount, getErrorPercentage());
      if (debug) 
          for (Account account :accounts) 
              string = string.concat(String.format("Account nr.: %d, Balance: %d%n", 
                      account.getAccountNumber(), account.getBalance()));
          
      
      return string;
  

  int nrOfAccounts() 
    return accounts.length;
  

  private long getTransactionCount() 
      return transactionCount;
  

  private int getDeviationCount() 
      return deviationCount;
  

  private double getErrorPercentage() 
      double dividend = getDeviationCount();
      double divisor = testCount;
      double result = dividend / divisor;
      return result;
  

这是 BankTest 类:

    public class BankTest 
    private static final boolean DEBUG = true;
    private static final int ACCOUNT_AMOUNT = 10;
    private static final int INITIAL_BALANCE = 100000;

    public BankTest() ;

    public static void main(String[] args) 
        Bank b = new Bank(ACCOUNT_AMOUNT, INITIAL_BALANCE, DEBUG);
        int i;
        for (i = 0; i < ACCOUNT_AMOUNT; i++) 
            AccountThread t = new AccountThread(b, i,
                    INITIAL_BALANCE, DEBUG);
            t.setPriority(Thread.NORM_PRIORITY + i % 2);
            t.start();
        
    

【问题讨论】:

【参考方案1】:

Q:当有test()在进行中,而其他AccountThread从一个已经测试过采样的账户中扣款,转入测试尚未采样的账户时会发生什么?

A:移动的钱将被计算两次。

如果资金从尚未抽样的帐户转移到已抽样的帐户,则会出现相反的问题。这笔钱根本不会被计算在内。

到目前为止,您尝试的锁定确保银行中的总数始终是正确的在任何一个瞬间,但您的test() 无法一次性完成立即的。您需要在清点期间关闭银行——防止任何和所有转账。

IMO:最好的方法是使用ReadWriteLock。 ReadWriteLock 的正常用例是线程经常想要检查但很少想要更新的共享数据结构。它将允许任意数量的同时“读者”访问该结构*或*它将允许一个“作者”访问它,但不能同时访问两者。

在您的应用程序中,想要转移资金的线程是“读者”。这可能听起来很奇怪,因为这些线程实际上想要更改 bank 结构,但重点是,您希望允许任意数量的线程同时执行它们的操作。另一方面,想要调用test() 的线程,即使它没有修改任何内容,也必须扮演“作者”角色,因为它必须独占 访问银行。在test() 进行期间不得进行任何转移。

【讨论】:

感谢您的回答。我发现了问题。 1) 'this.transactionCount++' 应该放入 transfer() 方法中的“锁定代码”(在 lock.lock() 和 lock.unlock() 之间)。 2)在 test() 方法中对所有余额求和之前,我们应该锁定所有帐户(因此在我们对所有余额求和时没有进行任何交易)(就像这样 ´account.getLock().unlock();´。3)“提款”并且“存款”方法应该在“交易”中(我不知道它是如何完成的),这样当账户被锁定时,没有“存款”就没有“提款”,所以当代码运行时不会损失任何钱 test( )。 @Coder88,“事务”描述了一种接口类型,而不是任何特定的实现或机制。事务系统的客户端可以“打开”事务,这有效地为客户端提供了系统状态的私有副本。客户端可以查询私有状态,进行更改,然后“提交”或“中止”事务。如果提交失败,或者客户端中止,那么就好像事务从未发生过一样。如果提交成功,就好像整个事情发生在一个单一的时刻(即,它“原子地”发生。)

以上是关于尽管在 Java 代码中使用了 lock(),但两个线程使用相同的方法?的主要内容,如果未能解决你的问题,请参考以下文章

从构建分布式秒杀系统聊聊Lock锁使用中的坑

java 线程安全 Lock

Java并发编程--Lock

Java有了synchronized,为什么还要提供Lock

尽管将 Python multiprocessing.Lock 作为目标函数参数传递,但在并行化时为无

JAVA基础知识(13)-----Lock接口