多线程程序中的意外结果

Unexpected result in multithreaded program

这个简单的程序有一个共享数组和 2 个线程: 第一个线程 - 显示数组中值的总和。 第二个线程 - 从数组的一个单元格中减去 200,并将 200 添加到另一个单元格中。

我希望看到结果:1500(数组的总和),1300(如果显示发生在减法和加法之间)。

但由于某些原因,有时会出现 1100 和 1700,我无法解释...

public class MainClass {

    public static void main(String[] args) {
        Bank bank = new Bank();
        bank.CurrentSum.start();
        bank.TransferMoney.start();
    }
}

class Bank {
    private int[] Accounts = { 100, 200, 300, 400, 500 };
    private Random rnd = new Random();

    Thread CurrentSum = new Thread("Show sum") {
        public void run() {
            for (int i = 0; i < 500; i++) {
                System.out.println(Accounts[0] + Accounts[1] + Accounts[2]
                        + Accounts[3] + Accounts[4]);
            }
        }
    };

    Thread TransferMoney = new Thread("Tranfer"){
        public void run(){
            for(int i=0; i<50000; i++)
            {
                Accounts[rnd.nextInt(5)]-=200;
                Accounts[rnd.nextInt(5)]+=200;
            }
        }
    };
}

您没有以原子或线程安全的方式更新值。这意味着有时您看到的 -200 比 +200 多两个,有时您看到的 +200 比 -200 多两个。当您遍历这些值时,可能会看到一个 +200 值,但 -200 值是一个较早的值,您错过了它,但是您看到另一个 +200 更新再次错过了 -200 更改。

在极少数情况下,最多应该可以看到 5 x +200 或 5 x -200。

也许是故意的,你没有以原子方式进行加法运算。

这意味着这一行:

System.out.println(Accounts[0] + Accounts[1] + Accounts[2]
                        + Accounts[3] + Accounts[4]);

将 运行 分为多个步骤,其中任何步骤都可能发生在第二个线程的任何迭代期间。

1. Get value of Accounts[0] = a
2. Get value of Accounts[1] = b
...So on

然后在从数组中提取所有值后进行添加。

你可以想象从Accounts[0]中减去200,被JRE解引用,然后在第二个线程的另一个循环中,从Accounts[1]中移除200,随后被JRE解引用.这可能会导致您看到的输出。

发生这种情况是因为五个值的加法不是原子的,并且可能会被另一个线程中发生的递减和递增打断。

这是一种可能的情况。

  • 显示线程添加Accounts[0]+Accounts[1]+Accounts[2].
  • 更新线程递减 Accounts[0] 并递增 Accounts[3]
  • 更新线程递减 Accounts[1] 并递增 Accounts[4]
  • 显示线程继续添加,将 Accounts[3]Accounts[4] 添加到它已经部分计算的总和中。

在这种情况下,总和将为 1900,因为您在递增后包含了两个值。

你应该能够计算出这样的情况,得到 7002300 之间的总和。

正在从多个线程访问 Accounts 变量,其中一个线程修改了它的值。为了让另一个线程完全可靠地读取修改后的值,有必要使用 "memory barrier"。 Java 有多种方式提供内存屏障:同步、易失性或其中一种原子类型是最常见的。

银行 class 也有一些逻辑要求在 Accounts 变量恢复一致状态之前分多个步骤进行修改。 synchronized 关键字还可用于防止在同一对象上同步的另一个代码块 运行ning 直到第一个同步块完成。

Bank 的此实现 class 使用拥有 Accounts 变量的 Bank 对象的互斥锁对象锁定对 Accounts 变量的所有访问。这确保每个同步块在其他线程可以 运行 自己的同步块之前完整 运行 。它还确保对 Accounts 变量的更改对其他线程可见:

class Bank {
    private int[] Accounts = { 100, 200, 300, 400, 500 };
    private Random rnd = new Random();

    Thread CurrentSum = new Thread("Show sum") {
        public void run() {
            for (int i = 0; i < 500; i++) {
                printAccountsTotal();
            }
        }
    };

    Thread TransferMoney = new Thread("Tranfer"){
        public void run(){
            for(int i=0; i<50000; i++)
            {
                updateAccounts();
            }
        }
    };

    synchronized void printAccountsTotal() {
        System.out.println(Accounts[0] + Accounts[1] + Accounts[2]
                + Accounts[3] + Accounts[4]);
    }

    synchronized void updateAccounts() {
        Accounts[rnd.nextInt(5)]-=200;
        Accounts[rnd.nextInt(5)]+=200;
    }
}