2

我有下面的代码来找到从 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(),我的问题就不会再发生了。

谁能解释我这种行为?

提前感谢您的帮助

4

2 回答 2

3

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

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

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 循环测试是唯一使用该标志的东西,因此它是否被清除是无关紧要的。

于 2015-09-11T14:17:34.277 回答
2

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

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

例如:

for(Thread t: threads) {
    try{
        t.join();
    } catch(InterruptedException e) {}
}
于 2015-09-11T12:54:40.307 回答