[英]Why doesn't the volatile keyword work as expected in java code?
[英]Volatile doesn't work as expected
所以我正在閱讀 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);
}
}
所以我期望newCounter
總是在oldCounter
上次被看到之后oldCounter
一次看到(我希望所有線程都注意到更新,所以沒有人引用過時的計數器)。 為了觀察這種行為,我使用了兩張地圖。 但令人驚訝的是,我不斷得到這樣的輸出:
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
你能解釋一下我錯在哪里嗎?
提前致謝!
您的觀察背后的原因不是 java 中的錯誤;) 但您的代碼中有一個錯誤。 在你的代碼不能保證該調用computeIfPresent
為lastseen
和firstSeen
映射自動執行(參考JavaDocs, computeIfPresent
不是原子)。 這意味着在您獲取object.getState().getCounter()
和實際更新地圖之間存在時間間隔。
如果設置newCounter
時線程 A 在此間隙(在獲得納米時間之前但已經獲得計數器引用 - 舊)和線程 B 在獲得object.getState().getCounter()
。 所以如果這個確切的時刻計數器引用被更新,線程 A 將更新舊的計數器鍵,而線程 B 將更新新的。 如果線程 B 在線程 A 之前花費了納米時間(這可能發生,因為這些是分離的線程,我們無法知道實際的 cpu 調度是什么),這可能會完全導致您的觀察。
我想我的解釋很清楚。 還有一件事要澄清,在State
類中,您已經將AtomicInteger counter
聲明為 volatile 。 這不是必需的,因為 AtomicInteger 本質上是易變的。 沒有“非易失性” Atomic** s。
我只是在您的代碼中更改了一些內容以省略上述問題:
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);
}
}
您看到的不是 volatile 的影響,而是同步對ConcurrentMap<> lastSeen
。
假設所有十個線程幾乎同時啟動。 每個都做object.getState().getCounter().incrementAndGet();
幾乎並行,從而增加oldCounter
。
接下來,這些線程嘗試執行lastSeen.computeIfPresent(object.getState().getCounter().hashCode(), (key, oldValue) -> Math.max(oldValue, System.nanoTime()));
. 這意味着,它們都並行地評估object.getState().getCounter().hashCode()
,每個都獲得oldCounter
的相同哈希oldCounter
,然后使用相同的哈希值調用ConcurrentHashMap.computeIfPresent(Integer, ..)
。
由於所有這些線程都嘗試更新相同鍵的值,因此ConcurrentHashMap
必須同步這些更新 - 僅。
在第一個線程更新lastSeen
,主線程執行object.getState().setCounter(newCounter);
,所以第一個線程將執行firstSeen
為newCounter
,而多個線程仍在等待更新lastSeen
。
為了獲得更好的結果,最好將信息收集步驟與分析步驟分開。
例如,線程可以將計數器哈希碼和更新時間戳捕獲到您在所有計算完成后分析的數組中。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.