關於synchronized

weixin_33670713發表於2017-12-06

什麼是同步

對於Java程式設計師來說,synchronized關鍵字肯定都不陌生了。最近由於培訓新員工的原因,自己在複習Java中多執行緒的知識,看到synchronized關鍵字的時候,想到它的中文翻譯“同步”,腦子裡突然冒出一個問題:“到底什麼是同步?”停下來想了一下,似乎同步就是對資料的併發訪問進行保護,以免出現資料錯誤。那麼IO中的同步IO也是防止資料被併發訪問嗎?好像不是,因為同步IO是指有IO資料返回前,執行緒會被掛起、阻塞住,這裡的同步主要體現的是阻塞性,好像不涉及到對資料的保護。這些似乎都是同步,但感覺自己又沒法用一句完整的話將它描述出來!看來肯定是有什麼東西自己還沒有掌握透徹,亦或是某些知識點還沒有總結到位,這才出現這種似懂非懂的狀況。索性花點時間再專研一下吧。

那麼,什麼是同步呢?
習慣性的先百度了一下,看到百度百科裡是這樣解釋的:

同步指兩個或兩個以上隨時間變化的量在變化過程中保持一定的相對關係。
同步(英語:Synchronization),指對在一個系統中所發生的事件(event)之間進行協調,在時間上出現一致性與統一化的現象。在系統中進行同步,也被稱為及時(in time)、同步化的(synchronous、in sync)。

嗯,說的挺抽象的。那再看看維基百科是怎麼解釋的呢?

Synchronization is the coordination of events to operate a system in unison
(同步是對事件的協調以使得系統和諧一致)

嗯,一樣挺抽象的~感覺就是讀的懂這些字,但就是不知道它們在說什麼。既然這些解釋都太抽象不好理解,那就先找些它的具體例子例項化一下吧。有些什麼例子呢?最近做的專案中,涉及到兩個系統間的告警資料同步,這裡就出現了“同步”這個詞了。還有其他例子嗎?前段時間我換了手機,需要把原來手機上的通訊錄導到新手機裡,當時下了個QQ同步助手,兩分鐘搞定,嗯,這裡也出現了“同步”。還有,以前看美劇的時候,常常有畫面和聲音不同步的情況,那麼反過來正常的情況就是聲畫同步,看起來這也是一種“同步”。另外,我們常常會說在進行一項活動時,需要另一項相關的活動同步進行,比如“促進工業化、資訊化、城鎮化、農業現代化同步發展”,這又是一個“同步”的例子。

看看這些例子,除了都符合前面“同步”的抽象定義外,感覺說不上它們之間有什麼共性。每個例子所處的領域差別很大。這時正好看到維基百科裡對同步的進一步說明,發現下面的話:

Synchronization is an important concept in the following fields:

  • Computer science
  • Cryptography
  • Multimedia
  • Music
  • Neuroscience
    ...

突然意識到我提出“什麼是同步”的問題時,忽略掉了它所在的領域和上下文。其實,同步這個詞在不同的領域有差別很大的具體解釋。例如通訊領域中的同步含義就很廣泛,具體到網路通訊可能指的是一個操作要阻塞直到其完成後才能繼續下一步操作(個人理解IO其實就是一種通訊問題,因而IO中的同步可以等同理解為阻塞)。如果拋開具體的領域來說什麼是同步,就只能得到那種包羅永珍、抽象又不具體的解釋。

回過頭來再看看我的問題,其實應該更具體的這樣問:在java的多執行緒中同步是什麼。

電腦科學中的同步是什麼?

在回答java多執行緒中什麼是同步之前,先看看在電腦科學中,什麼是同步。因為前者是後者的一個子領域,理解了計算機這個大領域中什麼是同步,可以幫助理解java執行緒這個小領域中的同步。

維基百科中是這樣解釋電腦科學領域中的同步的:

In computer science, synchronization refers to one of two distinct but related concepts: synchronization of processes, and synchronization of data. Process synchronization refers to the idea that multiple processes are to join up or handshake at a certain point, in order to reach an agreement or commit to a certain sequence of action. Data synchronization refers to the idea of keeping multiple copies of a dataset in coherence with one another, or to maintain data integrity. Process synchronization primitives are commonly used to implement data synchronization.

電腦科學中,同步涉及到兩個各自獨立、但又相互關聯的概念:程式的同步,以及資料的同步

這句話我覺得值得牢記在心裡!不過上面引文中的最後一句話也很重要:

程式同步原語通常用於實現資料同步

我覺得這第二句話說明了程式同步和資料同步的關聯關係。

程式的同步就是本文要討論的問題域,具體來說是java中多執行緒的同步。而資料同步也很好理解,例如mysql的主從資料同步,QQ同步助手的通訊錄同步等都符合上面的資料同步定義,即資料集的多份拷貝之間保證彼此之間的一致性和完整性。

雖然維基百科的解釋不一定是最權威的解釋,不過我覺得在計算機領域按程式和資料兩種物件來細分同步還是有道理的。這兩個領域中同步的具體形式完全不同,而又彼此有聯絡。如果不能清楚的意識到這種即不同又相關的特點,很容易因為兩者的相關性從而下意識的把不同的概念混淆在一起。

那麼接下來就看看具體到java裡面,是如何使用synchronized關鍵字來實現執行緒的同步吧。其實這個討論的過程中也會涉及到資料的同步,後面會具體說明。

Java中執行緒的同步

提到Java中的執行緒同步,就不得不提作業系統中的程式同步,這兩者的問題域是極其相似的。同步實際上就是對併發執行的程式或執行緒進行某種協調,而協調的具體手段有不止一種,需要根據協調的具體要求來使用對應的手段。從這點來看,併發是引入同步的起因,而要實現同步,則需要具體的一些方法

那我們首先看下併發的執行緒面臨哪些問題場景(為了少寫一些字,後面就省略程式二字),瞭解了這些併發的場景就知道了參與到這些場景中的執行緒需要哪一類的協調方式,該選擇哪種具體的同步手段。實際上,併發的多個執行緒之間,會存在下面兩種協作關係:

  • 競爭
  • 合作

對於競爭,相信大家都很熟悉了。就是多個執行緒會爭搶某種線上程之間共享的資源,這類資源叫做臨界資源,而使用臨界資源的那部分程式碼塊叫做臨界區。例如下面的Java程式碼:

public class Counter {
    private int count;

    public void increase() {
        count++;
    }

    public static void main(String[] args) {
        final Counter counter = new Counter();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // counter是臨界資源
                synchronized (counter) {
                    // 這段程式碼塊是臨界區
                    counter.increase();
                }
            }
        });
    }
}

而協調執行緒之間的競爭關係,則是提供互斥這種同步手段來實現的。互斥就是當一個執行緒在臨界區訪問臨界資源時,其他執行緒不能進入該臨界區訪問任何臨界資源。也就是說互斥提供的同步手段就是讓執行緒順序的依次使用臨界資源。而Java通過synchronized關鍵字提供了互斥的能力。

而對於合作相信大家也不陌生。例如經典的生產者-消費者問題就是一種執行緒間合作的例子。而對合作的執行緒進行協調,其中一種方式就是提供執行緒間的相互通訊能力。如果更抽象一點的形容這種通訊方式,那就是多個併發執行的執行緒之間存在某種訊息傳遞機制,使得某些執行緒執行到一個特定點後,可以給另一些執行緒傳送訊息,以通知它們開始執行。而Java多執行緒對通訊的支援則通過Object中的wait()、notify()、notifyAll()等方法完成,而這些方法必須被包含在一個synchronized程式碼塊中:

synchronized (lock) {
    try {
        lock.wait();
    } catch (InterruptedException e) {

    }
}


synchronized (lock) {
    lock.notify();
}

看,執行緒間的通訊也需要synchronized關鍵字的參與,因此說通訊也是一種具體的同步手段也不為過。

總結一下,同步指的就是對執行緒執行的相互關係進行協調,它源於執行緒併發執行的引入。併發執行緒存在競爭和合作兩種關係,我們可以通過提供互斥和通訊這兩種同步手段來對前面兩種關係提供支援。

事實上,正如前面說的那樣,在作業系統中也是通過提供互斥和通訊這兩種手段,來解決併發程式的同步問題。而作業系統中提供互斥、通訊手段的具體實現有很多種,例如:

  • 管程 Monitor
  • 訊號量 Semaphore
  • 訊息傳遞 Message Passing
  • ...

實際上,Java語言的設計、JDK API的實現、以及JVM虛擬機器的規範中關於執行緒的同步,就採用了一些上面作業系統中對程式同步的解決方案。

例如,前面的synchronized關鍵字其實現方式就是用的管程Monitor。如果我們用javap -c命令看下前面程式碼的class檔案,會得到類似下面的位元組碼:

public void run();
    Code:
       0: aload_0
       1: getfield      #1                  // Field val$counter:Lcom/leo/base/javas/jvm/Counter;
       4: dup
       5: astore_1
       6: monitorente
       7: aload_0
       8: getfield      #1                  // Field val$counter:Lcom/leo/base/javas/jvm/Counter;
      11: invokevirtual #3                  // Method com/leo/base/javas/jvm/Counter.increase:()V
      14: aload_1
      15: monitorexit
      16: goto          24
      19: astore_2
      20: aload_1
      21: monitorexit

注意其中的第6、21行,分別代表了管程的進入和退出。

另外,訊號量Semaphore也通過jdk中的java.util.concurrent.Semaphore類提供了實現。其他的還有諸如CyclicBarrier等就不一一列舉了。

而一旦我們知道了諸如管程、訊號量、訊息傳遞、互斥鎖這些具體的手段都是用於提供執行緒同步的互斥、通訊能力的話,在開發中就能很容易的知道應該在哪些場景下來如何使用這些手段了。

把知識點連起來,形成體系

在查閱這些執行緒同步的知識的過程中,心裡還隱隱有另一個問題。以前學習過Java記憶體模型的相關知識,知道在Java多執行緒中,資料是儲存在Java的主記憶體中的,也就是Java堆裡面。而在每個執行緒使用這些資料時,需要把堆中的資料拷貝到執行緒的工作記憶體中,也就是棧中。那麼Java通過synchronized的關鍵字可以保證執行緒對資料的修改結果在同步程式碼塊結束後被其他執行緒看到。這也就是說執行緒必定需要在執行到同步程式碼塊的某一個點的時候,將執行緒棧中的資料拷貝回主記憶體的堆中,否則下一個獲得同步程式碼塊執行權的執行緒就看不到上一個執行緒修改的資料了。

那麼問題來了,具體的細節上,Java是怎麼做的這個動作的呢?

這個問題冒出來的時候,自己馬上聯想到之前在閱讀《深入理解Java虛擬機器》這本書的時候,看到書裡提到過Java記憶體模型中定義的8個原子的操作,用於規範主記憶體和工作記憶體之間的互動協議,這些協議涉及到一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步回主記憶體這樣的細節。(大家注意到沒,這裡就出現了前面說到的計算機領域中的另一類同步 - 針對資料的同步。果然執行緒同步和資料同步是相關的)

這8個原子操作分別是:

  1. lock:作用於主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態。
  2. unlock:作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
  3. read:作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用。
  4. load:作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
  5. use:作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用到變數的值的位元組碼指令時將會執行這個操作。
  6. assign:作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
  7. store:作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用。
  8. write:作用於主記憶體的變數,它把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中。

Java記憶體進一步規定了這8個操作的執行時必須滿足的規則。其中有兩條:

如果對一個變數執行lock操作,那將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行load或assign操作初始化變數的值

對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中

前一條說明了Java執行緒在進入同步程式碼塊時,是怎樣從主記憶體拷貝資料到工作記憶體的。後一條則回答了同步程式碼塊執行結束時,是怎樣把工作記憶體的值拷貝回主記憶體的。

當時在看這部分內容時,雖然理解了這些操作的作用、規則機制,但是感覺比較抽象,時間一久就很模糊了。此時想起來,通過synchronized這個具體的知識點,將Java記憶體模型機制和多執行緒實現這兩者之間又建立了一個連線,感覺對自己的知識體系做了進一步完善。而體系中各個點的連線越多,一來對知識的理解更深,二來也越不容易忘記。

關於模式的複用

從上面我們可以看到,java中對多執行緒併發的同步採用了很多作業系統程式同步的解決方案。而這些方案、模式之所以可以複用過來,就在於執行緒同步和程式同步這兩個具體領域背後的問題模型是一樣的,因而一個領域的解決方案可以形成模式,並複用到新的具體領域。

那這給了我什麼啟發呢?我覺得有兩點。

  • 面對新的具體問題時,先看看能不能複用模式

當我們遇到新領域、新問題後,如果能拋開問題的表象,看到具體問題背後的本質、模型,這樣就可以優先想一想,對這種問題模型是否存在已經成熟的解決方案可以直接複用或是參考借鑑呢?有了這樣的思維方式和看待問題的角度,相信在不少時候都可以極大的提升我們解決問題、學習新知識的效率。

例如,對於分散式平行計算中的同步問題,從具體載體的形式上來講,並行的是各個計算機,而不是程式和執行緒了,因此問題的表象發生了變化。但是問題的基本模型還是在討論併發中的同步,也就是問題本質和模型沒有變化。那麼當我們學習這個新領域時,是否可以借鑑執行緒和程式同步中得到的經驗來幫助我們更快速的理解呢?在實施平行計算的工程應用時,是否也可以用到類似管程、訊號量這些手段呢?

再比如,看過《數學之美》這本書的同學大概知道早期的機器翻譯的解決辦法是人工構建語法詞法規則,但是效果很差。後來有人將這個問題的模型進行重新定義,把它變成一個數學上的概率問題,這樣一來語言翻譯問題從本質和模型上來講就是一個數學問題了,就可以轉而用數學方式進行處理,具體來說就是利用數學上的隱含馬爾科夫模型。隱含馬爾科夫模型早在機器翻譯之前就被提出來了,當將它應用到機器翻譯領域後,翻譯的準確度得到極大的提高。此外,隱含馬爾科夫模型還應用在了語音識別、影象處理、基因序列分析、甚至是股票預測和投資。從這可以看到這些不同形式的問題背後,其本質和模型都是一樣的,因而其具體的解決手段也可以有相通的地方。

我想,有了這種思維方式,我們其實可以在生活的方方面面使用到它。最近我在一個育兒專欄,裡面提到如何引導害羞的孩子參與到社交活動中,其實裡面的理念和方法用到成年人身上也是類似的。而這就是一個在生活中使用上面思維方式的例子。

  • 對具有共性的不同問題進行總結,提取問題模型和通用解決方案

上面是將問題模型應用到具體的領域。其實反過來,我們也應該注重從具體問題抽取共性,看看是否可以總結出具有普遍意義的問題模型,進而在理論上研究一些形而上的解決方案。當然,這項工作對於像我這樣搞工程應用的人來說很難做到,也缺乏時間進行理論性的研究,往大了說,是搞科學研究的那些人擅長的事。但是若我們具有這種意識,哪怕是我們從中能得到一點點有用的可複用的模型,對我們的工作、學習都是有好處的。

舉個例子,前幾個月遇到一個故障,一個系統上的某些定時任務沒有執行,而其他時間點的任務都觸發執行了。最終分析的原因是有些定時任務中進行了網路IO操作,而網路IO設定的超時時間較長,超過了下一次任務的觸發時間點,進而使得本該執行的下一個定時任務被錯過了。當時我覺得這是一個典型問題,因此留心大致做了一下總結:在定時任務中(尤其是觸發頻率較高的定時任務中),如果涉及到IO,那麼一定要小心這些IO操作的執行時間會不會影響下一次任務的觸發和執行。而恰巧最近團隊在做一個新需求時,也涉及到類似的場景,只是差別在於觸發定時任務的是外部系統,我們只是對任務執行做二次開發。在我們的實現中要對多個網路裝置進行訪問,如果這時把這些網路IO操作都放到任務觸發時來做,而且是序列的來做,就很有可能出現之前那種問題。還好有了上次的經驗,總結了問題的本質,因而在這次開發中及時對實現方案進行了修改,規避了潛在的風險。

從這個例子看,有意識的從具體問題中總結共性、模型,對我們還是有幫助的吧。

再進一步,我覺得擁有上面這兩種思維方式可以提高一個人的核心能力。因為這種能力在人的一生當中的方方面面都是用得上的,即便這個人跨界到其他領域當中,這些思維方式也有用,而不像具體的知識、技能那樣就失效了。什麼意思呢?我們常常會看到一些很有能力的人,他們在各種工作崗位、擔任不同的角色時都能做得很好,即使這些崗位、角色涉及的領域有很大的不同。比如搞開發他能做得好,轉型到管理他也可以勝任。甚至在跨學科中也有人能達到這樣的境界,例如達芬奇、牛頓、愛因斯坦。與其說是他們聰明,倒不如說他們掌握了好的思維方式。這些思維方式可以幫助他們迅速的適應新領域,學東西比別人快,看問題比別人透徹。我覺得作為個人成長,我們除了學習具體的知識和技術外,掌握這些思維方式也是很重要,而且這些思維方式、方法更通用。

後記

通過這次溫習Java中同步相關知識的過程,個人感覺收穫的不僅是具體的技術、知識,還在於修煉了一些解決問題、提高工作學習效率的方法。個人總結一下有下面幾點:

  1. 在面對一個問題時,首先應該確定清楚問題的具體領域、範圍。如果問題域不清不楚,不僅方向容易走偏,得不到正確結果,甚至在你誤打誤撞得到了比較正確的結果時,自己心裡也是沒底的,也說不清楚到底是怎麼一會事兒。
  2. 構建知識體系,將各個離散的知識點通過建立連線的方式聯絡起來,能極大的幫助理解和記憶。
  3. 工作、學習中不僅要解決具體的問題、掌握具體的知識,還要注意總結一些形而上的道理,嘗試去看到問題的本質。這些形而上的東西就像是中國傳統所說的道,如果一個人得了道,那他再去學習、使用具體的術的時候就會更快、更好。這就像《倚天屠龍記》中的張無忌一樣,練會了九陽神功後內功了得,其他具體的武功招式在他眼裡都一目瞭然、沒了祕密,一看就懂,一學就會。當然,對於我們這些搞工程應用的人來說,光悟了道而沒掌握到具體的術也不行。還是拿張無忌來類比,雖然他練了九陽神功後內功了得,但是剛出師的時候遇到滅絕師太這樣的高手,身上不會一招半式,也沒有實戰經驗,還是被逼得身處下風。因此內外兼修至關重要。
  4. 學習掌握好的思維方式可以受用一生,即使環境大變,也有辦法去應對。

相關文章