天天看點

Java高并發程式設計實戰,那些年學過的鎖

作者:儒雅程式員阿鑫

目錄

  • 1、線程組和線程池有啥差別?
  • 2、使用者線程與守護線程
  • 3、多線程鎖的更新原理是什麼?
  • 4、Java多線程思維導圖

一、程序與線程

程式本身是靜态的,是衆多代碼的組合産物,代碼儲存在檔案中。如果程式要運作,則需要将程式加載到記憶體中,通過編譯器将其編譯成計算機能夠了解的方式運作。

如果想啟動一個Java程式,先要建立一個JVM程序。

程序是作業系統進行資源配置設定的最小機關,在一個程序中可以建立多個線程。多個線程各自擁有獨立的局部變量、線程堆棧和程式計數器,能夠通路共享的資源。

  1. 程序是作業系統配置設定資源的最小機關,線程是CPU排程的最小機關;
  2. 一個程序中可以包含多個線程;
  3. 程序與程序之間是相對獨立的,程序中的線程之間并不完全獨立,可以共享程序中的堆記憶體、方法區記憶體、系統資源等;
  4. 程序上下文的切換要比線程的上下文切換慢很多;
  5. 某個程序發生異常,不會對其它程序造成影響,但,某個線程發生異常,可能會對此程序中的其它線程造成影響;
Java高并發程式設計實戰,那些年學過的鎖

二、線程組與線程池

1、線程組

線程組可以管理多個線程,顧名思義,線程組,就是把功能相似的線程放到一個組裡,友善管理。

package com.guor.test;

public class ThreadGroupTest {
 
    public static void main(String[] args) {
 
        // 建立線程組
        ThreadGroup threadGroup = new ThreadGroup("nezha");

        Thread thread = new Thread(threadGroup,()->{
 
            // 線程組名稱
            String groupName = Thread.currentThread().getThreadGroup().getName();
            // 線程名稱
            String threadName = Thread.currentThread().getName();
            System.out.println("groupName -- "+groupName);//groupName -- nezha
            System.out.println("threadName -- "+threadName);//threadName -- thread
        },"thread");

        thread.start();
    }
}           

2、線程組和線程池有啥差別?

  1. 線程組中的線程可以跨線程修改資料,而線程組和線程組之間不可以跨線程修改資料;
  2. 線程池就是建立一定數量的線程,批量處理任務,目前任務執行完畢後,線程又可以去執行其它任務,通過重用已存在的線程,降低線程建立和銷毀造成的消耗;
  3. 線程池可以有效的管理線程的數量,避免線程的無限制建立,線程是很耗費系統資源的,動不動就會産生OOM,并且會造成cpu過度切換,也有強大的拓展功能,比如延時定時線程池。

三、使用者線程與守護線程

在Java中有兩類線程:User Thread(使用者線程)、Daemon Thread(守護線程) 。

使用者線程是最常見的線程,比如通過main方法啟動,就會建立一個使用者線程。

Daemon的作用是為其他線程的運作提供便利服務,守護線程最典型的應用就是 GC (垃圾回收器),它就是一個很稱職的守護者。

JVM中的垃圾回收、JIT編譯器線程就是最常見的守護線程。

隻要有一個使用者線程在運作,守護線程就會一直運作。隻有所有的使用者線程都結束的時候,守護線程才會退出。

編寫代碼時,也可以通過 thread.setDaemon(true) 指定線程為守護線程。

Thread daemonTread = new Thread();
 
  // 設定 daemonThread 為 守護線程,預設false
 daemonThread.setDaemon(true);
 
 // 驗證目前線程是否為守護線程,傳回 true 則為守護線程
 daemonThread.isDaemon();           

守護線程的注意事項:

  1. thread.setDaemon(true) 要在 thread.start() 之前設定,否則會抛出 IllegalThreadStateException 異常。你不能把正在運作的線程設定為守護線程;
  2. 在守護線程中産生的新線程也是守護線程;
  3. 讀寫操作或者計算邏輯不可以設定為守護線程;

四、并行與并發

并行指當多核CPU中的一個CPU執行一個線程時,其它CPU能夠同時執行另一個線程,兩個線程之間不會搶占CPU資源,可以同時運作。

并發指在一段時間内CPU處理多個線程,這些線程會搶占CPU資源,CPU資源根據時間片周期在多個線程之間來回切換,多個線程在一段時間内同時運作,而在同一時刻不是同時運作的。

并行和并發的差別?

  1. 并行指多個線程在一段時間的每個時刻都同時運作,并發指多個線程在一段時間内同時運作(不是同一時刻,一段時間内交叉執行)
  2. 并行的多個線程不會搶占系統資源,并發的多個線程會搶占系統資源;
  3. 并行是多CPU的産物,單核CPU中隻有并發,沒有并行;
Java高并發程式設計實戰,那些年學過的鎖

五、悲觀鎖與樂觀鎖

1、悲觀鎖

悲觀鎖在一個線程進行加鎖操作後使得該對象變為該線程的獨有對象,其它的線程都會被悲觀鎖阻攔在外,無法操作。

悲觀鎖的缺陷:

  1. 一個線程獲得悲觀鎖後其它線程必須阻塞。
  2. 線程切換時要不停地釋放鎖和擷取鎖,開銷巨大。
  3. 當一個低優先級的線程獲得悲觀鎖後,高優先級的線程必須等待,導緻線程優先級倒置,synchronized鎖是一種典型的悲觀鎖。

2、樂觀鎖

樂觀鎖認為對一個對象的操作不會引發沖突,是以每次操作都不進行加鎖,隻是在最後送出更改時驗證是否發生沖突,如果沖突則再試一遍直到成功為止,這個嘗試的過程被稱為自旋。樂觀鎖其實并沒有加鎖,但樂觀鎖也引入了諸如ABA、自旋次數過多等問題。

樂觀鎖一般會采用版本号機制,先讀取資料的版本号,在寫資料時比較版本号是否一緻,如果一緻,則更新資料,否則再次讀取版本号,直到版本号一緻。

Java中的樂觀鎖都是基于CAS自旋實作的。

1、什麼是CAS?

Compare And Swap。

CAS(V, A, B) ,記憶體值V,期待值A, 修改值B(V 是否等于 A, 等于執行, 不等于将B賦給V)

Java高并發程式設計實戰,那些年學過的鎖

2、CAS帶來的問題

(1)ABA問題

CAS操作的流程為:

  1. 讀取原值。
  2. 通過原子操作比較和替換。
  3. 雖然比較和替換是原子性的,但是讀取原值和比較替換這兩步不是原子性的,期間原值可能被其它線程修改。

ABA問題有些時候對系統不會産生問題,但是有些時候卻也是緻命的。

ABA問題的解決方法是對該變量增加一個版本号,每次修改都會更新其版本号。JUC包中提供了一個類AtomicStampedReference,這個類中維護了一個版本号,每次對值的修改都會改動版本号。

(2)自旋次數過多

CAS操作在不成功時會重新讀取記憶體值并自旋嘗試,當系統的并發量非常高時即每次讀取新值之後該值又被改動,導緻CAS操作失敗并不斷的自旋重試,此時使用CAS并不能提高效率,反而會因為自旋次數過多還不如直接加鎖進行操作的效率高。

(3)隻能保證一個變量的原子性

當對一個變量操作時,CAS可以保證原子性,但同時操作多個變量時CAS就無能為力了。

可以封裝成對象,再對對象進行CAS操作,或者直接加鎖。

七、那些年學過的鎖

1、公平鎖與非公平鎖

  • 公平鎖:按照線程在隊列中的排隊順序,先到者先拿到鎖
  • 非公平鎖:當線程要擷取鎖時,無視隊列順序直接去搶鎖,誰搶到就是誰的

2、獨占鎖與共享鎖

  • 獨占鎖:當多個線程則争搶鎖的過程中,無論是讀操作還是寫操作,隻能有一個線程擷取到鎖,其他線程阻塞等待。
  • 共享鎖:允許多個線程同時擷取共享資源,采取的是樂觀鎖的機制,共享鎖限制寫寫操作、讀寫操作,但不會限制讀讀操作。

3、可重入鎖與不可重入鎖

  • 可重入鎖:一個線程可以多次占用同一個鎖,但是解鎖時,需要執行相同次數的解鎖操作;
  • 不可重入鎖:一個線程不能多次占用同一個鎖;

八、死鎖、活鎖、餓死

多個線程互相持有對方需要的資源,導緻多個線程互相等待,無法繼續執行後續任務。

2、産生死鎖的4個必要條件

  1. 互斥條件:指程序對所配置設定到的資源進行排它性使用,在一段時間内某資源隻由一個程序占用,如果此時還有其他程序請求資源,則請求者隻能等待,直至占有資源的程序被釋放;
  2. 請求和保持條件:指程序已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它程序占有,此時請求程序阻塞,但又對自己已獲得的其它資源保持不放。
  3. 不可剝奪:指程序已獲得的資源,在未使用完之前,不能被剝奪,隻能在使用完時由自己釋放。
  4. 循環等待:一個等待一個,産生了一個閉環。

饑餓指的是線程由于無法擷取需要的資源而無法繼續執行。

4、産生饑餓的主要原因

  1. 高優先級的線程不斷搶占資源,低優先級的線程搶不到;
  2. 某個線程一直不釋放資源,導緻其他線程無法擷取資源;

5、如何避免饑餓

  1. 使用公平鎖配置設定資源;
  2. 為程式配置設定足夠的系統資源;
  3. 避免持有鎖的線程長時間占用鎖;

活鎖指的是多個線程同時搶占同一個資源時,都主動将資源讓給其他線程使用,導緻這個資源在多個線程之間來回切換,導緻線程因無法擷取相應資源而無法繼續執行的現象。

7、如何避免活鎖

可以讓多個線程随機等待一段時間後再次搶占資源,這樣會大大減少線程搶占資源的沖突次數,有效避免活鎖的産生。

九、多線程鎖的更新原理是什麼?

鎖的狀态總共有四種,無鎖狀态、偏向鎖、輕量級鎖、重量級鎖。

Java高并發程式設計實戰,那些年學過的鎖

随着鎖的競争,鎖可以從偏向鎖更新到輕量級鎖,再更新到重量級鎖。但是鎖的更新是單向的,隻能更新不能降級。

沒有對資源進行鎖定,所有的線程都能通路并修改同一個資源,但同時隻有一個線程能修改成功,其它修改失敗的線程會不斷重試直到修改成功。

無鎖總是假設對共享資源的通路沒有沖突,線程可以不停執行,無需加鎖,無需等待,一旦發現沖突,無鎖政策則采用一種稱為CAS的技術來保證線程執行的安全性,CAS是無鎖技術的關鍵。

2、偏向鎖

對象的代碼一直被同一線程執行,不存在多個線程競争,該線程在後續執行中自動擷取鎖,降低擷取鎖帶來的性能開銷。偏向鎖,指的是偏向第一個加鎖線程,該線程是不會主動釋放偏向鎖的,隻有當其他線程嘗試競争偏向鎖才會被釋放。

偏向鎖的撤銷,需要在某個時間點上沒有位元組碼正在執行時,先暫停偏向鎖的線程,然後判斷鎖對象是否處于被鎖定狀态,如果線程不處于活動狀态,則将對象頭設定成無鎖狀态,并撤銷偏向鎖。

如果線程處于活動狀态,更新為輕量級鎖的狀态

3、輕量級鎖

輕量級鎖是指當鎖是偏向鎖的時候,被第二個線程B通路,此時偏向鎖就會更新為輕量級鎖,線程B會通過自旋的形式嘗試擷取鎖,線程不會阻塞,從er提升性能。

目前隻有一個等待線程,則該線程将通過自旋進行等待。但是當自旋超過一定次數時,輕量級鎖便會更新為重量級鎖,當一個線程已持有鎖,另一個線程在自旋,而此時第三個線程來訪時,輕量級鎖也會更新為重量級鎖。

注:自旋是什麼?

自旋(spinlock)是指當一個線程擷取鎖的時候,如果鎖已經被其它線程擷取,那麼該線程将循環等待,然後不斷的判斷鎖是否能夠被成功擷取,直到擷取到鎖才會退出循環。

4、重量級鎖

指當有一個線程擷取鎖之後,其餘所有等待擷取該鎖的線程都會處于阻塞狀态。

重量級鎖通過對象内部的監聽器(monitor)實作,而其中monitor的本質是依賴于底層作業系統的Mutex Lock實作,作業系統實作線程之間的切換需要從使用者态切換到核心态,切換成本非常高。

5、鎖狀态對比

偏向鎖 輕量級鎖 重量級鎖
使用場景 隻有一個線程進入同步塊 雖然很多線程,但沒有沖突,線程進入時間錯開因而并未争搶鎖 發生了鎖争搶的情況,多條線程進入同步塊争用鎖
本質 取消同步操作 CAS操作代替互斥同步 互斥同步
優點 不阻塞,執行效率高(隻有第一次擷取偏向鎖時需要CAS操作,後面隻是比對ThreadId) 不會阻塞 不會空耗CPU
缺點 适用場景太局限。若競争産生,會有額外的偏向鎖撤銷的消耗 長時間擷取不到鎖空耗CPU 阻塞,上下文切換,重量級操作,消耗作業系統資源

6、鎖消除

消除鎖是虛拟機另外一種鎖的優化,這種優化更徹底,Java虛拟機在JIT編譯時(可以簡單了解為當某段代碼即将第一次被執行時進行編譯,又稱即時編譯),通過對運作上下文的掃描,去除不可能存在共享資源競争的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間,如StringBuffer的append是一個同步方法,但是在add方法中的StringBuffer屬于一個局部變量,别切不會被其它線程所使用,是以StringBuffer不可能存在共享資源競争的情景,JVM會自動将其鎖消除。

十、Java多線程思維導圖

Java高并發程式設計實戰,那些年學過的鎖
原文連結:https://blog.csdn.net/guorui_java/article/details/126727119?utm_source=tuicool&utm_medium=referral

繼續閱讀