多線程

多線程三大特性

  1. 原子性:是指一個操作不可中斷。但對于處理器,一個操作(比如b++)都會被解釋成多條指令執(zhí)行,沒有同步限制的話,操作的原子性會破壞。
  2. 可見性:指的是一個線程修改的值,另外一個線程能及時看到。但是不管是處理器的高速緩存和主內(nèi)存,還是JMM的工作內(nèi)存與主內(nèi)存,都會導致線程修改的同步延遲現(xiàn)象,無法保證可見性。
  3. 有序性:指程序有序執(zhí)行。但是在編譯器和處理器的重排序、多線程并發(fā)執(zhí)行的環(huán)境下,一個看似順序執(zhí)行的代碼在真實執(zhí)行時都可能是亂序的。

重排序

  1. 編譯器重排序
  2. 處理器指令重排序

如何解決:

  1. 原子性:樂觀鎖(CAS)和悲觀鎖(synchronized和ReentrantLock)
  2. 可見性:volatile、final、鎖
  3. 有序性:volatile、final、鎖

多線程編程的問題

優(yōu)點:
資源利用率更好
程序設(shè)計在某些情況下更簡單
程序響應更快

代價:
設(shè)計更復雜
上下文切換的開銷
增加內(nèi)存資源消耗

競態(tài)和臨界區(qū)
線程安全和共享資源
不可變性和只讀的對象時線程安全的,比如String對象不可變所以是線程安全的

java的線程模型

程序?qū)崿F(xiàn)線程主要有3種方式

  1. 依賴內(nèi)核線程實現(xiàn),由內(nèi)核直接來調(diào)度線程,程序一般不會直接去使用內(nèi)核線程(Kernel-Level Thread,KLT),而是調(diào)用內(nèi)核的輕量級進程(Light Weight Process,LWP)接口,每個輕量級進程都由一個內(nèi)核線程支持。這種輕量級進程與內(nèi)核線程之間1:1的關(guān)系稱為一對一的線程模型。
image.png
  1. 使用用戶線程實現(xiàn),指的是用戶線程完全建立在用戶空間的線程庫上,系統(tǒng)內(nèi)核不能感知線程存在的實現(xiàn)。這種進程與用戶線程之間1:N的關(guān)系稱為一對多的線程模型。
image.png
  1. 使用用戶線程加輕量級進程混合實現(xiàn),在這種混合模式中,用戶線程與輕量級進程的數(shù)量比是不定的,即為N:M的關(guān)系。
image.png

java使用的就是第一種,依賴內(nèi)核線程實現(xiàn)

java的線程調(diào)度:

線程調(diào)度是指系統(tǒng)為線程分配處理器使用權(quán)的過程,主要調(diào)度方式有兩種,分別是協(xié)同式線程調(diào)度(Cooperative Threads-Scheduling)和搶占式線程調(diào)度(Preemptive Threads-Scheduling)。

  1. 協(xié)同式調(diào)度
    如果使用協(xié)同式調(diào)度的多線程系統(tǒng),線程的執(zhí)行時間由線程本身來控制,線程把自己的工作執(zhí)行完了之后,要主動通知系統(tǒng)切換到另外一個線程上。協(xié)同式多線程的最大好處是實現(xiàn)簡單,而且由于線程要把自己的事情干完后才會進行線程切換,切換操作對線程自己是可知的,所以沒有什么線程同步的問題。Lua語言中的“協(xié)同例程”就是這類實現(xiàn)。它的壞處也很明顯:線程執(zhí)行時間不可控制,甚至如果一個線程編寫有問題,一直不告知系統(tǒng)進行線程切換,那么程序就會一直阻塞在那里。很久以前的Windows 3.x系統(tǒng)就是使用協(xié)同式來實現(xiàn)多進程多任務的,相當不穩(wěn)定,一個進程堅持不讓出CPU執(zhí)行時間就可能會導致整個系統(tǒng)崩潰。

  2. 搶占式調(diào)度
    如果使用搶占式調(diào)度的多線程系統(tǒng),那么每個線程將由系統(tǒng)來分配執(zhí)行時間,線程的切換不由線程本身來決定(在Java中,Thread.yield()可以讓出執(zhí)行時間,但是要獲取執(zhí)行時間的話,線程本身是沒有什么辦法的)。在這種實現(xiàn)線程調(diào)度的方式下,線程的執(zhí)行時間是系統(tǒng)可控的,也不會有一個線程導致整個進程阻塞的問題,Java使用的線程調(diào)度方式就是搶占式調(diào)度。在JDK后續(xù)版本中有可能會提供協(xié)程(Coroutines)方式來進行多任務處理。與前面所說的Windows 3.x的例子相對,在Windows 9x/NT內(nèi)核中就是使用搶占式來實現(xiàn)多進程的,當一個進程出了問題,我們還可以使用任務管理器把這個進程“殺掉”,而不至于導致系統(tǒng)崩潰。

java使用的是搶占式線程調(diào)度。

死鎖和避免死鎖

死鎖的四個條件 :
1) 互斥條件:一個資源每次只能被一個進程使用。
2) 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
3) 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
4) 循環(huán)等待條件:若干進程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系。

避免死鎖的方法:
1)加鎖順序:比如銀行轉(zhuǎn)賬要鎖定兩個賬戶,我們可以按照固定順序,先鎖定賬號ID大的,再鎖定賬號ID小的,這樣所有線程都按照同樣的順序加解鎖就可以避免死鎖。
2)加鎖時限:比如使用顯式鎖的tryLock(long time)方法加鎖,如果固定時間內(nèi)無法獲取鎖,則返回false,不阻塞線程;
3)死鎖檢測 :比如使用顯式鎖的tryLock()方法,如果加鎖失敗,則釋放已經(jīng)持有的其它鎖,再才重試。
4)避免一個線程同時獲取多個鎖,避免一個線程的鎖內(nèi)部同時占用多個資源。

鎖死

鎖死的情況是:比如,線程1鎖定了對象A,同時調(diào)用wait()方法等待信號,線程2要先對對象A加鎖才能給線程1發(fā)送notify信號喚醒線程1。

鎖死跟死鎖很像,區(qū)別在于:

  • 死鎖中,二個線程都在等待對方釋放鎖。
  • 鎖死中,線程1持有鎖A,同時等待從線程2發(fā)來的信號,線程2需要鎖A來發(fā)信號給線程1。

線程饑餓

在Java中,下面三個常見的原因會導致線程饑餓:

  1. 高優(yōu)先級線程吞噬所有的低優(yōu)先級線程的CPU時間。
  2. 線程被永久堵塞在一個等待進入同步塊的狀態(tài),因為其他線程總是能在它之前持續(xù)地對該同步塊進行訪問。
  3. 線程在等待一個本身(在其上調(diào)用wait())也處于永久等待完成的對象,因為其他線程總是被持續(xù)地獲得喚醒。

解決問題,使用ReentranLock的公平鎖。

悲觀鎖和樂觀鎖

概念區(qū)分
悲觀鎖:悲觀的認為對于同一個數(shù)據(jù)的并發(fā)操作,一定是會發(fā)生修改的,因此對于同一個數(shù)據(jù)的并發(fā)操作采取加鎖的形式。在Java中,各種鎖(synchronized、ReentrantLock)基本是屬于悲觀的。
樂觀鎖:樂觀的認為對于同一個數(shù)據(jù)的并發(fā)操作,是不會發(fā)生修改的,在更新數(shù)據(jù)的時候,會采用嘗試更新,不斷重新的方式更新數(shù)據(jù)。在Java中的使用,是無鎖編程,常常采用的是CAS算法,典型的例子就是原子類,通過CAS自旋實現(xiàn)原子操作的更新。

使用場景
悲觀鎖:線程阻塞的,并發(fā)時會引起線程上下文切換的,所以適合寫比較多的場景。另外,在線程沖突多的情況下,自旋式的樂觀鎖(CAS)消耗cpu性能太嚴重,這時候更適合使用悲觀鎖(synchronized)。
樂觀鎖:不阻塞的,并發(fā)時會失敗的線程會自旋式的重試,消耗cpu資源,所以適合讀多寫少、線程并發(fā)少的場景。

隱式鎖(synchronizied)和顯式鎖(java.util.Lock)

概念區(qū)分

  • 隱式鎖,不需要顯式進行加、解鎖操作,只需使用synchronizied關(guān)鍵字修飾代碼。然后synchronizied是內(nèi)置的,底層實現(xiàn)機制是對象監(jiān)控器,屬于JVM級別的鎖。
  • 顯式鎖,必須要顯式調(diào)用加鎖和解鎖操作方法。Lock是顯式鎖接口,具體的實現(xiàn)類有ReentrantLock、ReentrantReadWriteLock,底層實現(xiàn)是基于AQS同步器,屬于接口級別的鎖。

顯式鎖的優(yōu)點:

  1. 支持響應線程中斷,而synchronized無法中斷一個等待獲取鎖的線程。
  2. 可定時等待鎖
  3. 支持condition條件來await()/signal()線程,比synchronized的await()/notify()方法更加靈活,不局限于通知持有鎖的對象,而且,Condition因為是一個等待隊列,可以確保等待的線程能夠按順序被喚醒。
  4. 支持公平和不公平鎖,而synchronized是不公平的。
  5. 支持共享鎖和排他鎖,而synchronized是排他的。
  6. 性能比synchronized好,java 6.0后,jvm對內(nèi)置鎖進行優(yōu)化,差距變小了很多。

顯式鎖的問題:

  1. 必須要在finally塊里手動釋放鎖,如果忘記的話會很危險,而synchronizied是自動加解鎖;
  2. 在java 5時,使用Lock鎖,JVM生成線程轉(zhuǎn)儲無法獲取阻塞對象的信息,無法定位死鎖等異常。從java 6開始,LockSupport才支持存儲鎖定對象,以便dump時獲取鎖定信息。

共享鎖和互斥鎖(排他鎖)

互斥鎖是獨占式的,每次只能有一個線程占用資源。
共享鎖是非獨占式的。
synchronized和ReentrantLockd都是排他鎖。
ReentrantReadWriteLock的寫鎖是一個支持可重入的排他鎖。
ReentrantReadWriteLock的讀鎖是一個支持可重入的共享鎖,允許多個線程共享,從而提高了線程的并發(fā)量。
ReentrantReadWriteLock也是使用同步器AQS實現(xiàn)的,特別的地方是,它使用一個共享int變量(32位),同時存儲讀和寫兩種狀態(tài)(高16位表示讀,低16位表示寫)。

可重入鎖

可重入性,意思就是一個線程可以對一個資源(對象)重復加鎖。synchronized和ReentrantLockd都是可重入鎖。

實現(xiàn)原理:每個鎖關(guān)聯(lián)一個線程持有者和一個計數(shù)器。當計數(shù)器為0時表示該鎖沒有被任何線程持有,那么任何線程都都可能獲得該鎖而調(diào)用相應方法。當一個線程請求鎖成功后,會記下持有鎖的線程,并將計數(shù)器計為1。此時其他線程請求該鎖,則必須等待。而該持有鎖的線程如果再次請求這個鎖,就可以再次拿到這個鎖,同時計數(shù)器會遞增。線程每次釋放鎖時,計數(shù)器會遞減,直到第n次釋放,計數(shù)器為0后才真正釋放該鎖,其它線程才有機會加鎖。

公平鎖和不公平鎖

公平鎖是按照fifo原則分配線程的,而不公平鎖是搶占式的,誰搶到就是誰的。
公平鎖是先來先得,但代價是不可避免的線程上下切換。
不公平鎖因為不公平的競爭,可能造成有線程長久等待——“饑餓”,但減少了線程切換,保證更大的吞吐量。
默認情況下,使用不公平鎖,除非有特別需要且鎖的時間比較長,才考慮使用公平鎖。

偏向鎖、輕量級鎖、自旋鎖、重量級鎖

這三種鎖是指鎖的狀態(tài),并且是針對Synchronized。
以前同步使用的鎖都是重量級鎖,Java 6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入“偏向鎖”和“輕量級鎖”,鎖一共有4種狀態(tài),從低到高:無所狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)、重量級鎖狀態(tài),這幾種鎖狀態(tài)會隨著競爭情況而升級。鎖可以升級但不能降級,目的是為了提高獲得和釋放鎖的效率。

  • 偏向鎖:是指一段同步代碼一直被一個線程所訪問,那么該線程會自動獲取鎖。降低獲取鎖的代價。
  • 輕量級鎖:是指當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。
  • 重量級鎖:是指當鎖為輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續(xù)下去,當自旋一定次數(shù)的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹為重量級鎖。重量級鎖會讓其他申請的線程進入阻塞,性能降低。
image.png

對象頭和同步塊synchronized

在HotSpot JVM實現(xiàn)中,synchronized內(nèi)置鎖有個專門的名字:對象監(jiān)視器。
synchronized的狀態(tài)是通過對象監(jiān)視器在對象頭中的字段來表明的。

對象頭:

對象頭-Mark Word

synchronized的用法:

image.png

對象的wait和notify方法

對象的wait()、wait(long timeout)、notify()、notifyAll方法的使用注意事項:

  1. 線程必須在synchronize的語句塊中調(diào)用wait或者notify方法
  2. 調(diào)用wait或者notify方法的對象必須時synchronize鎖定的對象
  3. notify()不能保證喚醒哪個線程
  4. 不要對String對象或者全局對象調(diào)用wait方法,因為JVM/Compiler 在內(nèi)部將常量的String變成相同的對象。
image.png

存在問題

  1. 過早喚醒:比如調(diào)用notifyAll()把滿足條件和未沒有滿足條件的線程都喚醒;
  2. 信號丟失(Missed Signals):比如notify喚醒方法在wait()方法前被調(diào)用了,導致線程執(zhí)行wait()后永遠等待;
  3. 虛假喚醒(Spurious Wakeups):等待線程也有可能會在沒有任何線程調(diào)用notify()的情況下被喚醒,這種現(xiàn)象是由于操作系統(tǒng)詭異而出現(xiàn)的;

有一種規(guī)范的寫法,避免上面的過早喚醒和虛假喚醒問題:

synchronized(someobject){
  while(保護條件不成立){
    //暫停線程
    someobject.wait();
  }
  //保護條件成立后,執(zhí)行操作
  dosomething();
}

synchronized(someobject){
  //修改,使保護條件成立
  updateSharedState();
  //喚醒線程
  someobject.notify();
}

鎖優(yōu)化

  1. 減少鎖的時間
  2. 減少鎖的粒度,LongAdder、ConcurrentHashMap、LinkedBlockingQueue都是將一個鎖拆成多個鎖,增加并行度
  3. 讀寫分離
  4. 鎖粗化
  5. 使用CAS

線程狀態(tài)

image.png
image.png

Daemon守護線程

Thread.setDaemon(true)可以設(shè)置一個線程為守護線程。
守護線程是一種支持線程,當java虛擬機不存在主線程時,虛擬機將退出,守護線程將會終止。
所以,不能依賴守護線程finally塊內(nèi)的內(nèi)容來確保執(zhí)行關(guān)閉或清理資源。

線程中斷

調(diào)用線程的interrupt()方法對其進行中斷操作。
線程有兩種響應中斷的:

  1. 線程本身通過isInterrupted()方法判斷是否被中斷,為true表示被中斷
  2. 部分java的方法,比如sleep()、Lock.await(),響應到中斷,首先會清除中斷標識(設(shè)為false),然后拋出InterruptedException異常,這時調(diào)isInterrupted()是false。

如何優(yōu)雅地終止一個線程:

image.png

上面的案例,通過自定義變量和中斷標識判斷的方式能夠使線程終止時有機會去清理資源,而不是武斷終止線程,這種做法顯得更加安全和優(yōu)雅。

線程通信

  1. volatile和synchronized,用于線程同步,保證變量訪問的可見性和排他性。
  2. 等待/通知機制,wait()/notify系列方法,用于阻塞和喚醒線程
  3. 管道輸入/輸出流,PipeOutputStream、PipeInputStream、PipeReader、PipeWriter可以用于線程之間數(shù)據(jù)傳輸。
  4. Thread1.join()方法,意思是Thread1插入,當前線程先阻塞,等Thread1執(zhí)行完才執(zhí)行。join的背后實現(xiàn)機制其實是wait()/notifyAll。
  5. 過期的suspend()、resume()、stop(),可以暫停、恢復、停止線程,但是這幾個方法調(diào)用后,線程不會釋放資源(比如鎖),所以已經(jīng)被wait()/notify替代。

ThreadLocal

ThreadLocal線程變量,支持泛型,是一個以ThreadLocal對象為鍵、任意對象為值的存儲結(jié)構(gòu)。也叫線程副本,即ThreadLocal變量對每個線程都有一個獨立的副本,不會互相影響。

ThreadLocal適合使用在方法調(diào)用耗時統(tǒng)計,可以跨方法,甚至跨類統(tǒng)計,因為它是以線程為統(tǒng)計路徑的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文首發(fā)于我的個人博客:尾尾部落 本文是我刷了幾十篇一線互聯(lián)網(wǎng)校招java后端開發(fā)崗位的面經(jīng)后總結(jié)的多線程相關(guān)題目...
    繁著閱讀 2,125評論 0 7
  • 進程和線程 進程 所有運行中的任務通常對應一個進程,當一個程序進入內(nèi)存運行時,即變成一個進程.進程是處于運行過程中...
    勝浩_ae28閱讀 5,257評論 0 23
  • Java多線程學習 [-] 一擴展javalangThread類 二實現(xiàn)javalangRunnable接口 三T...
    影馳閱讀 3,106評論 1 18
  • 該文章轉(zhuǎn)自:http://blog.csdn.net/evankaka/article/details/44153...
    加來依藍閱讀 7,465評論 3 87
  • 生命中一切重要的決定都來之不易!尋尋覓覓,才知這二十七年都是為了走向你而鋪墊;相遇相惜,執(zhí)著的、求索的心之所向。春...
    eb1af16c57c3閱讀 254評論 0 1

友情鏈接更多精彩內(nèi)容