摘要:并發(fā)教程原子變量和原文譯者飛龍協(xié)議歡迎閱讀我的多線程編程系列教程的第三部分。如果你能夠在多線程中同時(shí)且安全地執(zhí)行某個(gè)操作,而不需要關(guān)鍵字或上一章中的鎖,那么這個(gè)操作就是原子的。當(dāng)多線程的更新比讀取更頻繁時(shí),這個(gè)類通常比原子數(shù)值類性能更好。
Java 8 并發(fā)教程:原子變量和 ConcurrentMap
原文:Java 8 Concurrency Tutorial: Synchronization and Locks
譯者:飛龍
協(xié)議:CC BY-NC-SA 4.0
歡迎閱讀我的Java8多線程編程系列教程的第三部分。這個(gè)教程包含并發(fā)API的兩個(gè)重要部分:原子變量和ConcurrentMap。由于最近發(fā)布的Java8中的lambda表達(dá)式和函數(shù)式編程,二者都有了極大的改進(jìn)。所有這些新特性會(huì)以一些簡(jiǎn)單易懂的代碼示例來(lái)描述。希望你能喜歡。
第一部分:線程和執(zhí)行器
第二部分:同步和鎖
第三部分:原子變量和 ConcurrentMap
出于簡(jiǎn)單的因素,這個(gè)教程的代碼示例使用了定義在這里的兩個(gè)輔助函數(shù)sleep(seconds) 和 stop(executor)。
AtomicIntegerjava.concurrent.atomic包包含了許多實(shí)用的類,用于執(zhí)行原子操作。如果你能夠在多線程中同時(shí)且安全地執(zhí)行某個(gè)操作,而不需要synchronized關(guān)鍵字或上一章中的鎖,那么這個(gè)操作就是原子的。
本質(zhì)上,原子操作嚴(yán)重依賴于比較與交換(CAS),它是由多數(shù)現(xiàn)代CPU直接支持的原子指令。這些指令通常比同步塊要快。所以在只需要并發(fā)修改單個(gè)可變變量的情況下,我建議你優(yōu)先使用原子類,而不是上一章展示的鎖。
譯者注:對(duì)于其它語(yǔ)言,一些語(yǔ)言的原子操作用鎖實(shí)現(xiàn),而不是原子指令。
現(xiàn)在讓我們選取一個(gè)原子類,例如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
通過(guò)使用AtomicInteger代替Integer,我們就能線程安全地并發(fā)增加數(shù)值,而不需要同步訪問(wèn)變量。incrementAndGet()方法是原子操作,所以我們可以在多個(gè)線程中安全調(diào)用它。
AtomicInteger支持多種原子操作。updateAndGet()接受lambda表達(dá)式,以便在整數(shù)上執(zhí)行任意操作:
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表達(dá)式。我們?cè)谙聜€(gè)例子中,使用這個(gè)方法并發(fā)計(jì)算0~1000所有值的和:
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
其它實(shí)用的原子類有AtomicBoolean、AtomicLong 和 AtomicReference。
LongAdderLongAdder是AtomicLong的替代,用于向某個(gè)數(shù)值連續(xù)添加值。
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()方法,就像原子數(shù)值類一樣,同樣是線程安全的。但是這個(gè)類在內(nèi)部維護(hù)一系列變量來(lái)減少線程之間的爭(zhēng)用,而不是求和計(jì)算單一結(jié)果。實(shí)際的結(jié)果可以通過(guò)調(diào)用sum()或sumThenReset()來(lái)獲取。
當(dāng)多線程的更新比讀取更頻繁時(shí),這個(gè)類通常比原子數(shù)值類性能更好。這種情況在抓取統(tǒng)計(jì)數(shù)據(jù)時(shí)經(jīng)常出現(xiàn),例如,你希望統(tǒng)計(jì)Web服務(wù)器上請(qǐng)求的數(shù)量。LongAdder缺點(diǎn)是較高的內(nèi)存開銷,因?yàn)樗趦?nèi)存中儲(chǔ)存了一系列變量。
LongAccumulatorLongAccumulator是LongAdder的更通用的版本。LongAccumulator以類型為LongBinaryOperatorlambda表達(dá)式構(gòu)建,而不是僅僅執(zhí)行加法操作,像這段代碼展示的那樣:
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
我們使用函數(shù)2 * x + y創(chuàng)建了LongAccumulator,初始值為1。每次調(diào)用accumulate(i)的時(shí)候,當(dāng)前結(jié)果和值i都會(huì)作為參數(shù)傳入lambda表達(dá)式。
LongAccumulator就像LongAdder那樣,在內(nèi)部維護(hù)一系列變量來(lái)減少線程之間的爭(zhēng)用。
ConcurrentMapConcurrentMap接口繼承自Map接口,并定義了最實(shí)用的并發(fā)集合類型之一。Java8通過(guò)將新的方法添加到這個(gè)接口,引入了函數(shù)式編程。
在下面的代碼中,我們使用這個(gè)映射示例來(lái)展示那些新的方法:
ConcurrentMapmap = new ConcurrentHashMap<>(); map.put("foo", "bar"); map.put("han", "solo"); map.put("r2", "d2"); map.put("c3", "p0");
forEach()方法接受類型為BiConsumer的lambda表達(dá)式,以映射的鍵和值作為參數(shù)傳遞。它可以作為for-each循環(huán)的替代,來(lái)遍歷并發(fā)映射中的元素。迭代在當(dāng)前線程上串行執(zhí)行。
map.forEach((key, value) -> System.out.printf("%s = %s ", key, value));
新方法putIfAbsent()只在提供的鍵不存在時(shí),將新的值添加到映射中。至少在ConcurrentHashMap的實(shí)現(xiàn)中,這一方法像put()一樣是線程安全的,所以你在不同線程中并發(fā)訪問(wèn)映射時(shí),不需要任何同步機(jī)制。
String value = map.putIfAbsent("c3", "p1"); System.out.println(value); // p0
getOrDefault()方法返回指定鍵的值。在傳入的鍵不存在時(shí),會(huì)返回默認(rèn)值:
String value = map.getOrDefault("hi", "there"); System.out.println(value); // there
replaceAll()接受類型為BiFunction的lambda表達(dá)式。BiFunction接受兩個(gè)參數(shù)并返回一個(gè)值。函數(shù)在這里以每個(gè)元素的鍵和值調(diào)用,并返回要映射到當(dāng)前鍵的新值。
map.replaceAll((key, value) -> "r2".equals(key) ? "d3" : value); System.out.println(map.get("r2")); // d3
compute()允許我們轉(zhuǎn)換單個(gè)元素,而不是替換映射中的所有值。這個(gè)方法接受需要處理的鍵,和用于指定值的轉(zhuǎn)換的BiFunction。
map.compute("foo", (key, value) -> value + value); System.out.println(map.get("foo")); // barbar
除了compute()之外還有兩個(gè)變體:computeIfAbsent() 和 computeIfPresent()。這些方法的函數(shù)式參數(shù)只在鍵不存在或存在時(shí)被調(diào)用。
最后,merge()方法可以用于以映射中的現(xiàn)有值來(lái)統(tǒng)一新的值。這個(gè)方法接受鍵、需要并入現(xiàn)有元素的新值,以及指定兩個(gè)值的合并行為的BiFunction。
map.merge("foo", "boo", (oldVal, newVal) -> newVal + " was " + oldVal); System.out.println(map.get("foo")); // boo was fooConcurrentHashMap
所有這些方法都是ConcurrentMap接口的一部分,因此可在所有該接口的實(shí)現(xiàn)上調(diào)用。此外,最重要的實(shí)現(xiàn)ConcurrentHashMap使用了一些新的方法來(lái)改進(jìn),便于在映射上執(zhí)行并行操作。
就像并行流那樣,這些方法使用特定的ForkJoinPool,由Java8中的ForkJoinPool.commonPool()提供。該池使用了取決于可用核心數(shù)量的預(yù)置并行機(jī)制。我的電腦有四個(gè)核心可用,這會(huì)使并行性的結(jié)果為3:
System.out.println(ForkJoinPool.getCommonPoolParallelism()); // 3
這個(gè)值可以通過(guò)設(shè)置下列JVM參數(shù)來(lái)增減:
-Djava.util.concurrent.ForkJoinPool.common.parallelism=5
我們使用相同的映射示例來(lái)展示,但是這次我們使用具體的ConcurrentHashMap實(shí)現(xiàn)而不是ConcurrentMap接口,所以我們可以訪問(wèn)這個(gè)類的所有公共方法:
ConcurrentHashMapmap = new ConcurrentHashMap<>(); map.put("foo", "bar"); map.put("han", "solo"); map.put("r2", "d2"); map.put("c3", "p0");
Java8引入了三種類型的并行操作:forEach、search 和 reduce。這些操作中每個(gè)都以四種形式提供,接受以鍵、值、元素或鍵值對(duì)為參數(shù)的函數(shù)。
所有這些方法的第一個(gè)參數(shù)是通用的parallelismThreshold。這一閾值表示操作并行執(zhí)行時(shí)的最小集合大小。例如,如果你傳入閾值500,而映射的實(shí)際大小是499,那么操作就會(huì)在單線程上串行執(zhí)行。在下一個(gè)例子中,我們使用閾值1,始終強(qiáng)制并行執(zhí)行來(lái)展示。
forEachforEach()方法可以并行迭代映射中的鍵值對(duì)。BiConsumer以當(dāng)前迭代元素的鍵和值調(diào)用。為了將并行執(zhí)行可視化,我們向控制臺(tái)打印了當(dāng)前線程的名稱。要注意在我這里底層的ForkJoinPool最多使用三個(gè)線程。
map.forEach(1, (key, value) -> System.out.printf("key: %s; value: %s; thread: %s ", 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: mainsearch
search()方法接受BiFunction并為當(dāng)前的鍵值對(duì)返回一個(gè)非空的搜索結(jié)果,或者在當(dāng)前迭代不匹配任何搜索條件時(shí)返回null。只要返回了非空的結(jié)果,就不會(huì)往下搜索了。要記住ConcurrentHashMap是無(wú)序的。搜索函數(shù)應(yīng)該不依賴于映射實(shí)際的處理順序。如果映射的多個(gè)元素都滿足指定搜索函數(shù),結(jié)果是非確定的。
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
下面是另一個(gè)例子,僅僅搜索映射中的值:
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: soloreduce
reduce()方法已經(jīng)在Java 8 的數(shù)據(jù)流之中用過(guò)了,它接受兩個(gè)BiFunction類型的lambda表達(dá)式。第一個(gè)函數(shù)將每個(gè)鍵值對(duì)轉(zhuǎn)換為任意類型的單一值。第二個(gè)函數(shù)將所有這些轉(zhuǎn)換后的值組合為單一結(jié)果,并忽略所有可能的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
我希望你能喜歡我的Java8并發(fā)系列教程的第三部分。這個(gè)教程的代碼示例托管在Github上,還有許多其它的Java8代碼片段。歡迎fork我的倉(cāng)庫(kù)并自己嘗試。
如果你想要支持我的工作,請(qǐng)向你的朋友分享這篇教程。你也可以在Twiiter上關(guān)注我,因?yàn)槲視?huì)不斷推送一些Java或編程相關(guān)的東西。
第一部分:線程和執(zhí)行器
第二部分:同步和鎖
第三部分:原子變量和 ConcurrentMap
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/64957.html
摘要:在接下來(lái)的分鐘,你將會(huì)學(xué)會(huì)如何通過(guò)同步關(guān)鍵字,鎖和信號(hào)量來(lái)同步訪問(wèn)共享可變變量。所以在使用樂(lè)觀鎖時(shí),你需要每次在訪問(wèn)任何共享可變變量之后都要檢查鎖,來(lái)確保讀鎖仍然有效。 原文:Java 8 Concurrency Tutorial: Synchronization and Locks譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 歡迎閱讀我的Java8并發(fā)教程的第二部分。這份指南將...
同步 線程主要通過(guò)共享對(duì)字段和引用對(duì)象的引用字段的訪問(wèn)來(lái)進(jìn)行通信,這種通信形式非常有效,但可能產(chǎn)生兩種錯(cuò)誤:線程干擾和內(nèi)存一致性錯(cuò)誤,防止這些錯(cuò)誤所需的工具是同步。 但是,同步可能會(huì)引入線程競(jìng)爭(zhēng),當(dāng)兩個(gè)或多個(gè)線程同時(shí)嘗試訪問(wèn)同一資源并導(dǎo)致Java運(yùn)行時(shí)更慢地執(zhí)行一個(gè)或多個(gè)線程,甚至?xí)和K鼈儓?zhí)行,饑餓和活鎖是線程競(jìng)爭(zhēng)的形式。 本節(jié)包括以下主題: 線程干擾描述了當(dāng)多個(gè)線程訪問(wèn)共享數(shù)據(jù)時(shí)如何引入錯(cuò)誤。...
高級(jí)并發(fā)對(duì)象 到目前為止,本課程重點(diǎn)關(guān)注從一開始就是Java平臺(tái)一部分的低級(jí)別API,這些API適用于非常基礎(chǔ)的任務(wù),但更高級(jí)的任務(wù)需要更高級(jí)別的構(gòu)建塊,對(duì)于充分利用當(dāng)今多處理器和多核系統(tǒng)的大規(guī)模并發(fā)應(yīng)用程序尤其如此。 在本節(jié)中,我們將介紹Java平臺(tái)5.0版中引入的一些高級(jí)并發(fā)功能,大多數(shù)這些功能都在新的java.util.concurrent包中實(shí)現(xiàn),Java集合框架中還有新的并發(fā)數(shù)據(jù)結(jié)構(gòu)。 ...
摘要:多線程和并發(fā)問(wèn)題是技術(shù)面試中面試官比較喜歡問(wèn)的問(wèn)題之一。線程可以被稱為輕量級(jí)進(jìn)程。一個(gè)守護(hù)線程是在后臺(tái)執(zhí)行并且不會(huì)阻止終止的線程。其他的線程狀態(tài)還有,和。上下文切換是多任務(wù)操作系統(tǒng)和多線程環(huán)境的基本特征。 多線程和并發(fā)問(wèn)題是 Java 技術(shù)面試中面試官比較喜歡問(wèn)的問(wèn)題之一。在這里,從面試的角度列出了大部分重要的問(wèn)題,但是你仍然應(yīng)該牢固的掌握J(rèn)ava多線程基礎(chǔ)知識(shí)來(lái)對(duì)應(yīng)日后碰到的問(wèn)題。(...
以下是Java技術(shù)棧微信公眾號(hào)發(fā)布的關(guān)于 Java 的技術(shù)干貨,從以下幾個(gè)方面匯總。 Java 基礎(chǔ)篇 Java 集合篇 Java 多線程篇 Java JVM篇 Java 進(jìn)階篇 Java 新特性篇 Java 工具篇 Java 書籍篇 Java基礎(chǔ)篇 8張圖帶你輕松溫習(xí) Java 知識(shí) Java父類強(qiáng)制轉(zhuǎn)換子類原則 一張圖搞清楚 Java 異常機(jī)制 通用唯一標(biāo)識(shí)碼UUID的介紹及使用 字符串...
閱讀 3318·2023-04-25 16:25
閱讀 3823·2021-11-15 18:01
閱讀 1600·2021-09-10 11:21
閱讀 3007·2021-08-02 16:53
閱讀 3081·2019-08-30 15:55
閱讀 2489·2019-08-29 16:24
閱讀 2098·2019-08-29 13:14
閱讀 1027·2019-08-29 13:00