Java 使用 join() 的多线程程序在计算相邻数字之和时给出了错误的结果
Java multi-threaded program using join() gives wrong results in calculating sum of adjacent numbers
我编写了这个简单的多线程程序来将 1 到 100,000 的数字相加。当我 运行 这个时,我得到了不同的值作为最终结果(值小于预期的 5000050000 )。当我只使用一个线程执行程序时,它给出了正确的结果。程序也适用于较小的值,例如 100。可能出了什么问题?提前致谢。
class Calculation {
private long total=0;
public void calcSum(long start, long end) {
long i = start;
for( ;i<=end; i++) {
total += i;
}
}
public long getTotal() {
return total;
}
}
class CalculatorThread extends Thread{
private long start;
private long end;
private Calculation calc;
public CalculatorThread(long start, long end, Calculation calc) {
this.start = start;
this.end = end;
this.calc = calc;
}
@Override
public void run() {
calc.calcSum(start, end);
}
}
public class ParallelTest {
public static void main(String[] args) throws InterruptedException {
int start = 1;
int end = 100000;
Calculation calc = new Calculation();
CalculatorThread ct1 = new CalculatorThread(start, end/2 , calc);
CalculatorThread ct2 = new CalculatorThread( (end/2) + 1, end, calc);
ct1.start();
ct2.start();
ct1.join();
ct2.join();
System.out.println(calc.getTotal());
}
}
对共享可变状态的非同步访问通常不会很顺利。
在你的Calculation calc
中,有一个可变变量long total
。当您启动线程时:
CalculatorThread ct1 = new CalculatorThread(start, end/2 , calc);
CalculatorThread ct2 = new CalculatorThread( (end/2) + 1, end, calc);
您在这两个线程之间共享 calc
的可变状态。 calc
内部没有任何同步,因此线程只会在随机时间间隔内相互浪费内存。
这是一个工作版本:
class ParallelSum {
public static long calcSum(long start, long end) {
long total = 0;
for(long i = start; i < end; i++) {
total += i;
}
return total;
}
public static class CalculatorThread extends Thread {
private long result = 0;
private long start;
private long end;
public CalculatorThread(long start, long end) {
this.start = start;
this.end = end;
}
@Override
public void run() {
result = calcSum(start, end);
}
public long getResult() {
return result;
}
}
public static void main(String[] args) throws InterruptedException {
int start = 1;
int end = 100000;
int endExcl = end + 1;
CalculatorThread ct1 = new CalculatorThread(start, endExcl/2);
CalculatorThread ct2 = new CalculatorThread(endExcl / 2, endExcl);
ct1.start();
ct2.start();
ct1.join();
ct2.join();
System.out.println(ct1.getResult() + ct2.getResult());
}
}
输出:
5000050000
附加说明:始终使用 [inclusive, exclusive)
范围索引。这大大降低了差一错误的可能性。另外,我用一个方法替换了 Calculation
class:方法内部的局部变量不会出错,可变状态越少越好。
total += i
不是原子的(参见 )。你有两个线程同时修改同一个值total
,两个线程互相干扰
看看 AtomicLong
(javadoc),它有一个方法 addAndGet
自动添加一些数字:
class Calculation {
private AtomicLong total = new AtomicLong();
public void calcSum(long start, long end) {
long i = start;
for( ;i<=end; i++) {
total.addAndGet(i);
}
}
public long getTotal() {
return total.get();
}
}
使用这种方法,两个线程都可以自动添加到 total
,因此它们不会再干扰。
class Calculation
不是线程安全的,您在两个线程上使用同一个实例。所以每个线程都在替换另一个线程设置的值。
一个简单的解决方案是创建两个 Calculation
实例,每个实例对应 CalculatorThread
。
Calculation calc1 = new Calculation();
Calculation calc2 = new Calculation();
CalculatorThread ct1 = new CalculatorThread(start, end/2 , calc1);
CalculatorThread ct2 = new CalculatorThread( (end/2) + 1, end, calc2);
这是一个 classic 计算,可以分解为独立的部分(可以并发执行)并在最后减少为单个结果。
这不是实现结果的最简单方法。如果你想计算从 1 到 N 的数字之和,你应该使用这个公式:
从 1 到 N = N(N+1)/2 的总和。您正面临所谓的不一致阅读。您的两个线程共享同一个字段 "calc",特别是计算的长字段总数 class。
使用您的解决方案,可能会发生这种情况:
trhead1 读取总数等于0,
thread1 将 total 的值增加到 1,
thread2 读取总数等于0,
thread2 将 total 的值增加到 1.
您的解决方案也可能出现其他情况,事实是这样总计的值不是predictible.So使用提供的公式或简单的 for,不要对同步问题使用异步解决方案.
你的代码的根本问题是 Calculation
实际上是一个共享变量(count
字段)的包装器,它在没有任何同步的情况下被更新。
解决这个问题的方法有以下三种:
- 同步变量;例如通过在适当的点使用
synchronized
- 为变量使用原子类型
- 不要使用共享变量。
从性能的角度来看,第三个选项更好,而且实现起来很简单;请参阅@JenniferP 的回答。
请注意,如果您为每个线程提供自己的 Calculation
对象,则不需要额外的同步。与 Thread::start()
和 Thread::join()
调用关联的 happens before 关系确保主线程在子线程之后读取子线程时看到正确的结果加入。
就其价值而言,Calculation
的线程安全实现将在共享实例时运行,如下所示:
class Calculation {
private long total = 0;
public void calcSum(long start, long end) {
for (long i = start; i <= end; i++) {
increment(i);
}
}
public synchronized long getTotal() {
return total;
}
private synchronized void increment(long by) {
total += by;
}
}
请注意,您需要同步 getter 和增量方法。另一种方法是使用 AtomicLong
来表示总数。但在这两种情况下,由于使用共享变量,都会产生大量开销。 (在 synchronized
的版本中,由于锁争用,开销可能会很大。如果与单线程版本相比有任何加速,我会感到惊讶。即使你可以消除线程创建开销。 )
我编写了这个简单的多线程程序来将 1 到 100,000 的数字相加。当我 运行 这个时,我得到了不同的值作为最终结果(值小于预期的 5000050000 )。当我只使用一个线程执行程序时,它给出了正确的结果。程序也适用于较小的值,例如 100。可能出了什么问题?提前致谢。
class Calculation {
private long total=0;
public void calcSum(long start, long end) {
long i = start;
for( ;i<=end; i++) {
total += i;
}
}
public long getTotal() {
return total;
}
}
class CalculatorThread extends Thread{
private long start;
private long end;
private Calculation calc;
public CalculatorThread(long start, long end, Calculation calc) {
this.start = start;
this.end = end;
this.calc = calc;
}
@Override
public void run() {
calc.calcSum(start, end);
}
}
public class ParallelTest {
public static void main(String[] args) throws InterruptedException {
int start = 1;
int end = 100000;
Calculation calc = new Calculation();
CalculatorThread ct1 = new CalculatorThread(start, end/2 , calc);
CalculatorThread ct2 = new CalculatorThread( (end/2) + 1, end, calc);
ct1.start();
ct2.start();
ct1.join();
ct2.join();
System.out.println(calc.getTotal());
}
}
对共享可变状态的非同步访问通常不会很顺利。
在你的Calculation calc
中,有一个可变变量long total
。当您启动线程时:
CalculatorThread ct1 = new CalculatorThread(start, end/2 , calc);
CalculatorThread ct2 = new CalculatorThread( (end/2) + 1, end, calc);
您在这两个线程之间共享 calc
的可变状态。 calc
内部没有任何同步,因此线程只会在随机时间间隔内相互浪费内存。
这是一个工作版本:
class ParallelSum {
public static long calcSum(long start, long end) {
long total = 0;
for(long i = start; i < end; i++) {
total += i;
}
return total;
}
public static class CalculatorThread extends Thread {
private long result = 0;
private long start;
private long end;
public CalculatorThread(long start, long end) {
this.start = start;
this.end = end;
}
@Override
public void run() {
result = calcSum(start, end);
}
public long getResult() {
return result;
}
}
public static void main(String[] args) throws InterruptedException {
int start = 1;
int end = 100000;
int endExcl = end + 1;
CalculatorThread ct1 = new CalculatorThread(start, endExcl/2);
CalculatorThread ct2 = new CalculatorThread(endExcl / 2, endExcl);
ct1.start();
ct2.start();
ct1.join();
ct2.join();
System.out.println(ct1.getResult() + ct2.getResult());
}
}
输出:
5000050000
附加说明:始终使用 [inclusive, exclusive)
范围索引。这大大降低了差一错误的可能性。另外,我用一个方法替换了 Calculation
class:方法内部的局部变量不会出错,可变状态越少越好。
total += i
不是原子的(参见 total
,两个线程互相干扰
看看 AtomicLong
(javadoc),它有一个方法 addAndGet
自动添加一些数字:
class Calculation {
private AtomicLong total = new AtomicLong();
public void calcSum(long start, long end) {
long i = start;
for( ;i<=end; i++) {
total.addAndGet(i);
}
}
public long getTotal() {
return total.get();
}
}
使用这种方法,两个线程都可以自动添加到 total
,因此它们不会再干扰。
class Calculation
不是线程安全的,您在两个线程上使用同一个实例。所以每个线程都在替换另一个线程设置的值。
一个简单的解决方案是创建两个 Calculation
实例,每个实例对应 CalculatorThread
。
Calculation calc1 = new Calculation();
Calculation calc2 = new Calculation();
CalculatorThread ct1 = new CalculatorThread(start, end/2 , calc1);
CalculatorThread ct2 = new CalculatorThread( (end/2) + 1, end, calc2);
这是一个 classic 计算,可以分解为独立的部分(可以并发执行)并在最后减少为单个结果。
这不是实现结果的最简单方法。如果你想计算从 1 到 N 的数字之和,你应该使用这个公式: 从 1 到 N = N(N+1)/2 的总和。您正面临所谓的不一致阅读。您的两个线程共享同一个字段 "calc",特别是计算的长字段总数 class。 使用您的解决方案,可能会发生这种情况:
trhead1 读取总数等于0, thread1 将 total 的值增加到 1, thread2 读取总数等于0, thread2 将 total 的值增加到 1.
您的解决方案也可能出现其他情况,事实是这样总计的值不是predictible.So使用提供的公式或简单的 for,不要对同步问题使用异步解决方案.
你的代码的根本问题是 Calculation
实际上是一个共享变量(count
字段)的包装器,它在没有任何同步的情况下被更新。
解决这个问题的方法有以下三种:
- 同步变量;例如通过在适当的点使用
synchronized
- 为变量使用原子类型
- 不要使用共享变量。
从性能的角度来看,第三个选项更好,而且实现起来很简单;请参阅@JenniferP 的回答。
请注意,如果您为每个线程提供自己的 Calculation
对象,则不需要额外的同步。与 Thread::start()
和 Thread::join()
调用关联的 happens before 关系确保主线程在子线程之后读取子线程时看到正确的结果加入。
就其价值而言,Calculation
的线程安全实现将在共享实例时运行,如下所示:
class Calculation {
private long total = 0;
public void calcSum(long start, long end) {
for (long i = start; i <= end; i++) {
increment(i);
}
}
public synchronized long getTotal() {
return total;
}
private synchronized void increment(long by) {
total += by;
}
}
请注意,您需要同步 getter 和增量方法。另一种方法是使用 AtomicLong
来表示总数。但在这两种情况下,由于使用共享变量,都会产生大量开销。 (在 synchronized
的版本中,由于锁争用,开销可能会很大。如果与单线程版本相比有任何加速,我会感到惊讶。即使你可以消除线程创建开销。 )