Java 竞争条件:整数的最小和最大范围

Java 竞争条件:整数的最小和最大范围,java,multithreading,race-condition,Java,Multithreading,Race Condition,我最近在一次采访中被问到这个问题 给定以下代码,静态整数的最小和最大可能值是多少 import java.util.ArrayList; import java.util.List; public class ThreadTest { private static int num = 0; public static void foo() { for (int i = 0; i < 5; i++) { num++;

我最近在一次采访中被问到这个问题

给定以下代码,静态整数的最小和最大可能值是多少

import java.util.ArrayList;
import java.util.List;

public class ThreadTest {
    private static int num = 0;

    public static void foo() {
        for (int i = 0; i < 5; i++) {
            num++;
        }
    }

    public static void main(String[] args) throws Exception{
        List<Thread> threads = new ArrayList<Thread>();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Task());
            threads.add(thread);
            thread.start();
        }
        for (int i = 0; i < 5; i++) {
            threads.get(i).join();
        }
        // What will be the range of num ???
        System.out.println(ThreadTest.num);
    }
}

class Task implements Runnable {
    @Override
    public void run() {
        ThreadTest.foo();
    }

}
import java.util.ArrayList;
导入java.util.List;
公共类线程测试{
私有静态int num=0;
公共静态void foo(){
对于(int i=0;i<5;i++){
num++;
}
}
公共静态void main(字符串[]args)引发异常{
List threads=new ArrayList();
对于(int i=0;i<5;i++){
线程线程=新线程(新任务());
线程。添加(线程);
thread.start();
}
对于(int i=0;i<5;i++){
threads.get(i.join();
}
//数值的范围是多少???
System.out.println(ThreadTest.num);
}
}
类任务实现可运行{
@凌驾
公开募捐{
ThreadTest.foo();
}
}
我告诉他们,最大值是25(在没有竞争条件的情况下),最小值是5(在每次迭代的所有线程之间存在竞争条件的情况下)。
但是采访者说最小值甚至可以低于5。

这是怎么可能的?

您的线程正在更新一个非易失性变量,这意味着它不能保证每个线程都能看到更新后的
num
值。让我们考虑以下线程的执行流程:

Thread 1: 0->1->2 (2 iteration left)
Thread 2: 0->1->2->3 (1 iteration left)
Thread 3: 0->1->2->3 (1 iteration left)
Thread 4: 0->1->2->3 (1 iteration left)
Thread 5: 0->1->2->3 (1 iteration left)
此时,线程1将num的值
2
刷新到内存中,线程2,3,4,5决定再次从内存中读取
num
(出于任何原因)。现在:


线程1将值
4
刷新到内存中,然后刷新第2,3,4条。。将值刷新到内存中,显示数字的当前值将是
3
,而不是
5

我声称最小值可能是2

这一点的关键是
num++
的非原子性,即它是一个读操作和一个写操作,其间可能有其他操作

调用线程T1..T5:

  • T1读取0,T2读取0
  • T1写1,然后读写3次
  • 然后T2写入1
  • 然后T1读取1
  • 然后T2-5完成他们所有的工作
  • 然后,最后,T1写入2
(注意:结果2不依赖于线程数或迭代次数,前提是每个线程至少有2个。)

但诚实的回答是:这真的不重要。存在一种数据竞争,定义如下:

当一个程序包含两个冲突的访问(§17.4.1[“如果至少有一个访问是写入的,则对同一变量的两个访问(读取或写入)称为冲突。”]),而不是按“发生在之前”关系排序时,则称其包含数据竞争

(线程中的操作之间没有before关系)

所以你不能有效地依赖它所做的一切。这完全是错误的代码


(此外,我知道这个问题的答案并不是因为一些来之不易的调试多线程代码的战斗,也不是因为深入的技术阅读:我知道这一点是因为我以前在其他地方读过这个答案。这是一个客厅的把戏,仅此而已,所以问最小值不是一个很好的面试问题)。

嗯,我的答案是最大25,和最小值0,因为所有操作都是递增的,并且您将其初始化为0。。我认为静态的非易失性int是为了让你进入关于比赛条件的思考,但是在任何情况下,有没有任何东西会减少这个数字


编辑:值得一提的是,这将是一个典型的分心,他们可能期望你在现实世界中能够克服,为这样的“诡计”辩护,有很多的红鲱鱼

在我看来,由于缺少原子操作,达到25是不可能的(参见Java教程)

所有线程几乎同时启动,因此每个线程在第一次迭代中将
ThreadTest.num
值视为
0
。由于有5个线程并行访问同一变量,在第三次迭代中,线程可能会看到
ThreadTest.num
值仍然是
1
2
,并且会错误地增加到
2
3

根据硬件的不同,最终值会更低或更高,最快的可能具有最低值,最慢的可能具有较高值。但是,我的要求是最大值不能达到25

编辑(2019-10-07)

我在我的机器(CoreI5HQ)上测试了自己,最终的结果几乎总是达到
25
。为了更好地理解,我在
for
循环中测试了一个更大的数字:

for(int i=0;i<10000;i++){
num++;
}

现在,大多数情况下,最终结果在20000到30000之间,远远不是50000。

线程1在它的第一次迭代(
i==0
)中读取
num==0
,然后所有其他线程执行它们的操作,除了线程2,它只执行了前4次迭代。然后线程1继续,将
0
增加到
1
并存储它,线程2在最后一次迭代中读取
1
,线程2在最后一次迭代中读取此
1
,线程1执行其余操作,线程2将
1
增加到
2
并终止。不确定它是否能低于
2
。诚实的回答是:这其实并不重要。这是一场数据竞赛,所以你不能有效地依赖它所做的一切。这完全是不正确的代码。添加
volatile
将使该程序顺序一致,因此从VM的角度来看没有“竞争”。@johanneskun
num++
不是原子的,因此它甚至不能与
volatile
一起工作。你问采访者:这怎么可能?我认为“
Thread 1: 2->3->4 (completed 2 iteration)
Thread 2: 2->3 (completed 1 iteration)
Thread 3: 2->3 (completed 1 iteration)
Thread 4: 2->3 (completed 1 iteration)
Thread 5: 2->3 (completed 1 iteration)