天天看點

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

前言

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

隻有光頭才能變強

回顧前面:

  • 多線程三分鐘就可以入個門了!
  • Thread源碼剖析

本文章的知識主要參考《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這種輕量級同步機制了)

參考資料:

  • http://www.cnblogs.com/Mainz/p/3556430.html
  • https://www.cnblogs.com/Mainz/p/3546347.html
  • http://www.dataguru.cn/java-865024-1-1.html

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。為了大家友善,剛建立了一下qq群:742919422,大家也可以去交流交流。謝謝支援了!希望能多介紹給其他有需要的朋友

文章的目錄導航:

  • https://zhongfucheng.bitcron.com/post/shou-ji/wen-zhang-dao-hang

更多的文章可往:文章的目錄導航