讀書筆記之《Java 併發編程的藝術》

1、多線程語義

  • 即便是單核處理器也支持多線程執行代碼,CPU 經過給每一個線程分配 CPU 時間片來執行任務,當前任務執行一個時間片後會切換到下一個任務,因此 CPU 經過不停的切換線程執行。java

  • 併發執行若是沒有達到必定的數量級,速度反而會比串行執行要慢。這是由於線程有建立和上下文切換的開銷。程序員

  • 如何減小線程建立和上下文切換的開銷?(「vmstat 1」 的 cs 參數,查看線程切換的次數)算法

    • 無鎖併發編程。多線程競爭鎖時,會引發上下文切換,因此多線程處理數據時,能夠用一些辦法來避免使用鎖,好比 CAS 算法、好比將數據的ID按照 Hash 算法取模分段,不一樣的線程處理不一樣段的數據。
    • 使用最少線程。避免建立不須要的線程,好比經過線程池等方式。
    • 協程。在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換。
  • 如何避免死鎖?數據庫

    • 避免一個線程同時獲取多個鎖。
    • 嘗試使用定時鎖,使用 lock.tryLock(timeout) 來替代使用內部鎖機制。
    • 對於數據庫鎖,加鎖和解鎖必須在一個數據庫鏈接裏,不然會出現解鎖失敗的狀況。
  • 在Java SE 1.6中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖(也稱互斥鎖)狀態,這幾個狀態會隨着競爭狀況逐漸升級。鎖能夠升級但不能降級,這種策略的目的是爲了提升得到鎖和釋放鎖的效率。有意思的是除了偏向鎖,JVM 實現鎖的方式都用了循環 CAS,即當一個線程想進入同步塊的時候使用循環 CAS 的方式來獲取鎖,當它退出同步塊的時候使用循環 CAS 釋放鎖。

    若是隻有一個線程進入同步代碼塊,那麼它首先會得到「偏向鎖」;當存在線程間競爭的時候,「偏向鎖」會撤銷,從而使用「輕量級鎖」;當線程經過自旋方式始終獲取不到「輕量級鎖」時(獲取鎖的線程執行時間過長等緣由),那麼「輕量級鎖」會膨脹成「重量級鎖」。編程

2、Java 內存模型

  • JMM(Java 內存模型)採用共享內存模型,經過控制主內存(Main Memory)與每一個線程的本地內存(Local Memory)之間的交互,來爲 Java 程序員提供內存可見性保證。緩存

  • 從 Java 源代碼到最終實際執行的指令序列,會分別經歷「編譯器優化的重排序」、「指令級並行的重排序」、「內存系統的重排序」,前一個屬於編譯器重排序,後兩個屬於處理器重排序,這些重排序會致使多線程程序出現內存可見性問題。爲了保證內存可見性,Java 編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。JMM 把內存屏障分爲四類:
    服務器

    • 對於會改變程序執行結果的重排序,JMM 要求編譯器和處理器必須禁止這種重排序。
    • 對於不會改變程序執行結果的重排序,JMM 對編譯器和處理器不作要求。
    • 所以,在程序員看來,程序執行的語義(執行結果)不會改變,happens-before 關係本質上和 as-if-serial 語義是一回事。
  • happens-before 是 JMM 最核心的概念。happens-before 規則對應於一個或多個編譯器和處理器重排序規則,JMM 經過 happens-before 規則隱藏了複雜的重排序規則以及這些規則的具體實現,而程序員在 happens-before 規則上編程,從而保證內存可見性。多線程

    • 程序順序規則:一個線程中的每一個操做,happens-before 於該線程中的任意後續操做。這個規則由 as-if-serial 語義保證。
    • 監視器鎖規則:對一個鎖的解鎖,happens-before 於隨後對這個鎖的加鎖。
    • volatile 變量規則:對一個 volatile 域的寫,happens-before 於任意後續對這個 volatile 域的讀。
    • 傳遞性:若是 A happens-before B,且 B happens-before C,那麼 A happens-before C。
  • 順序一致性內存模型是一個理論參考模型,JMM 和處理器內存模型在設計時一般會以順序一致性內存模型爲參照。併發

  • 順序一致性模型、JMM、處理器內存模型,內存模型設計由強變弱,由於越是追求性能,內存模型就會設計的越弱,以此減小內存模型對它們的束縛。app

3、synchronized、volatile和 final 的內存語義

  • 理解 volatile 特性的一個好方法是把對 volatile 變量的單個讀/寫,當作是使用同一個鎖對這些單個讀/寫操做作了同步。簡而言之,volatile 變量自身具備下列特性:

    • 可見性。對一個 volatile 變量的讀,老是能看到(任意線程)對這個 volatile 變量最後的寫入。
    • 原子性。對任意單個 volatile 變量的讀/寫具備原子性,但相似於 volatile++ 這種複合操做不具備原子性。
  • volatile 關鍵字如何保證可見性?volatile 的內存語義?

    • 當對 volatile 變量寫的時候,會將當前處理器緩存行的數據寫回到系統內存。
    • 當對 volatile 變量讀的時候,會將當前處理器緩存行的數據置爲無效,所以要從系統內存中讀取變量值。
  • volatile 關鍵字如何保證有序性?

    • 在每一個 volatile 寫操做的前面插入一個 StoreStore 屏障(禁止上面的普通寫和下面的 volatile 寫重排序)。
    • 在每一個 volatile 寫操做的後面插入一個 StoreLoad 屏障(防止上面的 volatile 寫與下面可能有的 volatile 讀/寫重排序)。
    • 在每一個 volatile 讀操做的後面插入一個 LoadLoad 屏障(禁止下面全部的普通讀操做和上面的 volatile 讀重排序)。
    • 在每一個 volatile 讀操做的後面插入一個 LoadStore 屏障(禁止下面全部的普通寫操做和上面的 volatile 讀重排序)。
  • 爲何 JDK 文檔說 CAS 同時具備 volatile 讀和 volatile 寫的內存語義?

    • 編譯器不會對 volatile 讀與 volatile 讀後面的任意內存操做重排序。
    • 編譯器不會對 volatile 寫與 volatile 寫前面的任意內存操做重排序。
    • CAS(compare and swap)操做意味着要對 volatile 變量先讀後寫,要同時具有 volatile 讀和寫的語義,所以編譯器不能對 CAS 與 CAS 前面和後面的任意內存操做重排序。
  • Synchonized 關鍵字的原理?JVM 基於進入和退出 Monitor 對象來實現方法同步和代碼塊同步,但二者的實現細節不同。代碼塊同步是使用 monitorenter 和 monitorexit 指令實現的,而方法同步是使用另一種方式實現的,細節在 JVM 規範裏並無詳細說明。

  • 鎖的釋放和獲取的內存語義?

    • 當線程釋放鎖時,JMM 會把該線程對應的本地內存中的共享變量刷新到主內存中。(和 volatile 寫的內存語義相同)
    • 當線程獲取鎖時,JMM 會把該線程對應的本地內存置爲無效,所以要從系統內存中讀取變量值。(和 volatile 讀的內存語義相同)
  • 對於 final 變量,編譯器和處理器要遵照兩個重排序規則(對象引用不在構造函數中「溢出」的狀況下):

    • 在構造函數內對一個 final 變量的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操做之間不能重排序。
    • 初次讀一個包含 final 變量的對象引用,與隨後初次讀這個 final 變量,這兩個操做之間不能重排序。
  • final 變量的內存語義?

    • 寫 final 變量的重排序規則會要求編譯器在 final 變量的寫以後,構造函數 return 以前插入一個 StoreStore 屏障。
    • 讀 final 變量的重排序規則會要求編譯器在讀 final 變量的操做前面插入一個 LoadLoad 屏障。
  • 在構造函數內部,不能讓這個被構造對象的引用爲其餘線程所見,也就是對象引用不能在構造函數中「溢出」。由於 JMM 沒法保證在構造函數中「對變量的寫」和「被構造對象的引用」 這二者之間是否會被重排序。

4、其餘

  • jps 和 jstack 命令?

    • jps:查看 JVM 進程信息
    • jstack:查看某個JVM進程的堆棧信息
    • 用 jstack 命令dump線程信息,看看 pid 爲 18023 的進程裏的線程都在作什麼。
    jstack 18023 > /home/wwwroot/dump18023
    • 統計線程分別處於什麼狀態
    grep java.lang.Thread.State dump18023 | awk '{print $2$3$4$5}' | sort | uniq -c
  • 可使用ODPS、Hadoop 或者本身搭建服務器集羣來解決硬件資源限制的問題。

  • 一個對象的引用佔 4 個字節。

相關文章
相關標籤/搜索