Java 8 併發: 原子變數和 ConcurrentMap

Simeone_xu發表於2018-02-04

原文地址: 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

LongAccumulatorLongAdder 的一個更通用的版本。它不是執行簡單的新增操作, 類 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() 接受一個型別為 BiConsumerlambda 表示式, 並將 mapkeyvalue 作為引數傳遞。

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() 方法返回給定 keyvalue, 當 key 不存在時返回給定的值。

String value = map.getOrDefault("hi", "there");
System.out.println(value);    // there
複製程式碼

replaceAll() 方法接受一個 BiFunction 型別的 lambda 表示式, 並將 keyvalue 作為引數傳遞,用來更新 value

map.replaceAll((key, value) -> "r2".equals(key) ? "d3" : value);
System.out.println(map.get("r2"));    // d3
複製程式碼

compute() 方法和 replaceAll() 方法有些相同, 不同的是它多一個引數, 用來更新指定 keyvalue

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, searchreduce。 每個操作都有四種形式, 分別用 key, value, entrieskey-value 來作為引數。

所有這些方法的第一個引數都是 parallelismThreshold 閥值。 該閾值表示操作並行執行時的最小收集大小。 例如, 如果傳遞的閾值為500,並且 map 的實際大小為499, 則操作將在單個執行緒上按順序執行。 在下面的例子中,我們使用一個閾值來強制並行操作。

ForEach

方法 forEach() 能夠並行地迭代 map 的鍵值對。 BiConsumer 型別的 lambda 表示式接受當前迭代的 keyvalue。 為了視覺化並行執行,我們將當前執行緒名稱列印到控制檯。 請記住,在我的情況下,底層的 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() 方法接受兩個型別為 BiFunctionlambda 表示式。 第一個函式將每個鍵值對轉換為任何型別的單個值。 第二個函式將所有這些轉換後的值組合成一個結果, 其中火忽略 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
複製程式碼

相關文章