多线程程序中的意外结果
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,因为您在递增后包含了两个值。
你应该能够计算出这样的情况,得到 700
和 2300
之间的总和。
正在从多个线程访问 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;
}
}
这个简单的程序有一个共享数组和 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,因为您在递增后包含了两个值。
你应该能够计算出这样的情况,得到 700
和 2300
之间的总和。
正在从多个线程访问 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;
}
}