編程范式巡禮第二季 并發(fā)那些事
繼續(xù)上周的編程范式話題弃舒,今天想聊一下并發(fā)范式癞埠。
并發(fā)也算一種范式?
真正的并發(fā)式編程状原,絕不只是調(diào)用線程API或使用synchronized、lock之類的關(guān)鍵字那么簡單苗踪。從宏觀的架構(gòu)設(shè)計颠区,到微觀的數(shù)據(jù)結(jié)構(gòu)、流程控制乃至算法通铲,相比通常的串行式編程都可能發(fā)生變化毕莱。毫不夸張的說,是又一場思想和技術(shù)上革命颅夺。
在日常開發(fā)中朋截,并發(fā)編程難度是比較高的,屬于高級程序員才能掌握的內(nèi)容吧黄。其難點在哪里部服,我們?nèi)粘A?xí)慣的是線性思維,這與并發(fā)編程的多維世界觀是不同的拗慨,提升思考的維度無疑是艱難廓八。但還好,在大神們的努力下胆描,已逐漸化繁為簡瘫想,這也是并發(fā)范式帶來的力量。在并發(fā)領(lǐng)域有許多的模型昌讲,讓我們來巡禮一下国夜。
模型1:線程與鎖
并發(fā)編程以資源共享和競爭為主線。這意味著程序設(shè)計將圍繞進(jìn)程的劃分與調(diào)度短绸、進(jìn)程之間的通信與同步等來展開车吹。合理的并發(fā)式設(shè)計需要諸多方面的權(quán)衡考量。
線程是對底層硬件過程的形式化醋闭,是并發(fā)編程的核心窄驹。不同的線程各自獨立運行,有如一個個的平行宇宙证逻。但是乐埠,并發(fā)編程并不僅僅串行化編程的疊加,主要的差異在于囚企,線程之間存在共享和競爭丈咐。共享資源會帶來哪些問題呢?
- 問題1:臟讀
當(dāng)線程1對共享數(shù)據(jù)進(jìn)行修改時龙宏,線程2有可能會讀到處于中間狀態(tài)的數(shù)據(jù)棵逊,這個問題稱之為臟讀。

解決的思路比較簡單银酗,就是讓線程1僅提交最終修改結(jié)果辆影,在修改過程中產(chǎn)生并使用快照數(shù)據(jù)徒像。這種類似影分身的技術(shù)稱之為MVCC(Multi-Version Concurrency Control)。

- 問題2:丟失更新
當(dāng)線程1對數(shù)據(jù)進(jìn)行修改時蛙讥,如果線程2同時修改锯蛀,由于采用了MVCC,雙方各自無法看到键菱,那最終提交時谬墙,很可能會造成其中一個線程結(jié)果與預(yù)期不一致今布,這個問題稱為丟失更新经备。
其解決方法是加鎖,在修改前進(jìn)行加鎖部默,一旦占用侵蒙,則第二個線程無法獲取。

臟讀和丟失更新是需要同時考慮的傅蹂,所以標(biāo)準(zhǔn)的多線程處理是同時使用到了MVCC和鎖這兩個技術(shù)纷闺。

- 問題3:幻讀
在已解決了丟失更新和臟讀的情況下,下面要考慮多次讀取的情況份蝴。如下圖所示犁功,線程1對數(shù)據(jù)集進(jìn)行了多次讀取,但是部分?jǐn)?shù)據(jù)在線程2中進(jìn)行了更新婚夫,這時候出現(xiàn)了線程1在沒有任何作為的情況下浸卦,兩次讀取不一致的情況!0覆凇限嫌!這個問題稱為幻讀。

解決幻讀的方法是擴(kuò)大數(shù)據(jù)的鎖范圍时捌,不僅僅是更改過的記錄怒医,所有讀取的記錄都要加鎖。

- 綜述
這就是目前我們最主流的并發(fā)與鎖的實現(xiàn)思路方法奢讨,有非常廣泛的使用稚叹。不知道大家讀完這段的感覺怎么,我看的時候拿诸,第一個感覺是復(fù)雜扒袖,真的非常的燒腦,由于大量概念的堆積對于初學(xué)者來說非常不友好佳镜;第二個感覺是矛盾僚稿,按照最終幻讀的解決方案,實際上就是放棄了程序間的并行蟀伸,繞了一圈蚀同,又回到了原點缅刽。正因為如此,目前主流的數(shù)據(jù)庫蠢络,實際上默認(rèn)都是放棄對于幻讀問題解決的衰猛,這也是開發(fā)上的一大坑。
綜合的來看刹孔,這種解決方式學(xué)習(xí)成本很高啡省,而且還沒能解決全部的問題,并不能讓人滿意髓霞。有沒有更好點的方法卦睹,讓我們繼續(xù)。
模型2:函數(shù)式編程與Lambda架構(gòu)
傳統(tǒng)并發(fā)模型中方库,最令人糾結(jié)的無疑就是共享數(shù)據(jù)訪問這塊了结序。若不爽,就另辟蹊徑纵潦。我們能不能不對共享數(shù)據(jù)進(jìn)行寫入呢?有什么樣的程序是只讀不寫的呢徐鹤,大神們已經(jīng)找到了答案,就是上周介紹的函數(shù)式編程邀层。
首先想說明的事返敬,純函數(shù)式的編程功能上并不完備,有非常多的缺陷寥院,但其有一個天然的適用場景劲赠,就是數(shù)學(xué)運算分析,也就是我們現(xiàn)在時常掛在嘴邊的大數(shù)據(jù)計算只磷。
由于拋棄掉了共享狀態(tài)经磅,其代碼的健壯性和擴(kuò)展性得到了大大的增強(qiáng),只要有足夠的計算資源就可以處理無限大的數(shù)據(jù)钮追。
函數(shù)式編程思維比較數(shù)學(xué)化预厌,難度是比較高的,在此基礎(chǔ)上元媚,誕生了Lambda框架轧叽,是對應(yīng)用模式的固化,有助于降低學(xué)習(xí)成本和大范圍推廣刊棕。Lambda框架既使用了可以進(jìn)行大規(guī)模批處理的MapReduce技術(shù)炭晒,也使用了可以快速處理數(shù)據(jù)并及時反饋的流處理技術(shù),這樣的混搭能夠為大數(shù)據(jù)問題提供擴(kuò)展性甥角、響應(yīng)性和容錯性都能優(yōu)秀的解決方案网严。
Lambda架構(gòu)也可以這樣來描述:在該架構(gòu)中,被讀取的數(shù)據(jù)是不可變的嗤无,在并行處理過程中數(shù)據(jù)會依次進(jìn)入批處理系統(tǒng)(batch system)與流處理系統(tǒng)震束。從邏輯上看怜庸,傳輸過程發(fā)生了兩次,一次是在批處理中垢村,一次是在流處理中割疾。在查詢時,當(dāng)這兩者都返回結(jié)果后嘉栓,才算是完成一次完整的查詢宏榕。
模型3:Actor
函數(shù)式編程模型的應(yīng)用使得并發(fā)編程的應(yīng)用踏入了工業(yè)級,帶動了大數(shù)據(jù)的熱潮侵佃。但是其解決思路是拋棄了可變狀態(tài)麻昼,服務(wù)是有損的。對于必須提供無損服務(wù)的場景該如何進(jìn)行改進(jìn)呢趣钱。
從最一開始線程與鎖的模型中涌献,我們可以看到串行化是最重的解決方案,但是為了串行化首有,我們需要MVCC、鎖等一系列的工具枢劝,比較復(fù)雜井联,Actor模型就是用來簡化此類操作的。
Actor模型中抽象出了兩個概念A(yù)ctor和Mailbox您旁,Actor就是指代共享數(shù)據(jù)烙常,Mailbox管理數(shù)據(jù)的操作。對于每個Actor的操作鹤盒,要通過mailbox來進(jìn)行蚕脏,在mailbox端實現(xiàn)了隊列的控制,從而實現(xiàn)了序列化的效果侦锯。
Actor模型會帶來一些額外的好處:
- 用Actor來定義共享數(shù)據(jù)驼鞭,邊界非常清晰,實現(xiàn)了與主線代碼的解耦尺碰,最大化減少了序列化的影響挣棕,可以有效提升性能。
- Actor中引入了消息的概念亲桥,是位置透明的洛心,天然支持了分布式的部署。
- 在概念清晰之后题篷,代碼得到了簡化词身,下面摘錄一段Actor的代碼,可以看到是封裝了并發(fā)相關(guān)的技術(shù)細(xì)節(jié)番枚,非常的簡潔法严。
class Pong extends Actor {
def act() {
var pongCount = 0
while (true) {
receive {
case Ping =>
if (pongCount % 1000 == 0)
Console.println("Pong: ping " + pongCount)
sender ! Pong
pongCount = pongCount + 1
case Stop =>
Console.println("Pong: stop")
exit()
}
}
}
}
模型4:原子變量
很多情況下我們需要一個高效的璧瞬、線程安全的并發(fā)解決方案。高效意味著耗用資源要少渐夸,程序處理速度要快嗤锉;線程安全也非常重要,這個在多線程下能保證數(shù)據(jù)的正確性墓塌。有一個解決方案是原子變量瘟忱。
通常情況下,在Java里面苫幢,++i或者--i不是線程安全的访诱,這里面有三個獨立的操作:獲得變量當(dāng)前值,為該值+1/-1韩肝,然后寫回新的值触菜。在沒有額外資源可以利用的情況下,只能使用加鎖才能保證讀-改-寫這三個操作是“原子性”的哀峻。
下面是示例代碼:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
在這里采用了CAS操作涡相,每次從內(nèi)存中讀取數(shù)據(jù)然后將此數(shù)據(jù)和+1后的結(jié)果進(jìn)行CAS操作,如果成功就返回結(jié)果剩蟀,否則重試直到成功為止催蝗。而compareAndSet利用JNI來完成CPU指令的操作。
原子變量在一些對性能有極端要求的系統(tǒng)中(比如Jetty育特、Tomcat)有非常廣泛的應(yīng)用丙号,是一種精益求精的體現(xiàn),其在可靠性和性能方面表現(xiàn)很突出缰冤,但在易用性方面比較偏計算機(jī)思維犬缨,理解難度較大,并不夠簡潔棉浸,需要反復(fù)練習(xí)才能掌握怀薛。
小結(jié)
在今天的篇文章中,列舉了并發(fā)范式的四個主流模型:線程與鎖涮拗、函數(shù)式編程乾戏、Actor、原子變量三热」脑瘢可以看到,每個模型都是在功能就漾、性能和易用之間尋求了一種平衡呐能,并沒有一種模型在功能、性能和易用三方面同時達(dá)到最優(yōu),也就是說沒有銀彈摆出。
這是我們面對并發(fā)問題時的困境朗徊,也是挑戰(zhàn),也正說明了并發(fā)并不是一個簡單的線性問題偎漫,我們需要針對具體場景爷恳、具體問題進(jìn)行分析,尋找最適合的解決方法象踊,這也是開發(fā)人員需要養(yǎng)成的一種重要素養(yǎng)温亲。