原文地址: Java 8 Concurrency Tutorial: Atomic Variables and ConcurrentMap
AtomicInteger
java.concurrent.atomic
包下有很多原子操作的類。 在有些情況下,原子操作可以在不使用 synchronized
關鍵字和鎖的情況下解決多執行緒安全問題。
在內部,原子類大量使用 CAS
, 這是大多數現在 CPU 支援的原子操作指令, 這些指令通常情況下比鎖同步要快得多。如果需要同時改變一個變數, 使用原子類是極其優雅的。
現在選擇一個原子類 AtomicInteger
作為例子
AtomicInteger atomicInt = new AtomicInteger(0);
ExecutorService executor = Executors.newFixedThreadPool(2);
IntStream.range(0, 1000)
.forEach(i -> executor.submit(atomicInt::incrementAndGet));
stop(executor);
System.out.println(atomicInt.get()); // => 1000
複製程式碼
使用 AtomicInteger
代替 Integer
可以線上程安全的環境中增加變數, 而不要同步訪問變數。incrementAndGet()
方法是一個原子操作, 我們可以在多執行緒中安全的呼叫。
AtomicInteger
支援多種的原子操作, updateAndGet()
方法接受一個 lambda
表示式,以便對整數做任何的算術運算。
AtomicInteger atomicInt = new AtomicInteger(0);
ExecutorService executor = Executors.newFixedThreadPool(2);
IntStream.range(0, 1000)
.forEach(i -> {
Runnable task = () ->
atomicInt.updateAndGet(n -> n + 2);
executor.submit(task);
});
stop(executor);
System.out.println(atomicInt.get()); // => 2000
複製程式碼
accumulateAndGet()
方法接受一個 IntBinaryOperator
型別的另一種 lambda
表示式, 我們是用這種方法來計算 1 -- 999 的和:
AtomicInteger atomicInt = new AtomicInteger(0);
ExecutorService executor = Executors.newFixedThreadPool(2);
IntStream.range(0, 1000)
.forEach(i -> {
Runnable task = () ->
atomicInt.accumulateAndGet(i, (n, m) -> n + m);
executor.submit(task);
});
stop(executor);
System.out.println(atomicInt.get()); // => 499500
複製程式碼
還有一些其他的原子操作類: AtomicBoolean AtomicLong AtomicReference
LongAdder
作為 AtomicLong
的替代, LongAdder
類可以用來連續地向數字新增值。
ExecutorService executor = Executors.newFixedThreadPool(2);
IntStream.range(0, 1000)
.forEach(i -> executor.submit(adder::increment));
stop(executor);
System.out.println(adder.sumThenReset()); // => 1000
複製程式碼
LongAdder
類和其他的整數原子操作類一樣提供了 add()
和 increment()
方法, 同時也是執行緒安全的。但其內部的結果不是一個單一的值, 這個類的內部維護了一組變數來減少多執行緒的爭用。實際結果可以通過呼叫 sum()
和 sumThenReset()
來獲取。
當來自多執行緒的更新比讀取更頻繁時, 這個類往往優於其他的原子類。通常作為統計資料, 比如要統計 web 伺服器的請求數量。 LongAdder
的缺點是會消耗更多的記憶體, 因為有一組變數儲存在記憶體中。
LongAccumulator
LongAccumulator
是 LongAdder
的一個更通用的版本。它不是執行簡單的新增操作, 類 LongAccumulator
圍繞 LongBinaryOperator
型別的lambda表示式構建,如程式碼示例中所示:
LongBinaryOperator op = (x, y) -> 2 * x + 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);
System.out.println(accumulator.getThenReset()); // => 2539
複製程式碼
我們使用函式 2 * x + y
和初始值1
建立一個 LongAccumulator
。 每次呼叫 accumulate(i)
, 當前結果和值i
都作為引數傳遞給``lambda` 表示式。
像 LongAdder
一樣, LongAccumulator
在內部維護一組變數以減少對執行緒的爭用。
ConcurrentMap
ConcurrentMap
介面擴充套件了 Map
介面,並定義了最有用的併發集合型別之一。 Java 8
通過向此介面新增新方法引入了函數語言程式設計。
在下面的程式碼片段中, 來演示這些新的方法:
ConcurrentMap<String, String> map = new ConcurrentHashMap<>();
map.put("foo", "bar");
map.put("han", "solo");
map.put("r2", "d2");
map.put("c3", "p0");
複製程式碼
forEach()
接受一個型別為 BiConsumer
的 lambda
表示式, 並將 map
的 key
和 value
作為引數傳遞。
map.forEach((key, value) -> System.out.printf("%s = %s\n", key, value));
複製程式碼
putIfAbsent()
方法只有當給定的 key
不存在時才將資料存入 map
中, 這個方法和 put
一樣是執行緒安全的, 當多個執行緒訪問 map
時不要做同步操作。
String value = map.putIfAbsent("c3", "p1");
System.out.println(value); // p0
複製程式碼
getOrDefault()
方法返回給定 key
的 value
, 當 key
不存在時返回給定的值。
String value = map.getOrDefault("hi", "there");
System.out.println(value); // there
複製程式碼
replaceAll()
方法接受一個 BiFunction
型別的 lambda
表示式, 並將 key
和 value
作為引數傳遞,用來更新 value
。
map.replaceAll((key, value) -> "r2".equals(key) ? "d3" : value);
System.out.println(map.get("r2")); // d3
複製程式碼
compute()
方法和 replaceAll()
方法有些相同, 不同的是它多一個引數, 用來更新指定 key
的 value
map.compute("foo", (key, value) -> value + value);
System.out.println(map.get("foo")); // barbar
複製程式碼
ConcurrentHashMap
以上所有方法都是 ConcurrentMap
介面的一部分,因此可用於該介面的所有實現。 此外,最重要的實現 ConcurrentHashMap
已經進一步增強了一些新的方法來在 Map
上執行併發操作。
就像並行流一樣,這些方法在 Java 8
中通過 ForkJoinPool.commonPool()
提供特殊的 ForkJoinPool
。該池使用預設的並行性, 這取決於可用核心的數量。 我的機器上有四個CPU核心可以實現三種並行性:
System.out.println(ForkJoinPool.getCommonPoolParallelism()); // 3
複製程式碼
通過設定以下 JVM
引數可以減少或增加此值:
-Djava.util.concurrent.ForkJoinPool.common.parallelism=5
複製程式碼
我們使用相同的示例來演示, 不過下面使用 ConcurrentHashMap
型別, 這樣可以呼叫更多的方法。
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("foo", "bar");
map.put("han", "solo");
map.put("r2", "d2");
map.put("c3", "p0");
複製程式碼
Java 8
引入了三種並行操作:forEach
, search
和 reduce
。 每個操作都有四種形式, 分別用 key
, value
, entries
和 key-value
來作為引數。
所有這些方法的第一個引數都是 parallelismThreshold
閥值。 該閾值表示操作並行執行時的最小收集大小。 例如, 如果傳遞的閾值為500
,並且 map
的實際大小為499
, 則操作將在單個執行緒上按順序執行。 在下面的例子中,我們使用一個閾值來強制並行操作。
ForEach
方法 forEach()
能夠並行地迭代 map
的鍵值對。 BiConsumer
型別的 lambda
表示式接受當前迭代的 key
和 value
。 為了視覺化並行執行,我們將當前執行緒名稱列印到控制檯。 請記住,在我的情況下,底層的 ForkJoinPool
最多使用三個執行緒。
map.forEach(1, (key, value) ->
System.out.printf("key: %s; value: %s; thread: %s\n",
key, value, Thread.currentThread().getName()));
// key: r2; value: d2; thread: main
// key: foo; value: bar; thread: ForkJoinPool.commonPool-worker-1
// key: han; value: solo; thread: ForkJoinPool.commonPool-worker-2
// key: c3; value: p0; thread: main
複製程式碼
Search
search()
方法接受一個 BiFunction
型別的 lambda
表示式, 它能對 map
做搜尋操作, 如果當前迭代不符合所需的搜尋條件,則返回 null
。 請記住,ConcurrentHashMap
是無序的。 搜尋功能不應該取決於地圖的實際處理順序。 如果有多個匹配結果, 則結果可能是不確定的。
String result = map.search(1, (key, value) -> {
System.out.println(Thread.currentThread().getName());
if ("foo".equals(key)) {
return value;
}
return null;
});
System.out.println("Result: " + result);
// ForkJoinPool.commonPool-worker-2
// main
// ForkJoinPool.commonPool-worker-3
// Result: bar
複製程式碼
下面是對 value 的搜尋
String result = map.searchValues(1, value -> {
System.out.println(Thread.currentThread().getName());
if (value.length() > 3) {
return value;
}
return null;
});
System.out.println("Result: " + result);
// ForkJoinPool.commonPool-worker-2
// main
// main
// ForkJoinPool.commonPool-worker-1
// Result: solo
複製程式碼
Reduce
reduce()
方法接受兩個型別為 BiFunction
的 lambda
表示式。 第一個函式將每個鍵值對轉換為任何型別的單個值。 第二個函式將所有這些轉換後的值組合成一個結果, 其中火忽略 null
值。
String result = map.reduce(1,
(key, value) -> {
System.out.println("Transform: " + Thread.currentThread().getName());
return key + "=" + value;
},
(s1, s2) -> {
System.out.println("Reduce: " + Thread.currentThread().getName());
return s1 + ", " + s2;
});
System.out.println("Result: " + result);
// Transform: ForkJoinPool.commonPool-worker-2
// Transform: main
// Transform: ForkJoinPool.commonPool-worker-3
// Reduce: ForkJoinPool.commonPool-worker-3
// Transform: main
// Reduce: main
// Reduce: main
// Result: r2=d2, c3=p0, han=solo, foo=bar
複製程式碼