多執行緒基礎必要知識點!看了學習多執行緒事半功倍

Java3y發表於2018-04-23

前言

不小心就鴿了幾天沒有更新了,這個星期回家咯。在學校的日子要努力一點才行!

只有光頭才能變強

回顧前面:

本文章的知識主要參考《Java併發程式設計實戰》這本書的前4章,這本書的前4章都是講解併發的基礎的。要是能好好理解這些基礎,那麼我們往後的學習就會事半功倍。

當然了,《Java併發程式設計實戰》可以說是非常經典的一本書。我是未能完全理解的,在這也僅僅是拋磚引玉。想要更加全面地理解我下面所說的知識點,可以去閱讀一下這本書,總的來說還是不錯的。

首先來預覽一下《Java併發程式設計實戰》前4章的目錄究竟在講什麼吧:

第1章 簡介

  • 1.1 併發簡史
  • 1.2 執行緒的優勢
  • 1.2.1 發揮多處理器的強大能力
  • 1.2.2 建模的簡單性
  • 1.2.3 非同步事件的簡化處理
  • 1.2.4 響應更靈敏的使用者介面
  • 1.3 執行緒帶來的風險
  • 1.3.1 安全性問題
  • 1.3.2 活躍性問題
  • 1.3.3 效能問題
  • 1.4 執行緒無處不在

ps:這一部分我就不講了,主要是引出我們接下來的知識點,有興趣的同學可翻看原書~

第2章 執行緒安全性

  • 2.1 什麼是執行緒安全性
  • 2.2 原子性
  • 2.2.1 競態條件
  • 2.2.2 示例:延遲初始化中的競態條件
  • 2.2.3 複合操作
  • 2.3 加鎖機制
  • 2.3.1 內建鎖
  • 2.3.2 重入
  • 2.4 用鎖來保護狀態
  • 2.5 活躍性與效能

第3章 物件的共享

  • 3.1 可見性
  • 3.1.1 失效資料
  • 3.1.2 非原子的64位操作
  • 3.1.3 加鎖與可見性
  • 3.1.4 Volatile變數
  • 3.2 釋出與逸出
  • 3.3 執行緒封閉
  • 3.3.1 Ad-hoc執行緒封閉
  • 3.3.2 棧封閉
  • 3.3.3 ThreadLocal類
  • 3.4 不變性
  • 3.4.1 Final域
  • 3.4.2 示例:使用Volatile型別來發布不可變物件
  • 3.5 安全釋出
  • 3.5.1 不正確的釋出:正確的物件被破壞
  • 3.5.2  不可變物件與初始化安全性
  • 3.5.3 安全釋出的常用模式
  • 3.5.4 事實不可變物件
  • 3.5.5 可變物件
  • 3.5.6 安全地共享物件

第4章 物件的組合

  • 4.1 設計執行緒安全的類
  • 4.1.1 收集同步需求
  • 4.1.2 依賴狀態的操作
  • 4.1.3 狀態的所有權
  • 4.2 例項封閉
  • 4.2.1 Java監視器模式
  • 4.2.2 示例:車輛追蹤
  • 4.3 執行緒安全性的委託
  • 4.3.1 示例:基於委託的車輛追蹤器
  • 4.3.2 獨立的狀態變數
  • 4.3.3 當委託失效時
  • 4.3.4 釋出底層的狀態變數
  • 4.3.5 示例:釋出狀態的車輛追蹤器
  • 4.4 在現有的執行緒安全類中新增功能
  • 4.4.1 客戶端加鎖機制
  • 4.4.2 組合
  • 4.5 將同步策略文件化

那麼接下來我們就開始吧~

一、使用多執行緒遇到的問題

1.1執行緒安全問題

在前面的文章中已經講解了執行緒【多執行緒三分鐘就可以入個門了!】,多執行緒主要是為了提高我們應用程式的使用率。但同時,這會給我們帶來很多安全問題

如果我們在單執行緒中以“順序”(序列-->獨佔)的方式執行程式碼是沒有任何問題的。但是到了多執行緒的環境下(並行),如果沒有設計和控制得好,就會給我們帶來很多意想不到的狀況,也就是執行緒安全性問題

因為在多執行緒的環境下,執行緒是交替執行的,一般他們會使用多個執行緒執行相同的程式碼。如果在此相同的程式碼裡邊有著共享的變數,或者一些組合操作,我們想要的正確結果就很容易出現了問題

簡單舉個例子:

  • 下面的程式在單執行緒中跑起來,是沒有問題的

public class UnsafeCountingServlet extends GenericServlet implements Servlet {
    private long count = 0;

    public long getCount() {
        return count;
    }

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {

        ++count;
        // To something else...
    }
}
複製程式碼

但是在多執行緒環境下跑起來,它的count值計算就不對了!

首先,它共享了count這個變數,其次來說++count;這是一個組合的操作(注意,它並非是原子性

  • ++count實際上的操作是這樣子的:
    • 讀取count值
    • 將值+1
    • 將計算結果寫入count

於是多執行緒執行的時候很可能就會有這樣的情況:

  • 當執行緒A讀取到count的值是8的時候,同時執行緒B也進去這個方法上了,也是讀取到count的值為8
  • 它倆都對值進行加1
  • 將計算結果寫入到count上。但是,寫入到count上的結果是9
  • 也就是說:兩個執行緒進來了,但是正確的結果是應該返回10,而它返回了9,這是不正常的!

如果說:當多個執行緒訪問某個類的時候,這個類始終能表現出正確的行為,那麼這個類就是執行緒安全的!

有個原則:能使用JDK提供的執行緒安全機制,就使用JDK的

當然了,此部分其實是我們學習多執行緒最重要的環節,這裡我就不詳細說了。這裡只是一個總覽,這些知識點在後面的學習中都會遇到~~~

1.3效能問題

使用多執行緒我們的目的就是為了提高應用程式的使用率,但是如果多執行緒的程式碼沒有好好設計的話,那未必會提高效率。反而降低了效率,甚至會造成死鎖

就比如說我們的Servlet,一個Servlet物件可以處理多個請求的,Servlet顯然是一個天然支援多執行緒的

又以下面的例子來說吧:


public class UnsafeCountingServlet extends GenericServlet implements Servlet {
    private long count = 0;

    public long getCount() {
        return count;
    }

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {

        ++count;
        // To something else...
    }
}
複製程式碼

從上面我們已經說了,上面這個類是執行緒不安全的。最簡單的方式:如果我們在service方法上加上JDK為我們提供的內建鎖synchronized,那麼我們就可以實現執行緒安全了。


public class UnsafeCountingServlet extends GenericServlet implements Servlet {
    private long count = 0;

    public long getCount() {
        return count;
    }

    public void synchronized service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {

        ++count;
        // To something else...
    }
}
複製程式碼

雖然實現了執行緒安全了,但是這會帶來很嚴重的效能問題

  • 每個請求都得等待上一個請求的service方法處理了以後才可以完成對應的操作

這就導致了:我們完成一個小小的功能,使用了多執行緒的目的是想要提高效率,但現在沒有把握得當,卻帶來嚴重的效能問題

在使用多執行緒的時候:更嚴重的時候還有死鎖(程式就卡住不動了)。

這些都是我們接下來要學習的地方:學習使用哪種同步機制來實現執行緒安全,並且效能是提高了而不是降低了~

二、物件的釋出與逸出

書上是這樣定義釋出和逸出的:

釋出(publish) 使物件能夠在當前作用域之外的程式碼中使用

逸出(escape) 當某個不應該釋出的物件被髮布了

常見逸出的有下面幾種方式:

  • 靜態域逸出
  • public修飾的get方法
  • 方法引數傳遞
  • 隱式的this

靜態域逸出:

多執行緒基礎必要知識點!看了學習多執行緒事半功倍

public修飾get方法:

多執行緒基礎必要知識點!看了學習多執行緒事半功倍

方法引數傳遞我就不再演示了,因為把物件傳遞過去給另外的方法,已經是逸出了~

下面來看看該書給出this逸出的例子

多執行緒基礎必要知識點!看了學習多執行緒事半功倍

逸出就是本不應該釋出物件的地方,把物件釋出了。導致我們的資料洩露出去了,這就造成了一個安全隱患!理解起來是不是簡單了一丟丟?

2.1安全釋出物件

上面談到了好幾種逸出的情況,我們接下來來談談如何安全釋出物件

安全釋出物件有幾種常見的方式:

  • 在靜態域中直接初始化public static Person = new Person();
    • 靜態初始化由JVM在類的初始化階段就執行了,JVM內部存在著同步機制,致使這種方式我們可以安全釋出物件
  • 對應的引用儲存到volatile或者AtomicReferance引用中
    • 保證了該物件的引用的可見性和原子性
  • 由final修飾
    • 該物件是不可變的,那麼執行緒就一定是安全的,所以是安全釋出~
  • 由鎖來保護
    • 釋出和使用的時候都需要加鎖,這樣才保證能夠該物件不會逸出

三、解決多執行緒遇到的問題

從上面我們就可以看到,使用多執行緒會把我們的系統搞得挺複雜的。是需要我們去處理很多事情,為了防止多執行緒給我們帶來的安全和效能的問題~

下面就來簡單總結一下我們需要哪些知識點來解決多執行緒遇到的問題。

3.1簡述解決執行緒安全性的辦法

使用多執行緒就一定要保證我們的執行緒是安全的,這是最重要的地方!

在Java中,我們一般會有下面這麼幾種辦法來實現執行緒安全問題:

  • 無狀態(沒有共享變數)
  • 使用final使該引用變數不可變(如果該物件引用也引用了其他的物件,那麼無論是釋出或者使用時都需要加鎖)
  • 加鎖(內建鎖,顯示Lock鎖)
  • 使用JDK為我們提供的類來實現執行緒安全(此部分的類就很多了)
    • 原子性(就比如上面的count++操作,可以使用AtomicLong來實現原子性,那麼在增加的時候就不會出差錯了!)
    • 容器(ConcurrentHashMap等等...)
    • ......
  • ...等等

3.2原子性和可見性

何為原子性?何為可見性?當初我在ConcurrentHashMap基於JDK1.8原始碼剖析中已經簡單說了一下了。不瞭解的同學可以進去看看。

3.2.1原子性

在多執行緒中很多時候都是因為某個操作不是原子性的,使資料混亂出錯。如果操作的資料是原子性的,那麼就可以很大程度上避免了執行緒安全問題了!

  • count++,先讀取,後自增,再賦值。如果該操作是原子性的,那麼就可以說執行緒安全了(因為沒有中間的三部環節,一步到位【原子性】~

原子性就是執行某一個操作是不可分割的, - 比如上面所說的count++操作,它就不是一個原子性的操作,它是分成了三個步驟的來實現這個操作的~ - JDK中有atomic包提供給我們實現原子性操作~

多執行緒基礎必要知識點!看了學習多執行緒事半功倍

也有人將其做成了表格來分類,我們來看看:

圖片來源:https://blog.csdn.net/eson_15/article/details/51553338

使用這些類相關的操作也可以進他的部落格去看看:

3.2.2可見性

對於可見性,Java提供了一個關鍵字:volatile給我們使用~

  • 我們可以簡單認為:volatile是一種輕量級的同步機制

volatile經典總結:volatile僅僅用來保證該變數對所有執行緒的可見性,但不保證原子性

我們將其拆開來解釋一下:

  • 保證該變數對所有執行緒的可見性
    • 在多執行緒的環境下:當這個變數修改時,所有的執行緒都會知道該變數被修改了,也就是所謂的“可見性”
  • 不保證原子性
    • 修改變數(賦值)實質上是在JVM中分了好幾步,而在這幾步內(從裝載變數到修改),它是不安全的

使用了volatile修飾的變數保證了三點

  • 一旦你完成寫入,任何訪問這個欄位的執行緒將會得到最新的值
  • 在你寫入前,會保證所有之前發生的事已經發生,並且任何更新過的資料值也是可見的,因為記憶體屏障會把之前的寫入值都重新整理到快取。
  • volatile可以防止重排序(重排序指的就是:程式執行的時候,CPU、編譯器可能會對執行順序做一些調整,導致執行的順序並不是從上往下的。從而出現了一些意想不到的效果)。而如果宣告瞭volatile,那麼CPU、編譯器就會知道這個變數是共享的,不會被快取在暫存器或者其他不可見的地方。

一般來說,volatile大多用於標誌位上(判斷操作),滿足下面的條件才應該使用volatile修飾變數:

  • 修改變數時不依賴變數的當前值(因為volatile是不保證原子性的)
  • 該變數不會納入到不變性條件中(該變數是可變的)
  • 在訪問變數的時候不需要加鎖(加鎖就沒必要使用volatile這種輕量級同步機制了)

參考資料:

3.3執行緒封閉

在多執行緒的環境下,只要我們不使用成員變數(不共享資料),那麼就不會出現執行緒安全的問題了。

就用我們熟悉的Servlet來舉例子,寫了那麼多的Servlet,你見過我們說要加鎖嗎??我們所有的資料都是在方法(棧封閉)上操作的,每個執行緒都擁有自己的變數,互不干擾

多執行緒基礎必要知識點!看了學習多執行緒事半功倍

在方法上操作,只要我們保證不要在棧(方法)上釋出物件(每個變數的作用域僅僅停留在當前的方法上),那麼我們的執行緒就是安全的

線上程封閉上還有另一種方法,就是我之前寫過的:ThreadLocal就是這麼簡單

使用這個類的API就可以保證每個執行緒自己獨佔一個變數。(詳情去讀上面的文章即可)~

3.4不變性

不可變物件一定執行緒安全的。

上面我們共享的變數都是可變的,正由於是可變的才會出現執行緒安全問題。如果該狀態是不可變的,那麼隨便多個執行緒訪問都是沒有問題的

Java提供了final修飾符給我們使用,final的身影我們可能就見得比較多了,但值得說明的是:

  • final僅僅是不能修改該變數的引用,但是引用裡邊的資料是可以改的!

就好像下面這個HashMap,用final修飾了。但是它僅僅保證了該物件引用hashMap變數所指向是不可變的,但是hashMap內部的資料是可變的,也就是說:可以add,remove等等操作到集合中~~~

  • 因此,僅僅只能夠說明hashMap是一個不可變的物件引用

  final HashMap<Person> hashMap = new HashMap<>();

複製程式碼

不可變的物件引用在使用的時候還是需要加鎖

  • 或者把Person也設計成是一個執行緒安全的類~
  • 因為內部的狀態是可變的,不加鎖或者Person不是執行緒安全類,操作都是有危險的

要想將物件設計成不可變物件,那麼要滿足下面三個條件:

  • 物件建立後狀態就不能修改
  • 物件所有的域都是final修飾的
  • 物件是正確建立的(沒有this引用逸出)

String在我們學習的過程中我們就知道它是一個不可變物件,但是它沒有遵循第二點(物件所有的域都是final修飾的),因為JVM在內部做了優化的。但是我們如果是要自己設計不可變物件,是需要滿足三個條件的。

多執行緒基礎必要知識點!看了學習多執行緒事半功倍

3.5執行緒安全性委託

很多時候我們要實現執行緒安全未必就需要自己加鎖,自己來設計

我們可以使用JDK給我們提供的物件來完成執行緒安全的設計:

多執行緒基礎必要知識點!看了學習多執行緒事半功倍

非常多的"工具類"供我們使用,這些在往後的學習中都會有所介紹的~~這裡就不介紹了

四、最後

正確使用多執行緒能夠提高我們應用程式的效率,同時給我們會帶來非常多的問題,這些都是我們在使用多執行緒之前需要注意的地方。

無論是不變性、可見性、原子性、執行緒封閉、委託這些都是實現執行緒安全的一種手段。要合理地使用這些手段,我們的程式才可以更加健壯!

可以發現的是,上面在很多的地方說到了:。但我沒有介紹它,因為我打算留在下一篇來寫,敬請期待~~~

書上前4章花了65頁來講解,而我只用了一篇文章來概括,這是遠遠不夠的,想要繼續深入的同學可以去閱讀書籍~

之前在學習作業系統的時候根據《計算機作業系統-湯小丹》這本書也做了一點點筆記,都是比較淺顯的知識點。或許對大家有幫助

參考資料:

  • 《Java核心技術卷一》
  • 《Java併發程式設計實戰》
  • 《計算機作業系統-湯小丹》

如果文章有錯的地方歡迎指正,大家互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同學,可以關注微信公眾號:Java3y。謝謝支援了!希望能多介紹給其他有需要的朋友

文章的目錄導航

相關文章