LongAccumulator无法获得正确的结果

时间:2017-04-13 10:59:03

标签: java multithreading concurrency java-8

代码优先:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.function.LongBinaryOperator;
import java.util.stream.IntStream;

/**
 * Created by tom on 17-4-13.
 */
public class ForSOF {
    public static void main(String[] args){
        LongBinaryOperator op = (v, y) -> (v*2 + y);
        LongAccumulator accumulator = new LongAccumulator(op, 1L);

        ExecutorService executor = Executors.newFixedThreadPool(2);

        IntStream.range(0, 10)
                .forEach(i -> executor.submit(() -> accumulator.accumulate(i)));

        stop(executor);

        // 2539 expected, however result does not always be! I had got 2037 before.
        System.out.println(accumulator.getThenReset());
    }

    /**
     * codes for stop the executor, it's insignificant for my issue.
     */
    public static void stop(ExecutorService executor) {
        try {
            executor.shutdown();
            executor.awaitTermination(60, TimeUnit.SECONDS);
        }
        catch (InterruptedException e) {
            System.err.println("termination interrupted");
        }
        finally {
            if (!executor.isTerminated()) {
                System.err.println("killing non-finished tasks");
            }
            executor.shutdownNow();
        }
    }
}

以上是关于测试LongAccumulator类Java 8的崩溃的全部代码。来自博客winterbe.com的代码以及关于java8的好旅游文章。

正如API文档所说,包java.util.concurrent.atomic中那些类的方法将是原子的和线程安全的。但是,当我运行上述内容时,肯定会有结果。有时候会出现2037年或1244年的错误。一定有什么问题。

当我将主体内的第一行重写为:

LongBinaryOperator op = (v, y) -> (v + y);

它以正确的方式运作。

所以,我猜想当JVM得到v并加倍时,其他线程有中断并获得相同的v值,计算op表达式并给出一个结果,该结果很快就被前线程重写。这就是表达v + y的原因。

如果应该改进这些代码中的任何一个? 还是LongAccumulator类的错误? 我不确定。

天啊!这些代码的逻辑应该是这样的:

public void test(){
    int v = 1;
    for (int i = 0; i < 10; i++) {
        v = x(v, i);
    }
    System.out.println(v);
}

int x(int v, int y){
    return v*2+y;
}

,v的输出是2037。

所以让我困惑的一定是计算流的混乱。

3 个答案:

答案 0 :(得分:5)

  

正如API文档所说,java.util.concurrent.atomic包中的那些类的方法将是原子和线程安全的。但是,当我运行上述内容时,肯定会有结果。 2037年或1244年的时间错误。必定有问题。

LongAccumulator docs提供了一条重要信息。

  

线程内或线程之间的累积顺序无法保证且不能依赖,因此该类仅适用于累积顺序无关紧要的函数。

您的问题是v + y操作是noncommutative。如果混合操作的顺序,您可以获得各种输出。 List<Long> list = new ArrayList<>(); for (long i = 0; i < 10; i++) { list.add(i); } // if you uncomment this, you get varying results // Collections.shuffle(list); long total = 1; for (Long i : list) { total = total * 2 + i; } System.out.println(total); 有效的原因是您可以按任何顺序应用它并获得相同的结果。

要将其分解为串行代码,您实际上是这样做的:

Collections.shuffle(...)

有序结果是2037,但是如果你取消注释if(string.equals(string2) == true);,那么你会发现当数字的顺序相反时,结果会因最大值9218而变化。

答案 1 :(得分:2)

更新:下面描述的问题已被接受为错误:JDK-8178956

由于这是一个关于LongAccumulator是否有错误的问题,我想指出它是,或者更确切地说文档可能是错误的。

首先,文件明确指出:

  

线程内或线程之间的累积顺序无法保证且不能依赖,因此该类仅适用于累积顺序无关紧要的函数。

问题中使用的函数((v, y) -> (v*2 + y))明显违反了该函数,因为调用accumulate()的顺序很重要。

如果我们在数学形式中添加更多内容,如果我们累积了两个值a1a2,那么文档会明确指出结果可以是以下任何一种:

result = f( f(identity, a1), a2)
result = f( f(identity, a2), a1)

文档还说明:

  

提供的累加器功能应该是无副作用的,因为当尝试的更新由于线程之间的争用而失败时,它可能会被重新应用。

这意味着f(identity, a1)可能会被调用两次,但其中一个调用的结果将被丢弃。或者调用了f(identity, a1)f(identity, a2),但是只使用了其中一个,如上所示,另一个被丢弃。

但是,文档说:

  

该函数应用当前值作为其第一个参数,并将给定更新作为第二个参数。

上面显示的是错误,因为结果也可能像这样计算,明显违反了最后一个声明:

result = f( identity, f(a1, a2) )

其中内部调用的第一个参数是而不是&#34;当前值&#34;累加器,外部调用的第二个参数是&#34;给定更新&#34;值。

为了看到这一点,我将问题代码修改为:

LongBinaryOperator op = (v, y) -> {
    try { Thread.sleep(200); } catch (InterruptedException ignored) {}
    long result = (v + y);
    System.out.printf("f(%d, %d) = %d%n", v, y, result);
    return result;
};
LongAccumulator accumulator = new LongAccumulator(op, 1L);

ExecutorService executor = Executors.newFixedThreadPool(2);

Arrays.asList(2, 3, 5, 11, 17, 23, 29, 37, 41, 47).stream()
        .forEach(i -> executor.submit(() -> accumulator.accumulate(i)));

executor.shutdown();
executor.awaitTermination(60, TimeUnit.SECONDS);

System.out.println(accumulator.getThenReset());

运行它会产生这个结果(每次执行时结果都不同):

f(1, 3) = 4
f(1, 2) = 3
f(2, 11) = 13
f(4, 5) = 9
f(13, 17) = 30
f(23, 29) = 52
f(30, 37) = 67
f(52, 41) = 93
f(67, 47) = 114
f(9, 93) = 102
f(102, 114) = 216
216

为了说明如何调用函数来计算结果,我将通过对值应用名称对其进行注释,即id的{​​{1}}的初始身份值,{{1} } 1表示累积(输入)值,a0表示a9表示函数结果。请注意,r1被丢弃,这就是为什么有11个调用只有10个值。

r11

答案 2 :(得分:-1)

您必须更改代码,如下所示:

public static void main(String[] args) {
    LongBinaryOperator op = (v, y) -> (v + 2*y);
    LongAccumulator accumulator = new LongAccumulator(op, 1L);
    ExecutorService executor = Executors.newFixedThreadPool(4);
    IntStream.range(0, 100)
            .mapToObj(val -> executor.submit(() -> accumulator.accumulate(val)))
            .forEach(future -> { while (!future.isDone()); });
    executor.shutdown();
    System.out.println(accumulator.get());
}

首先它使op执行顺序独立,因为v表示已计算值的总和,y表示要为其计算新值的IntStream的值2*y

LongBinaryOperator op = (v, y) -> (v + 2*y);

其次,你必须等待每个提交的任务完成,这可以通过检查返回的Future来检测到:

forEach(future -> { while (!future.isDone()); });

现在它应该稳定运行。