主线程随机没有结束(试图在并发线程中求和自然数)
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 Thread
的 join()
方法让您的主线程等待它们中的每一个都结束。
例如:
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 循环测试是唯一使用标志的东西,所以它是否被清除是无关紧要的。
我有下面的代码求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 Thread
的 join()
方法让您的主线程等待它们中的每一个都结束。
例如:
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 循环测试是唯一使用标志的东西,所以它是否被清除是无关紧要的。