小課堂week17 編程範式巡禮第二季 併發那些事

編程範式巡禮第二季 併發那些事

繼續上週的編程範式話題,今天想聊一下併發範式。java

併發也算一種範式?

真正的併發式編程,毫不只是調用線程API或使用synchronized、lock之類的關鍵字那麼簡單。從宏觀的架構設計,到微觀的數據結構、流程控制乃至算法,相比一般的串行式編程均可能發生變化。絕不誇張的說,是又一場思想和技術上革命。程序員

在平常開發中,併發編程難度是比較高的,屬於高級程序員才能掌握的內容。其難點在哪裏,咱們平常習慣的是線性思惟,這與併發編程的多維世界觀是不一樣的,提高思考的維度無疑是艱難。但還好,在大神們的努力下,已逐漸化繁爲簡,這也是併發範式帶來的力量。在併發領域有許多的模型,讓咱們來巡禮一下。算法

模型1:線程與鎖

併發編程以資源共享和競爭爲主線。這意味着程序設計將圍繞進程的劃分與調度、進程之間的通訊與同步等來展開。合理的併發式設計須要諸多方面的權衡考量。數據庫

線程是對底層硬件過程的形式化,是併發編程的核心。不一樣的線程各自獨立運行,有如一個個的平行宇宙。可是,併發編程並不只僅串行化編程的疊加,主要的差別在於,線程之間存在共享和競爭。共享資源會帶來哪些問題呢?編程

  • 問題1:髒讀

當線程1對共享數據進行修改時,線程2有可能會讀處處於中間狀態的數據,這個問題稱之爲髒讀。安全

併發1

解決的思路比較簡單,就是讓線程1僅提交最終修改結果,在修改過程當中產生並使用快照數據。這種相似影分身的技術稱之爲MVCC(Multi-Version Concurrency Control)。數據結構

併發2

  • 問題2:丟失更新

當線程1對數據進行修改時,若是線程2同時修改,因爲採用了MVCC,雙方各自沒法看到,那最終提交時,極可能會形成其中一個線程結果與預期不一致,這個問題稱爲丟失更新。
其解決方法是加鎖,在修改前進行加鎖,一旦佔用,則第二個線程沒法獲取。多線程

併發3

髒讀和丟失更新是須要同時考慮的,因此標準的多線程處理是同時使用到了MVCC和鎖這兩個技術。架構

併發4

  • 問題3:幻讀

在已解決了丟失更新和髒讀的狀況下,下面要考慮屢次讀取的狀況。以下圖所示,線程1對數據集進行了屢次讀取,可是部分數據在線程2中進行了更新,這時候出現了線程1在沒有任何做爲的狀況下,兩次讀取不一致的狀況!!!這個問題稱爲幻讀。併發

併發5

解決幻讀的方法是擴大數據的鎖範圍,不只僅是更改過的記錄,全部讀取的記錄都要加鎖。

併發6

  • 綜述

這就是目前咱們最主流的併發與鎖的實現思路方法,有很是普遍的使用。不知道你們讀完這段的感受怎麼,我看的時候,第一個感受是複雜,真的很是的燒腦,因爲大量概念的堆積對於初學者來講很是不友好;第二個感受是矛盾,按照最終幻讀的解決方案,實際上就是放棄了程序間的並行,繞了一圈,又回到了原點。正由於如此,目前主流的數據庫,實際上默認都是放棄對於幻讀問題解決的,這也是開發上的一大坑。

綜合的來看,這種解決方式學習成本很高,並且還沒能解決所有的問題,並不能讓人滿意。有沒有更好點的方法,讓咱們繼續。

模型2:函數式編程與Lambda架構

傳統併發模型中,最使人糾結的無疑就是共享數據訪問這塊了。若不爽,就另闢蹊徑。咱們能不能不對共享數據進行寫入呢?有什麼樣的程序是隻讀不寫的呢,大神們已經找到了答案,就是上週介紹的函數式編程。

首先想說明的事,純函數式的編程功能上並不完備,有很是多的缺陷,但其有一個自然的適用場景,就是數學運算分析,也就是咱們如今時常掛在嘴邊的大數據計算。
因爲拋棄掉了共享狀態,其代碼的健壯性和擴展性獲得了大大的加強,只要有足夠的計算資源就能夠處理無限大的數據。

函數式編程思惟比較數學化,難度是比較高的,在此基礎上,誕生了Lambda框架,是對應用模式的固化,有助於下降學習成本和大範圍推廣。Lambda框架既使用了能夠進行大規模批處理的MapReduce技術,也使用了能夠快速處理數據並及時反饋的流處理技術,這樣的混搭可以爲大數據問題提供擴展性、響應性和容錯性都能優秀的解決方案。

Lambda架構也能夠這樣來描述:在該架構中,被讀取的數據是不可變的,在並行處理過程當中數據會依次進入批處理系統(batch system)與流處理系統。從邏輯上看,傳輸過程發生了兩次,一次是在批處理中,一次是在流處理中。在查詢時,當這二者都返回結果後,纔算是完成一次完整的查詢。

20140510204559203

模型3:Actor

函數式編程模型的應用使得併發編程的應用踏入了工業級,帶動了大數據的熱潮。可是其解決思路是拋棄了可變狀態,服務是有損的。對於必須提供無損服務的場景該如何進行改進呢。

從最一開始線程與鎖的模型中,咱們能夠看到串行化是最重的解決方案,可是爲了串行化,咱們須要MVCC、鎖等一系列的工具,比較複雜,Actor模型就是用來簡化此類操做的。

Actor模型中抽象出了兩個概念Actor和Mailbox,Actor就是指代共享數據,Mailbox管理數據的操做。對於每一個Actor的操做,要經過mailbox來進行,在mailbox端實現了隊列的控制,從而實現了序列化的效果。

AkkaComponentMatching

Actor模型會帶來一些額外的好處:

  1. 用Actor來定義共享數據,邊界很是清晰,實現了與主線代碼的解耦,最大化減小了序列化的影響,能夠有效提高性能。
  2. Actor中引入了消息的概念,是位置透明的,自然支持了分佈式的部署。
  3. 在概念清晰以後,代碼獲得了簡化,下面摘錄一段Actor的代碼,能夠看到是封裝了併發相關的技術細節,很是的簡潔。
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:原子變量

不少狀況下咱們須要一個高效的、線程安全的併發解決方案。高效意味着耗用資源要少,程序處理速度要快;線程安全也很是重要,這個在多線程下能保證數據的正確性。有一個解決方案是原子變量。

一般狀況下,在Java裏面,++i或者--i不是線程安全的,這裏面有三個獨立的操做:得到變量當前值,爲該值+1/-1,而後寫回新的值。在沒有額外資源能夠利用的狀況下,只能使用加鎖才能保證讀-改-寫這三個操做是「原子性」的。

下面是示例代碼:

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

在這裏採用了CAS操做,每次從內存中讀取數據而後將此數據和+1後的結果進行CAS操做,若是成功就返回結果,不然重試直到成功爲止。而compareAndSet利用JNI來完成CPU指令的操做。

原子變量在一些對性能有極端要求的系統中(好比Jetty、Tomcat)有很是普遍的應用,是一種精益求精的體現,其在可靠性和性能方面表現很突出,但在易用性方面比較偏計算機思惟,理解難度較大,並不夠簡潔,須要反覆練習才能掌握。

小結

在今天的篇文章中,列舉了併發範式的四個主流模型:線程與鎖、函數式編程、Actor、原子變量。能夠看到,每一個模型都是在功能、性能和易用之間尋求了一種平衡,並無一種模型在功能、性能和易用三方面同時達到最優,也就是說沒有銀彈。 這是咱們面對併發問題時的困境,也是挑戰,也正說明了併發並非一個簡單的線性問題,咱們須要針對具體場景、具體問題進行分析,尋找最適合的解決方法,這也是開發人員須要養成的一種重要素養。

相關文章
相關標籤/搜索