多線程基礎必要知識點!看了學習多線程事半功倍

前言

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

只有光頭才能變強java

回顧前面:編程

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

固然了,《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.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方法處理了之後才能夠完成對應的操做

這就致使了:咱們完成一個小小的功能,使用了多線程的目的是想要提升效率,但如今沒有把握得當,卻帶來嚴重的性能問題

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

這些都是咱們接下來要學習的地方:學習使用哪一種同步機制來實現線程安全,而且性能是提升了而不是下降了~

2、對象的發佈與逸出

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

發佈(publish) 使對象可以在當前做用域以外的代碼中使用

逸出(escape) 當某個不該該發佈的對象被髮布了

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

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

靜態域逸出:

public修飾get方法:

方法參數傳遞我就再也不演示了,由於把對象傳遞過去給另外的方法,已是逸出了~

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

逸出就是本不該該發佈對象的地方,把對象發佈了。致使咱們的數據泄露出去了,這就形成了一個安全隱患!理解起來是否是簡單了一丟丟?

2.1安全發佈對象

上面談到了好幾種逸出的狀況,咱們接下來來談談如何安全發佈對象

安全發佈對象有幾種常見的方式:

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

3、解決多線程遇到的問題

從上面咱們就能夠看到,使用多線程會把咱們的系統搞得挺複雜的。是須要咱們去處理不少事情,爲了防止多線程給咱們帶來的安全和性能的問題~

下面就來簡單總結一下咱們須要哪些知識點來解決多線程遇到的問題。

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、最後

正確使用多線程可以提升咱們應用程序的效率,同時給咱們會帶來很是多的問題,這些都是咱們在使用多線程以前須要注意的地方。

不管是不變性、可見性、原子性、線程封閉、委託這些都是實現線程安全的一種手段。要合理地使用這些手段,咱們的程序才能夠更加健壯!

能夠發現的是,上面在不少的地方說到了:。但我沒有介紹它,由於我打算留在下一篇來寫,敬請期待~~~

書上前4章花了65頁來說解,而我只用了一篇文章來歸納,這是遠遠不夠的,想要繼續深刻的同窗能夠去閱讀書籍~

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

參考資料:

  • 《Java核心技術卷一》
  • 《Java併發編程實戰》
  • 《計算機操做系統-湯小丹》

若是文章有錯的地方歡迎指正,你們互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同窗,能夠關注微信公衆號:Java3y。謝謝支持了!但願能多介紹給其餘有須要的朋友

文章的目錄導航

相關文章
相關標籤/搜索