您好,登錄后才能下訂單哦!
這篇文章將為大家詳細(xì)講解有關(guān)java中的Clojure怎樣抽象并發(fā)性和共享狀態(tài),小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。
前言
在所有 Java 下一代語(yǔ)言中,Clojure 擁有最激進(jìn)的并發(fā)性機(jī)制和功能。Groovy 和 Scala 都為并發(fā)性提供了改善的抽象和語(yǔ)法糖的一種組合,而 Clojure 堅(jiān)持了它始終在 JVM 上提供獨(dú)一無二的行為的強(qiáng)硬立場(chǎng)。在本期 Java 下一代 中,我將介紹 Clojure 中眾多并發(fā)性選項(xiàng)的一部分。首先是為 Clojure 中易變的引用提供支撐的基礎(chǔ)抽象:epochal 時(shí)間模型。
Epochal 事件模型
或許 Clojure 與其他語(yǔ)言最顯著的區(qū)別與易變的狀態(tài)和值 密切相關(guān)。Clojure 中的值 可以是任何用戶感興趣的數(shù)據(jù):數(shù)字 42、映射結(jié)構(gòu) {:first-name "Neal :last-name "Ford"} 或某些更大型的數(shù)據(jù)結(jié)構(gòu),比如 Wikipedia?;緛碇v,Clojure 語(yǔ)言對(duì)待所有值就像其他語(yǔ)言對(duì)待數(shù)字一樣。數(shù)字 42 是一個(gè)值,您不能重新定義它。但可對(duì)該值應(yīng)用一個(gè)函數(shù),返回另一個(gè)值。例如,(inc 42) 返回值 43。
在 Java 和其他基于 C 的語(yǔ)言中,變量 同時(shí)持有身份和值,這是讓并發(fā)性在 Java 語(yǔ)言中如此難以實(shí)現(xiàn)的因素之一。語(yǔ)言設(shè)計(jì)人員在線程抽象之前創(chuàng)建了變量抽象,變量的設(shè)計(jì)沒有考慮為并發(fā)性增加的復(fù)雜性。因?yàn)?Java 中的變量假設(shè)只有單個(gè)線程,所以在多線程環(huán)境中,需要像同步塊這樣麻煩的機(jī)制來保護(hù)變量。Clojure 的設(shè)計(jì)人員 Rich Hickey 讓交織(complect) 這個(gè)古老的詞匯恢復(fù)了活力(交織這個(gè)詞被定義為 “纏繞或編織”),用于描述 Java 變量中的設(shè)計(jì)缺陷。
Clojure 將值 與引用 分開。在 Clojure 世界觀中,數(shù)據(jù)以一系列不變的值的形式存在,如圖 1 所示。
圖 1. epochal 時(shí)間模型中的值
圖 1 顯示,像 v1 這樣的獨(dú)立的值表示 42 或 Wikipedia 等數(shù)據(jù),使用方框表示。與值獨(dú)立的是函數(shù),它們獲取值作為參數(shù)并生成新值,如圖 2 所示。
圖 2. epochal 時(shí)間模型中的函數(shù)
圖 2 將函數(shù)顯示為與值獨(dú)立的圓圈。函數(shù)調(diào)用會(huì)生成新值,使用值作為參數(shù)和結(jié)果。一連串的值保存在一個(gè)引用 中,它表示變量的身份。隨著時(shí)間的推移,此身份可能指向不同的值(由于函數(shù)應(yīng)用),但身份從不更改,如圖 3 中的虛線所示。
圖 3. epochal 時(shí)間模型中的引用
在圖 3 中,整幅圖表示一個(gè)引用隨時(shí)間的變化。虛線是一個(gè)引用,它持有其生存期內(nèi)的一連串的值??稍谀硞€(gè)時(shí)刻向引用分配一個(gè)新的不變值;引用指向的目標(biāo)可更改,而無需更改該引用。
在引用的生存期中,一個(gè)或多個(gè)觀察者(其他程序、用戶界面、任何對(duì)該引用持有的值感興趣的對(duì)象)將解除引用它,查看它的值(或許還執(zhí)行某種操作),如圖 4 所示。
圖 4. 解除引用
在圖 4 中,觀察者(有兩種楔形表示)可持有引用本身(由來自虛線引用的箭頭表示),或者可解除引用它,檢索它的值(由來自該值的箭頭表示)。例如,您可能有一個(gè)函數(shù),它以一個(gè)傳遞給您的數(shù)據(jù)庫(kù)連接作為參數(shù),您進(jìn)而將該參數(shù)傳遞給一個(gè)更低級(jí)的持久性函數(shù)。在此情況下,您持有該引用,但從不需要它的值;持久性函數(shù)可能會(huì)解除引用它,以獲取它的值來連接到一個(gè)數(shù)據(jù)庫(kù)。
請(qǐng)注意,圖 4 中的觀察者不會(huì)進(jìn)行協(xié)調(diào) — 它們完全不依賴彼此。此結(jié)構(gòu)使得 Clojure 運(yùn)行時(shí)能夠在整個(gè)語(yǔ)言中保證了一些有用的屬性,比如決不允許讀取程序阻塞,這使得讀取操作變得非常高效。如果您希望更改一個(gè)引用(也就是說,將它指向一個(gè)不同的值),可使用 Clojure 的一個(gè) API 來執(zhí)行更新,這會(huì)采用 epochal 時(shí)間模型。
epochal 時(shí)間模型為整個(gè) Clojure 中的引用更新提供了支持。因?yàn)檫\(yùn)行時(shí)控制所有更新,所以它可防御線程沖突,開發(fā)人員在不太復(fù)雜的語(yǔ)言中必須爭(zhēng)用線程。
Clojure 擁有廣泛的方式來更新引用,具體依賴于您想要何種特征。接下來,我將討論兩種方式:簡(jiǎn)單的原子 和復(fù)雜的軟件事務(wù)內(nèi)存。
原子
Clojure 中的原子 是對(duì)數(shù)據(jù)一個(gè)原子部分的引用,無論該部分有多大。您創(chuàng)建一個(gè) atom 并初始化它,然后應(yīng)用一個(gè)突變函數(shù)。這里,我為一個(gè)原子創(chuàng)建了一個(gè)稱為 counter 的引用,將它初始化為 0。如果我希望將引用更新到一個(gè)新值,我可使用 (swap!) 這樣的函數(shù),它原子化地為該引用換入一個(gè)新值:
(def counter (atom 0)) (swap! counter + 10)
根據(jù) Clojure 中的慣例,突變函數(shù)的名稱以一個(gè)感嘆號(hào)結(jié)尾。(swap!) 函數(shù)接受該引用、要應(yīng)用的函數(shù)(在本例中為 + 運(yùn)算符)和任何其他參數(shù)。
Clojure 原子持有任何大小的數(shù)據(jù),而不只是原始值。例如,我可圍繞一個(gè) person 映射創(chuàng)建一個(gè)原子引用,并使用 map 函數(shù)更新它。使用 (create-person) 函數(shù)(未顯示),我在一個(gè)原子中創(chuàng)建一個(gè) person 記錄,然后使用 (swap!) 和 (assoc ) 更新該引用,這會(huì)更新一個(gè)映射關(guān)聯(lián):
(def person (atom (create-person))) (swap! person assoc :name "John")
原子還會(huì)通過 (compare-and-set!) 函數(shù),使用原子實(shí)現(xiàn)一個(gè)通用的樂觀鎖定模式:
(compare-and-set! a 0 42) => false (compare-and-set! a 1 7) = true
(compare-and-set!) 函數(shù)接受 3 個(gè)參數(shù):原子引用、想要的現(xiàn)有值和新值。如果原子的值與想要的值不匹配,更新不會(huì)發(fā)生,函數(shù)會(huì)返回 false。
Clojure 有各種各樣的機(jī)制都遵循引用語(yǔ)義。例如,promise(是一種不同的引用)承諾在以后提供一個(gè)值。這里,我創(chuàng)建對(duì)一個(gè)名為 number-later 的 promise 的引用。此代碼不會(huì)生成任何值,就像它對(duì)最終會(huì)這么做的承諾一樣。調(diào)用 (deliver ) 函數(shù)時(shí),一個(gè)值會(huì)綁定到 number-later:
(def number-later (promise)) (deliver number-later 42)
盡管此示例使用了 Clojure 中的 futures 庫(kù),但引用語(yǔ)義與簡(jiǎn)單的原子保持一致。
軟件事務(wù)內(nèi)存
沒有其他任何 Clojure 特性獲得了比軟件事務(wù)內(nèi)存 (STM) 更多的關(guān)注,這是 Clojure 以 Java 語(yǔ)言封裝垃圾收集的方式來封裝并發(fā)性的內(nèi)部機(jī)制。換句話說,您可編寫高性能的多線程 Clojure 應(yīng)用程序,而從不考慮同步塊、死鎖、線程庫(kù)等。
Clojure 封裝并發(fā)性的方式是,通過 STM 控制引用的所有突變。更新一個(gè)引用(惟一的易變抽象)時(shí),必須在一個(gè)事務(wù)中執(zhí)行,以使 Clojure 運(yùn)行時(shí)能夠管理更新??紤]一個(gè)經(jīng)典的銀行問題:向一個(gè)帳戶中存款,同時(shí)向另一個(gè)帳戶貸款。清單 1 顯示了一個(gè)簡(jiǎn)單的 Clojure 解決方案。
清單 1. 銀行交易
(defn transfer [from to amount] (dosync (alter from - amount) (alter to + amount)))
在 清單 1 中,我定義了一個(gè) (transfer ) 函數(shù),它接受 3 個(gè)參數(shù):from 和 to 帳戶 — 二者都是引用 — 以及金額。我從 from 帳戶中減去該金額,將它添加到 to 帳戶中,但此操作必須與 (dosync ) 事務(wù)一起發(fā)生。如果我在事務(wù)塊的外部嘗試一個(gè) (alter ) 調(diào)用,更新會(huì)失敗并拋出一個(gè) IllegalStateException:
(alter from - 1) =>> IllegalStateException No transaction running
在 清單 1 中,(alter ) 函數(shù)仍然遵守 epochal 時(shí)間模型,但使用 STM 來確保兩個(gè)操作都完成或都未完成。為此,STM — 非常像一個(gè)數(shù)據(jù)庫(kù)服務(wù)器 — 臨時(shí)重試阻塞的操作,所以您的更新函數(shù)在更新之外不應(yīng)有任何副作用。例如,如果您的函數(shù)還寫入一個(gè)日志,由于不斷重試,您可能會(huì)看到多個(gè)日志條目。STM 還會(huì)隨未解決事務(wù)的時(shí)長(zhǎng)增長(zhǎng)而逐步提高它們的優(yōu)先級(jí),顯示數(shù)據(jù)庫(kù)引擎中的其他更常見的行為。
STM 的使用很簡(jiǎn)單,但底層機(jī)制很復(fù)雜。從名稱可以看出,STM 是一個(gè)事務(wù)系統(tǒng)。STM 實(shí)現(xiàn)了 ACID 事務(wù)標(biāo)準(zhǔn)的 ACI 部分:所有更改都是原子性、一致 和隔離的。ACID 的耐久 部分在這里不適用,因?yàn)?STM 在內(nèi)存中操作。很少看到將像 STM 這樣的高性能機(jī)制內(nèi)置于一種語(yǔ)言的核心中;Haskell 是惟一認(rèn)真實(shí)現(xiàn)了 STM 的另一種主流語(yǔ)言 — 不要奇怪,因?yàn)?Haskell(像 Clojure 一樣)非常喜歡不變性。(.NET 生態(tài)系統(tǒng)曾嘗試構(gòu)建一個(gè) STM 管理器,但最終放棄了,因?yàn)樘幚硎聞?wù)和不變性變得太復(fù)雜了。)
縮減程序(reducer)和數(shù)字分類
如果不討論 上一期 中的數(shù)字分類器問題的替代實(shí)現(xiàn),并行性介紹都是不完整的。清單 2 顯示了一個(gè)沒有并行性的原子版本。
清單 2. Clojure 中的數(shù)字分類器
(defn classify [num] (let [facts (->> (range 1 (inc num)) (filter #(= 0 (rem num %)))) sum (reduce + facts) aliquot-sum (- sum num)] (cond (= aliquot-sum num) :perfect (> aliquot-sum num) :abundant (< aliquot-sum num) :deficient)))
清單 2 中的分類器版本濃縮為單個(gè)函數(shù),它返回一個(gè) Clojure 關(guān)鍵字(由一個(gè)前導(dǎo)冒號(hào)表示)。(let ) 塊使我能夠建立局部綁定。為了確定因數(shù),我使用 thread-last 運(yùn)算符來過濾數(shù)字范圍,讓代碼更有序。sum 和 aliquot-sum 的計(jì)算都很簡(jiǎn)單;一個(gè)數(shù)字的真因數(shù)和 是它的因數(shù)之和減去它本身,這使我的比較代碼更簡(jiǎn)單。該函數(shù)的最后一行是 (cond ) 語(yǔ)句,它針對(duì)計(jì)算的值來計(jì)算 aliquot-sum,返回合適的關(guān)鍵字枚舉。此代碼的一個(gè)有趣之處是,我以前的實(shí)現(xiàn)中的方法在這個(gè)版本中折疊為簡(jiǎn)單的賦值。在計(jì)算足夠簡(jiǎn)單和簡(jiǎn)潔時(shí),您通常需要?jiǎng)?chuàng)建的函數(shù)更少。
Clojure 包含一個(gè)稱為 縮減程序 的強(qiáng)大的并發(fā)性庫(kù)。(有關(guān)縮減程序庫(kù)的開發(fā)過程的解釋 — 包括為利用最新的 JVM 原生的 fork/join 工具而進(jìn)行的優(yōu)化 — 是一個(gè)吸引人的故事。)縮減程序庫(kù)提供了常見運(yùn)算的就地替換,比如 map、filter 和 reduce,使這些預(yù)算能夠自動(dòng)利用多個(gè)線程。例如,將標(biāo)準(zhǔn)的 (map ) 替換為 (r/map )(r/ 是縮減程序的命名空間),會(huì)導(dǎo)致您的映射操作自動(dòng)被運(yùn)行時(shí)并行化。
清單 3 給出了一個(gè)利用了縮減程序的數(shù)字分類器版本。
清單 3. 使用了縮減程序庫(kù)的分類器
(ns pperfect.core (:require [clojure.core.reducers :as r])) (defn classify-with-reducer [num] (let [facts (->> (range 1 (inc num)) (r/filter #(= 0 (rem num %)))) sum (r/reduce + facts) aliquot-sum (- sum num)] (cond (= aliquot-sum num) :perfect (> aliquot-sum num) :abundant (< aliquot-sum num) :deficient)))
必須仔細(xì)觀察,才能找出 清單 2 和 清單 3 之間的區(qū)別。惟一的區(qū)別是引入了縮減程序命名空間和別名,向 filter 和 reduce 都添加了 r/。借助這些細(xì)微的更改,我的過濾和縮減操作現(xiàn)在可自動(dòng)使用多個(gè)線程。
關(guān)于“java中的Clojure怎樣抽象并發(fā)性和共享狀態(tài)”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,使各位可以學(xué)到更多知識(shí),如果覺得文章不錯(cuò),請(qǐng)把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。