简体   繁体   English

Volatile 没有按预期工作

[英]Volatile doesn't work as expected

So I'm reading Brian Goetz' JCIP and wrote a following code for experimenting with volatile behavior.所以我正在阅读 Brian Goetz 的 JCIP 并编写了以下代码来试验volatile性行为。

public class StatefulObject {

    private static final int NUMBER_OF_THREADS = 10;

    private volatile State state;

    public StatefulObject() {
        state = new State();
    }

    public State getState() {
        return state;
    }

    public void setState(State state) {
        this.state = state;
    }

    public static class State {
        private volatile AtomicInteger counter;

        public State() {
            counter = new AtomicInteger();
        }

        public AtomicInteger getCounter() {
            return counter;
        }

        public void setCounter(AtomicInteger counter) {
            this.counter = counter;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        StatefulObject object = new StatefulObject();

        ExecutorService executorService = Executors.newFixedThreadPool(NUMBER_OF_THREADS);

        AtomicInteger oldCounter = new AtomicInteger();
        AtomicInteger newCounter = new AtomicInteger();

        object.getState().setCounter(oldCounter);

        ConcurrentMap<Integer, Long> lastSeen = new ConcurrentHashMap<>();
        ConcurrentMap<Integer, Long> firstSeen = new ConcurrentHashMap<>();
        lastSeen.put(oldCounter.hashCode(), 0L);
        firstSeen.put(newCounter.hashCode(), Long.MAX_VALUE);

        List<Future> futures = IntStream.range(0, NUMBER_OF_THREADS)
            .mapToObj(num -> executorService.submit(() -> {
                for (int i = 0; i < 1000; i++) {
                    object.getState().getCounter().incrementAndGet();
                    lastSeen.computeIfPresent(object.getState().getCounter().hashCode(), (key, oldValue) -> Math.max(oldValue, System.nanoTime()));
                    firstSeen.computeIfPresent(object.getState().getCounter().hashCode(), (key, oldValue) -> Math.min(oldValue, System.nanoTime()));
                }
            })).collect(Collectors.toList());

        executorService.shutdown();

        object.getState().setCounter(newCounter);

        futures.forEach(future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        System.out.printf("Counter: %s\n", object.getState().getCounter().get());
        long lastSeenOld = lastSeen.get(oldCounter.hashCode());
        long firstSeenNew = firstSeen.get(newCounter.hashCode());
        System.out.printf("Last seen old counter: %s\n", lastSeenOld);
        System.out.printf("First seen new counter: %s\n", firstSeenNew);
        System.out.printf("Old was seen after the new: %s\n", lastSeenOld > firstSeenNew);
        System.out.printf("Old was seen %s nanoseconds after the new\n", lastSeenOld - firstSeenNew);
    }
}

So I'm expecting that newCounter is always first seen only after oldCounter was last seen (I expect all threads to notice the update so none is referencing the stale counter).所以我期望newCounter总是在oldCounter上次被看到之后oldCounter一次看到(我希望所有线程都注意到更新,所以没有人引用过时的计数器)。 To observe this behavior I use two maps.为了观察这种行为,我使用了两张地图。 But surprisingly, I constantly get output like this:但令人惊讶的是,我不断得到这样的输出:

Counter: 9917
Last seen old counter: 695372684800871
First seen new counter: 695372684441226
Old was seen after the update: true
Old was seen 359645 nanoseconds after the new

Can you please explain where I'm wrong?你能解释一下我错在哪里吗?

Thanks in advance!提前致谢!

The reason behind your observation is not a bug in java ;) but there is one in your code.您的观察背后的原因不是 java 中的错误;) 但您的代码中有一个错误。 In your code you cannot guarantee that invocation of computeIfPresent for lastseen and firstSeen maps executed atomically (refer to the Javadocs, computeIfPresent is not atomic).在你的代码不能保证该调用computeIfPresentlastseenfirstSeen映射自动执行(参考JavaDocs, computeIfPresent不是原子)。 What this means is there is time gap between when you gets object.getState().getCounter() and actually updates the map.这意味着在您获取object.getState().getCounter()和实际更新地图之间存在时间间隔。

If setting newCounter happens while thread A in this gap (before getting the nanotime but already got the counter reference - old) and Thread B at just before getting object.getState().getCounter() .如果设置newCounter时线程 A 在此间隙(在获得纳米时间之前但已经获得计数器引用 - 旧)和线程 B 在获得object.getState().getCounter() So if this exact moment counter reference got updated, Thread A will update the old counter key while Thread B will update the new.所以如果这个确切的时刻计数器引用被更新,线程 A 将更新旧的计数器键,而线程 B 将更新新的。 If Thread B took nanotime before Thread A (this could happen because these are separated threads which we cannot know what are the actual cpu scheduling) that could perfectly lead to your observation.如果线程 B 在线程 A 之前花费了纳米时间(这可能发生,因为这些是分离的线程,我们无法知道实际的 cpu 调度是什么),这可能会完全导致您的观察。

I think my explanation is clear.我想我的解释很清楚。 One more thing to clarify, in State class, you have declared AtomicInteger counter as volatile as well.还有一件事要澄清,在State类中,您已经将AtomicInteger counter声明为 volatile 。 This is not needed since a AtomicInteger is inherently is volatile.这不是必需的,因为 AtomicInteger 本质上是易变的。 There are no "non-volatile" Atomic** s.没有“非易失性” Atomic** s。

I just changed few things in your code to omit the above mentioned issues :我只是在您的代码中更改了一些内容以省略上述问题:

import java.util.Collections;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class StatefulObject {

    private static final int NUMBER_OF_THREADS = 10;

    private volatile State state;

    public StatefulObject() {
        state = new State();
    }

    public State getState() {
        return state;
    }

    public void setState(State state) {
        this.state = state;
    }

    public static class State {
        private volatile AtomicInteger counter;

        public State() {
            counter = new AtomicInteger();
        }

        public AtomicInteger getCounter() {
            return counter;
        }

        public void setCounter(AtomicInteger counter) {
            this.counter = counter;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        StatefulObject object = new StatefulObject();

        ExecutorService executorService = Executors.newFixedThreadPool(NUMBER_OF_THREADS);

        AtomicInteger oldCounter = new AtomicInteger();
        AtomicInteger newCounter = new AtomicInteger();

        object.getState().setCounter(oldCounter);

        List<Long> oldList = new CopyOnWriteArrayList<>();
        List<Long> newList = new CopyOnWriteArrayList<>();

        List<Future> futures = IntStream.range(0, NUMBER_OF_THREADS)
            .mapToObj(num -> executorService.submit(() -> {
                for (int i = 0; i < 1000; i++) {
                    long l = System.nanoTime();
                    object.getState().getCounter().incrementAndGet();
                    if (object.getState().getCounter().equals(oldCounter)) {
                        oldList.add(l);
                    } else {
                        newList.add(l);
                    }
                }
            })).collect(Collectors.toList());

        executorService.shutdown();

        object.getState().setCounter(newCounter);

        futures.forEach(future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        System.out.printf("Counter: %s\n", object.getState().getCounter().get());
        Collections.sort(oldList);
        Collections.sort(newList);
        long lastSeenOld = oldList.get(oldList.size() - 1);
        long firstSeenNew = newList.get(0);
        System.out.printf("Last seen old counter: %s\n", lastSeenOld);
        System.out.printf("First seen new counter: %s\n", firstSeenNew);
        System.out.printf("Old was seen after the new: %s\n", lastSeenOld > firstSeenNew);
        System.out.printf("Old was seen %s nanoseconds after the new\n", lastSeenOld - firstSeenNew);
    }
}

What you are seeing are not the effects of volatile but the effects of the synchronization on the ConcurrentMap<> lastSeen .您看到的不是 volatile 的影响,而是同步对ConcurrentMap<> lastSeen

Let's assume that all ten threads started at about the same time.假设所有十个线程几乎同时启动。 Each one does object.getState().getCounter().incrementAndGet();每个都做object.getState().getCounter().incrementAndGet(); almost in parallel, thereby incrementing the oldCounter .几乎并行,从而增加oldCounter

Next, those threads try to execute lastSeen.computeIfPresent(object.getState().getCounter().hashCode(), (key, oldValue) -> Math.max(oldValue, System.nanoTime()));接下来,这些线程尝试执行lastSeen.computeIfPresent(object.getState().getCounter().hashCode(), (key, oldValue) -> Math.max(oldValue, System.nanoTime())); . . That means, they all evaluate object.getState().getCounter().hashCode() in parallel, each one getting the same hashcode of the oldCounter , then call ConcurrentHashMap.computeIfPresent(Integer, ..) with the same hash value.这意味着,它们都并行地评估object.getState().getCounter().hashCode() ,每个都获得oldCounter的相同哈希oldCounter ,然后使用相同的哈希值调用ConcurrentHashMap.computeIfPresent(Integer, ..)

Since all those threads try to update the value for the same key the ConcurrentHashMap has to synchronize these updates - only .由于所有这些线程都尝试更新相同键的值,因此ConcurrentHashMap必须同步这些更新 - 仅。

During the time that the first thread is updating lastSeen the main thread executes object.getState().setCounter(newCounter);在第一个线程更新lastSeen ,主线程执行object.getState().setCounter(newCounter); , so the first thread will execute firstSeen for newCounter , while several threads are still waiting to update lastSeen . ,所以第一个线程将执行firstSeennewCounter ,而多个线程仍在等待更新lastSeen


To get better results, it would be better to separate the information gathering steps from the analyze steps.为了获得更好的结果,最好将信息收集步骤与分析步骤分开。

For example, the threads could capture the counters hashcode and the timestamp of update into arrays that you analyze after all computations have been done.例如,线程可以将计数器哈希码和更新时间戳捕获到您在所有计算完成后分析的数组中。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM