主线程随机没有结束(试图在并发线程中求和自然数)

The main thread randomly doesn't reach end (trying to sum natural numbers in concurrent threads)

我有下面的代码求1到5000的自然数之和。这是一个练习并发的简单练习。

public static void main(String[] args) throws InterruptedException {
        final int[] threadNb = new int[] {5};
        final Integer[] result = new Integer[1];
        result[0] = 0;
        List<Thread> threads = new LinkedList<>();
        IntStream.range(0, threadNb[0]).forEach(e -> {
            threads.add(new Thread(() -> {
                int sum = 0;
                int idx = e * 1000 + 1;
                while (!Thread.interrupted()) {
                    if (idx <= (e + 1) * 1000) {
                        sum += idx++;
                    } else {
                        synchronized(result) {
                            result[0] += sum;
                            System.err.println("sum found (job " + e + "); sum=" + sum + "; result[0]=" + result[0] + "; idx=" + idx);
                            Thread.currentThread().interrupt();
                        }
                    }
                }
                synchronized(result) {
                    System.err.println("Job " + e + " done. threadNb = " + threadNb[0]);
                    threadNb[0]--;
                    System.err.println("threadNb = " + threadNb[0]);
                }
            }));
        });
        threads.forEach(Thread::start);
        //noinspection StatementWithEmptyBody
        while(threadNb[0] > 0);
        System.out.println("begin result");
        System.out.println(result[0]);
        System.out.println("end result");
    }

有时,当我运行代码时,最后3System.out.println()不显示。如果我在 while(threadNb[0] > 0) 中放置一个语句,就像另一个 System.out.println() 一样,我的问题就不会再发生了。

任何人都可以向我解释这种行为吗?

在此先感谢您的帮助

最可能的解释是编译器优化了您的代码,以便缓存 threadNb[0] 的值。因此,主线程可能看不到其他线程所做的更新。制作你的计数器volatile可以帮助解决这个问题。

但是,当前忙等待的方法通常不是最佳解决方案。您应该使用 class Threadjoin() 方法让您的主线程等待它们中的每一个都结束。

例如:

for(Thread t: threads) {
    try{
        t.join();
    } catch(InterruptedException e) {}
}

threadNb 变量的声明方式并没有告诉 JVM 它需要对其他线程进行更新。在什么时候对变量的更新对其他线程可见完全取决于 JVM 实现,它可以根据情况使它们可见或不可见。此外,如果 JIT 认为它可以摆脱它,它可以自由地重新排序或优化代码,并且它的决策基于可见性规则。所以很难说这里到底发生了什么,因为 Java 语言规范没有指定行为,但是你肯定遇到了一个问题,即你的工作线程的更新通常不会被主线程看到。

如果您将数组替换为 AtomicInteger,则保证更新对其他线程可见。 (Volatile 也有效,但首选 AtomicInteger。为了使用 volatile,您必须使变量成为实例或 class 成员。)如果将更新的值保存在局部变量中,则不需要同步:

import java.util.*;
import java.util.stream.*;
import java.util.concurrent.atomic.*;    
public class SumNumbers {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger threadNb = new AtomicInteger(5);
        AtomicInteger result = new AtomicInteger(0);
        List<Thread> threads = new LinkedList<>();
        IntStream.range(0, threadNb.intValue()).forEach(e -> {
            threads.add(new Thread(() -> {
                int sum = 0;
                int idx = e * 1000 + 1;
                while (!Thread.currentThread().isInterrupted()) {
                    if (idx <= (e + 1) * 1000) {
                        sum += idx++;
                    } else {
                        int r = result.addAndGet(sum);
                        System.out.println("sum found (job " + e + "); sum=" 
                        + sum + "; result=" + r 
                        + "; idx=" + idx);
                        Thread.currentThread().interrupt();
                    }
                }
                System.out.println("Job " + e + " done.");
                int threadNbVal = threadNb.decrementAndGet();
                System.out.println("Job " + e + " done, threadNb = " + threadNbVal);
            }));
        });
        threads.forEach(Thread::start);
        //noinspection StatementWithEmptyBody
        while(threadNb.intValue() > 0);
        System.out.println("result=" + result.intValue());
    }
}

在哪里可以看到更新。

不推荐忙等待,因为它会浪费 CPU 个周期。您确实在无锁编程中看到了它,但它在这里并不是一件好事。 Thread#join 将起作用,或者您可以使用 CountdownLatch。

请注意,使用 Thread#interrupted() 会清除中断标志。通常在你即将抛出InterruptedException时使用,否则最好使用Thread.currentThread().isInterrupted()。在这种特定情况下它不会造成任何伤害,因为 while 循环测试是唯一使用标志的东西,所以它是否被清除是无关紧要的。