互聯網校招面試必備——Java多線程

来源:https://www.cnblogs.com/weiweiblog/archive/2018/09/19/9672353.html
-Advertisement-
Play Games

本文首發於我的個人博客: "尾尾部落" 本文是我刷了幾十篇一線互聯網校招java後端開發崗位的面經後總結的多線程相關題目,雖然有點小長,但是面試前看一看,相信能幫你輕鬆啃下多線程這塊大骨頭。 什麼是進程,什麼是線程?為什麼需要多線程編程? 進程間的通信方式、線程間的通信方式 實現多線程的三種方法 三 ...


本文首發於我的個人博客:尾尾部落

本文是我刷了幾十篇一線互聯網校招java後端開發崗位的面經後總結的多線程相關題目,雖然有點小長,但是面試前看一看,相信能幫你輕鬆啃下多線程這塊大骨頭。

什麼是進程,什麼是線程?為什麼需要多線程編程?
進程間的通信方式、線程間的通信方式
實現多線程的三種方法
三種創建多線程方法的對比
線程狀態
線程式控制制
wait、notify、notifyAll的區別
sleep() 和 wait() 有什麼區別?
鎖類型
什麼是樂觀鎖和悲觀鎖
樂觀鎖的實現方式(CAS)
CAS的缺點
實現一個死鎖
如何確保 N 個線程可以訪問 N 個資源同時又不導致死鎖?
volatile
volatile使用建議
volatile和synchronized區別
synchronized
synchronized的三種應用方式
Lock
Lock介面中獲取鎖的方法
Condition類
Condition與Object中的wait, notify, notifyAll區別
synchronized和lock的區別
鎖的狀態
偏向鎖、輕量級鎖、重量級鎖、自旋鎖、自適應自旋鎖
偏向鎖、輕量級鎖、重量級鎖適用於不同的併發場景
AQS
線程池
使用線程池的好處
線程池都有哪幾種工作隊列

什麼是進程,什麼是線程?為什麼需要多線程編程?

進程是執行著的應用程式,而線程是進程內部的一個執行序列。一個進程可以有多個線程。線程又叫做輕量級進程。
進程是具有一定獨立功能的程式關於某個數據集合上的一次運行活動,是操作系統進行資源分配和調度的一個獨立單位;線程是進程的一個實體,是 CPU 調度和分派的基本單位,是比進程更小的能獨立運行的基本單位。線程的劃分尺度小於進程,這使得多線程程式的併發性高;進程在執行時通常擁有獨立的記憶體單元,而線程之間可以共用記憶體。使用多線程的編程通常能夠帶來更好的性能和用戶體驗,但是多線程的程式對於其他程式是不友好的,因為它占用了更多的 CPU 資源。

進程間的通信方式

  • 管道( pipe ):管道是一種半雙工的通信方式,數據只能單向流動,而且只能在具有親緣關係的進程間使用。進程的親緣關係通常是指父子進程關係。
  • 有名管道 (namedpipe) : 有名管道也是半雙工的通信方式,但是它允許無親緣關係進程間的通信。
  • 信號量(semophore ) : 信號量是一個計數器,可以用來控制多個進程對共用資源的訪問。它常作為一種鎖機制,防止某進程正在訪問共用資源時,其他進程也訪問該資源。因此,主要作為進程間以及同一進程內不同線程之間的同步手段。
  • 消息隊列( messagequeue ) : 消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列剋服了信號傳遞信息少、管道只能承載無格式位元組流以及緩衝區大小受限等缺點。
  • 信號 (sinal) : 信號是一種比較複雜的通信方式,用於通知接收進程某個事件已經發生。
  • 共用記憶體(shared memory ) :共用記憶體就是映射一段能被其他進程所訪問的記憶體,這段共用記憶體由一個進程創建,但多個進程都可以訪問。共用記憶體是最快的 IPC 方式,它是針對其他進程間通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號兩,配合使用,來實現進程間的同步和通信。
  • 套接字(socket ) : 套解口也是一種進程間通信機制,與其他通信機制不同的是,它可用於不同及其間的進程通信。

線程間的通信方式

  • 鎖機制:包括互斥鎖、條件變數、讀寫鎖
    • 互斥鎖提供了以排他方式防止數據結構被併發修改的方法。
    • 讀寫鎖允許多個線程同時讀共用數據,而對寫操作是互斥的。
    • 條件變數可以以原子的方式阻塞進程,直到某個特定條件為真為止。對條件的測試是在互斥鎖的保護下進行的。條件變數始終與互斥鎖一起使用。
  • 信號量機制(Semaphore):包括無名線程信號量和命名線程信號量
  • 信號機制(Signal):類似進程間的信號處理

線程間的通信目的主要是用於線程同步,所以線程沒有像進程通信中的用於數據交換的通信機制。

實現多線程的三種方法

  • 繼承Thread類,重寫父類run()方法
public class thread1 extends Thread {
        public void run() {
                for (int i = 0; i < 10000; i++) {
                        System.out.println("我是線程"+this.getId());
                }
        }
        public static void main(String[] args) {
                thread1 th1 = new thread1();
                thread1 th2 = new thread1();
                th1.start();
                th2.start();
        }
}
  • 實現runnable介面
public class thread2 implements Runnable {
        public String ThreadName;
        public thread2(String tName){
                ThreadName = tName;
        }
        public void run() {
                for (int i = 0; i < 10000; i++) {
                        System.out.println(ThreadName);
                }
        }
        public static void main(String[] args) {
                // 創建一個Runnable介面實現類的對象
                thread2 th1 = new thread2("線程A:");
                thread2 th2 = new thread2("線程B:");
                // 將此對象作為形參傳遞給Thread類的構造器中,創建Thread類的對象,此對象即為一個線程
                Thread myth1 = new Thread(th1);
                Thread myth2 = new Thread(th2);
                // 調用start()方法,啟動線程並執行run()方法
                myth1.start();
                myth2.start();
        }
}
  • 通過Callable和Future創建線程
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class CallableThreadTest implements Callable<Integer>
{
    @Override
    public Integer call() throws Exception{
        int i = 0;
        for(;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
        return i;
    }
    
    public static void main(String[] args){
        CallableThreadTest ctt = new CallableThreadTest();
        FutureTask<Integer> ft = new FutureTask<>(ctt);
        for(int i = 0;i < 100;i++){
            System.out.println(Thread.currentThread().getName()+" 的迴圈變數i的值"+i);
            if(i==20){
                new Thread(ft,"有返回值的線程").start();
            }
        }
        try{
            System.out.println("子線程的返回值:"+ft.get());
        } catch (InterruptedException e){
            e.printStackTrace();
        } catch (ExecutionException e){
            e.printStackTrace();
        }
    }
}

三種創建多線程方法的對比

1、採用實現Runnable、Callable介面的方式創建多線程時,線程類只是實現了Runnable介面或Callable介面,還可以繼承其他類。缺點是編程稍微複雜,如果要訪問當前線程,則必須使用Thread.currentThread()方法。
2、使用繼承Thread類的方式創建多線程時,編寫簡單,如果需要訪問當前線程,則無需使用Thread.currentThread()方法,直接使用this即可獲得當前線程。缺點是線程類已經繼承了Thread類,所以不能再繼承其他父類。
3、Runnable和Callable的區別

(1) Callable規定重寫call(),Runnable重寫run()。

(2) Callable的任務執行後可返回值,而Runnable的任務是不能返回值的。

(3) call方法可以拋出異常,run方法不可以。

(4) 運行Callable任務可以拿到一個Future對象,表示非同步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並檢索計算的結果。通過Future對象可以瞭解任務執行情況,可取消任務的執行,還可獲取執行結果。

線程狀態

  • 新建狀態:新建線程對象,並沒有調用start()方法之前
  • 就緒狀態:調用start()方法之後線程就進入就緒狀態,但是並不是說只要調用start()方法線程就馬上變為當前線程,在變為當前線程之前都是為就緒狀態。值得一提的是,線程在睡眠和掛起中恢復的時候也會進入就緒狀態。
  • 運行狀態:線程被設置為當前線程,獲得CPU後,開始執行run()方法,就是線程進入運行狀態。
  • 阻塞狀態:處於運行的狀態的線程,除非執行時間非常非常非常短,否則它會因為系統對資源的調度而被中斷進入阻塞狀態。比如說調用sleep()方法後線程就進入阻塞狀態。
  • 死亡狀態:處於運行狀態的線程,當它主動或者被動結束,線程就處於死亡狀態。結束的形式,通常有以下幾種:1. 線程執行完成,線程正常結束;2. 線程執行過程中出現異常或者錯誤,被動結束;3. 線程主動調用stop方法結束線程。

線程式控制制

  • join():等待。讓一個線程等待另一個線程完成才繼續執行。如A線程線程執行體中調用B線程的join()方法,則A線程被阻塞,知道B線程執行完為止,A才能得以繼續執行。
  • sleep():睡眠。讓當前的正在執行的線程暫停指定的時間,併進入阻塞狀態。
  • yield():線程讓步。將線程從運行狀態轉換為就緒狀態。當某個線程調用 yiled() 方法從運行狀態轉換到就緒狀態後,CPU 會從就緒狀態線程隊列中只會選擇與該線程優先順序相同或優先順序更高的線程去執行。
  • setPriority():改變線程的優先順序。每個線程在執行時都具有一定的優先順序,優先順序高的線程具有較多的執行機會。每個線程預設的優先順序都與創建它的線程的優先順序相同。main線程預設具有普通優先順序。參數priorityLevel範圍在1-10之間,常用的有如下三個靜態常量值:MAX_PRIORITY:10;MIN_PRIORITY:1;NORM_PRIORITY:5。

PS: 具有較高線程優先順序的線程對象僅表示此線程具有較多的執行機會,而非優先執行。

  • setDaemon(true):設置為後臺線程。後臺線程主要是為其他線程(相對可以稱之為前臺線程)提供服務,或“守護線程”。如JVM中的垃圾回收線程。當所有的前臺線程都進入死亡狀態時,後臺線程會自動死亡。

sleep() 和 yield() 兩者的區別:
① sleep()方法會給其他線程運行的機會,不考慮其他線程的優先順序,因此會給較低優先順序線程一個運行的機會。yield()方法只會給相同優先順序或者更高優先順序的線程一個運行的機會。
② 當線程執行了 sleep(long millis) 方法,將轉到阻塞狀態,參數millis指定睡眠時間。當線程執行了yield()方法,將轉到就緒狀態。
③ sleep() 方法聲明拋出InterruptedException異常,而 yield() 方法沒有聲明拋出任何異常。

wait、notify、notifyAll的區別

wait、notify、notifyAll是java同步機制中重要的組成部分,結合synchronized關鍵字使用,可以建立很多優秀的同步模型。這3個方法並不是Thread類或者是Runnable介面的方法,而是Object類的3個本地方法。
調用一個Object的wait與notify/notifyAll的時候,必須保證調用代碼對該Object是同步的,也就是說必須在作用等同於synchronized(obj){......}的內部才能夠去調用obj的wait與notify/notifyAll三個方法,否則就會報錯:java.lang.IllegalMonitorStateException:current thread not owner

先說兩個概念:鎖池和等待池
鎖池:假設線程A已經擁有了某個對象(註意:不是類)的鎖,而其它的線程想要調用這個對象的某個synchronized方法(或者synchronized塊),由於這些線程在進入對象的synchronized方法之前必須先獲得該對象的鎖的擁有權,但是該對象的鎖目前正被線程A擁有,所以這些線程就進入了該對象的鎖池中。
等待池:假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖後,進入到了該對象的等待池中
@知乎--文龍

  • 如果線程調用了對象的 wait()方法,那麼線程便會處於該對象的等待池中,等待池中的線程不會去競爭該對象的鎖。
  • 當有線程調用了對象的 notifyAll()方法(喚醒所有 wait 線程)或 notify()方法(只隨機喚醒一個 wait 線程),被喚醒的的線程便會進入該對象的鎖池中,鎖池中的線程會去競爭該對象鎖。也就是說,調用了notify後只要一個線程會由等待池進入鎖池,而notifyAll會將該對象等待池內的所有線程移動到鎖池中,等待鎖競爭
  • 優先順序高的線程競爭到對象鎖的概率大,假若某線程沒有競爭到該對象鎖,它還會留在鎖池中,唯有線程再次調用 wait()方法,它才會重新回到等待池中。而競爭到對象鎖的線程則繼續往下執行,直到執行完了 synchronized 代碼塊,它會釋放掉該對象鎖,這時鎖池中的線程會繼續競爭該對象鎖。

小結

  • wait:線程自動釋放其占有的對象鎖,並等待notify
  • notify:喚醒一個正在wait當前對象鎖的線程,並讓它拿到對象鎖
  • notifyAll:喚醒所有正在wait當前對象鎖的線程
    notify和notifyAll的最主要的區別是:notify只是喚醒一個正在wait當前對象鎖的線程,而notifyAll喚醒所有。值得註意的是:notify是本地方法,具體喚醒哪一個線程由虛擬機控制;notifyAll後並不是所有的線程都能馬上往下執行,它們只是跳出了wait狀態,接下來它們還會是競爭對象鎖。

sleep() 和 wait() 有什麼區別?

sleep()方法是線程類(Thread)的靜態方法,導致此線程暫停執行指定時間,將執行機會給其他線程,但是監控狀態依然保持,到時後會自動恢復(線程回到就緒(ready)狀態),因為調用 sleep 不會釋放對象鎖。wait() 是 Object 類的方法,對此對象調用 wait()方法導致本線程放棄對象鎖(線程暫停執行),進入等待此對象的等待鎖定池,只有針對此對象發出 notify 方法(或 notifyAll)後本線程才進入對象鎖定池準備獲得對象鎖進入就緒狀態。

鎖類型

  • 可重入鎖:廣義上的可重入鎖指的是可重覆可遞歸調用的鎖,在外層使用鎖之後,在內層仍然可以使用,並且不發生死鎖(前提得是同一個對象或者class),這樣的鎖就叫做可重入鎖。即在執行對象中所有同步方法不用再次獲得鎖。ReentrantLock和synchronized都是可重入鎖。舉個簡單的例子,當一個線程執行到某個synchronized方法時,比如說method1,而在method1中會調用另外一個synchronized方法method2,此時線程不必重新去申請鎖,而是可以直接執行方法method2。
  • 可中斷鎖:在等待獲取鎖過程中可中斷。synchronized就不是可中斷鎖,而Lock是可中斷鎖。
  • 公平鎖: 按等待獲取鎖的線程的等待時間進行獲取,等待時間長的具有優先獲取鎖權利。非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的,這樣就可能導致某個或者一些線程永遠獲取不到鎖。synchronized是非公平鎖,它無法保證等待的線程獲取鎖的順序。對於ReentrantLock和ReentrantReadWriteLock,預設情況下是非公平鎖,但是可以設置為公平鎖。
  • 讀寫鎖:對資源讀取和寫入的時候拆分為2部分處理,一個讀鎖和一個寫鎖。讀的時候可以多線程一起讀,寫的時候必須同步地寫。ReadWriteLock就是讀寫鎖,它是一個介面,ReentrantReadWriteLock實現了這個介面。可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。

什麼是樂觀鎖和悲觀鎖

(1)樂觀鎖:很樂觀,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會去判斷在此期間有沒有人去更新這個數據(可以使用版本號等機制)。如果因為衝突失敗就重試。樂觀鎖適用於寫比較少的情況下,即衝突比較少發生,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。像資料庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下麵的原子變數類就是使用了樂觀鎖的一種實現方式CAS實現的。
(2)悲觀鎖:總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,因此每次拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖,效率比較低。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。再比如Java裡面的同步原語synchronized關鍵字的實現也是悲觀鎖。

樂觀鎖的實現方式(CAS)

樂觀鎖的實現主要就兩個步驟:衝突檢測和數據更新。其實現方式有一種比較典型的就是 Compare and Swap ( CAS )。
CAS:CAS是樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變數時,只有其中一個線程能更新變數的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。
CAS 操作中包含三個操作數 —— 需要讀寫的記憶體位置(V)、進行比較的預期原值(A)和擬寫入的新值(B)。如果記憶體位置V的值與預期原值A相匹配,那麼處理器會自動將該位置值更新為新值B。否則處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明瞭“ 我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。 ”這其實和樂觀鎖的衝突檢查+數據更新的原理是一樣的。

樂觀鎖是一種思想,CAS是這種思想的一種實現方式。

CAS的缺點

  1. ABA問題

    如果記憶體地址V初次讀取的值是A,並且在準備賦值的時候檢查到它的值仍然為A,那我們就能說它的值沒有被其他線程改變過了嗎?如果在這段期間它的值曾經被改成了B,後來又被改回為A,那CAS操作就會誤認為它從來沒有被改變過。這個漏洞稱為CAS操作的“ABA”問題。ava併發包為瞭解決這個問題,提供了一個帶有標記的原子引用類“AtomicStampedReference”,它可以通過控制變數值的版本來保證CAS的正確性。因此,在使用CAS前要考慮清楚“ABA”問題是否會影響程式併發的正確性,如果需要解決ABA問題,改用傳統的互斥同步可能會比原子類更高效。

  2. 迴圈時間長開銷很大

    自旋CAS(不成功,就一直迴圈執行,直到成功)如果長時間不成功,會給CPU帶來非常大的執行開銷。

  3. 只能保證一個共用變數的原子操作。

    當對一個共用變數執行操作時,我們可以使用迴圈CAS的方式來保證原子操作,但是對多個共用變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖來保證原子性。

實現一個死鎖

什麼是死鎖:兩個進程都在等待對方執行完畢才能繼續往下執行的時候就發生了死鎖。結果就是兩個進程都陷入了無限的等待中。
產生死鎖的四個必要條件:
互斥條件:一個資源每次只能被一個進程使用。
請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
迴圈等待條件:若幹進程之間形成一種頭尾相接的迴圈等待資源關係。
這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。
考慮如下情形:
(1)線程A當前持有互斥所鎖lock1,線程B當前持有互斥鎖lock2。
(2)線程A試圖獲取lock2,因為線程B正持有lock2,因此線程A會阻塞等待線程B對lock2釋放。
(3)如果此時線程B也在試圖獲取lock1,同理線程也會阻塞。
(4)兩者都在等待對方所持有但是雙方都不釋放的鎖,這時便會一直阻塞形成死鎖。
死鎖的解決方法:
a 撤消陷於死鎖的全部進程;
b 逐個撤消陷於死鎖的進程,直到死鎖不存在;
c 從陷於死鎖的進程中逐個強迫放棄所占用的資源,直至死鎖消失。
d 從另外一些進程那裡強行剝奪足夠數量的資源分配給死鎖進程,以解除死鎖狀態

如何確保 N 個線程可以訪問 N 個資源同時又不導致死鎖?

使用多線程的時候,一種非常簡單的避免死鎖的方式就是:指定獲取鎖的順序,並強制線程按照指定的順序獲取鎖。因此,如果所有的線程都是以同樣的順序加鎖和釋放鎖,就不會出現死鎖了

volatile關鍵字

  對於過可見性、有序性及原子性問題,通常情況下我們可以通過Synchronized關鍵字來解決這些個問題,不過如果對Synchronized原理有瞭解的話,應該知道Synchronized是一個比較重量級的操作,對系統的性能有比較大的影響,所以,如果有其他解決方案,我們通常都避免使用Synchronized來解決問題。而volatile關鍵字就是Java中提供的另一種解決可見性和有序性問題的方案。對於原子性,需要強調一點,也是大家容易誤解的一點:對volatile變數的單次讀/寫操作可以保證原子性的,如long和double類型變數,但是並不能保證i++這種操作的原子性,因為本質上i++是讀、寫兩次操作。

  • 防止重排序

    問題:操作系統可以對指令進行重排序,多線程環境下就可能將一個未初始化的對象引用暴露出來,從而導致不可預料的結果
    解決原理:volatile關鍵字通過提供“記憶體屏障”的方式來防止指令被重排序,為了實現volatile的記憶體語義,編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定類型的處理器重排序。
    1、在每個volatile寫操作前插入StoreStore屏障,在寫操作後插入StoreLoad屏障。
    2、在每個volatile讀操作前插入LoadLoad屏障,在讀操作後插入LoadStore屏障。

  • 實現可見性

    問題:可見性問題主要指一個線程修改了共用變數值,而另一個線程卻看不到
    解決原理:(1)修改volatile變數時會強制將修改後的值刷新的主記憶體中。
    (2)修改volatile變數後會導致其他線程工作記憶體中對應的變數值失效。因此,再讀取該變數值的時候就需要重新從讀取主記憶體中的值。

  • 註:volatile並不保證變數更新的原子性

volatile使用建議

相對於synchronized塊的代碼鎖,volatile應該是提供了一個輕量級的針對共用變數的鎖,當我們在多個線程間使用共用變數進行通信的時候需要考慮將共用變數用volatile來修飾。
volatile是一種稍弱的同步機制,在訪問volatile變數時不會執行加鎖操作,也就不會執行線程阻塞,因此volatile變數是一種比synchronized關鍵字更輕量級的同步機制。
使用建議:在兩個或者更多的線程需要訪問的成員變數上使用volatile。當要訪問的變數已在synchronized代碼塊中,或者為常量時,沒必要使用volatile。
由於使用volatile屏蔽掉了JVM中必要的代碼優化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。

volatile和synchronized區別

1、volatile不會進行加鎖操作:
volatile變數是一種稍弱的同步機制在訪問volatile變數時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變數是一種比synchronized關鍵字更輕量級的同步機制。
2、volatile變數作用類似於同步變數讀寫操作:
從記憶體可見性的角度看,寫入volatile變數相當於退出同步代碼塊,而讀取volatile變數相當於進入同步代碼塊。
3、volatile不如synchronized安全:
在代碼中如果過度依賴volatile變數來控制狀態的可見性,通常會比使用鎖的代碼更脆弱,也更難以理解。僅當volatile變數能簡化代碼的實現以及對同步策略的驗證時,才應該使用它。一般來說,用同步機制會更安全些。
4、volatile無法同時保證記憶體可見性和原子性:
加鎖機制(即同步機制)既可以確保可見性又可以確保原子性,而volatile變數只能確保可見性,原因是聲明為volatile的簡單變數如果當前值與該變數以前的值相關,那麼volatile關鍵字不起作用,也就是說如下的表達式都不是原子操作:“count++”、“count = count+1”。

當且僅當滿足以下所有條件時,才應該使用volatile變數:
1、對變數的寫入操作不依賴變數的當前值,或者你能確保只有單個線程更新變數的值。
2、該變數沒有包含在具有其他變數的不變式中。
總結:在需要同步的時候,第一選擇應該是synchronized關鍵字,這是最安全的方式,嘗試其他任何方式都是有風險的。尤其在、jdK1.5之後,對synchronized同步機製做了很多優化,如:自適應的自旋鎖、鎖粗化、鎖消除、輕量級鎖等,使得它的性能明顯有了很大的提升。

synchronized

synchronized可以保證方法或者代碼塊在運行時,同一時刻只有一個方法可以進入到臨界區,同時它還可以保證共用變數的記憶體可見性。Synchronized主要有以下三個作用:保證互斥性、保證可見性、保證順序性。

synchronized的三種應用方式

  • 修飾實例方法,作用於當前實例加鎖,進入同步代碼前要獲得當前實例的鎖。實現原理:指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,如果設置了,執行線程將先持有monitor(虛擬機規範中用的是管程一詞), 然後再執行方法,最後再方法完成(無論是正常完成還是非正常完成)時釋放monitor。

    public synchronized void increase(){
        i++;
    }
  • 修飾靜態方法,作用於當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖

    public static synchronized void increase(){
        i++;
    }
  • 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。實現原理:使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置。

    static AccountingSync instance=new AccountingSync();
    synchronized(instance){
        for(int j=0;j<1000000;j++){
            i++;
        }
    }

Lock

Lock是一個介面,它的的實現類提供了比synchronized更廣泛意義上鎖操作,它允許用戶更靈活的代碼結構,更多的不同特效。Lock的實現類主要有ReentrantLock和ReentrantReadWriteLock。

Lock lock=new ReentrantLock();
lock.lock();
try{
    // do something
    // 如果有return要寫在try塊中
}finally{
    lock.unlock();
}

Lock介面中獲取鎖的方法

  • void lock():lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。在發生異常時,它不會自動釋放鎖,要記得在finally塊中釋放鎖,以保證鎖一定被被釋放,防止死鎖的發生。
  • void lockInterruptibly():可以響應中斷,當通過這個方法去獲取鎖時,如果線程 正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態。
  • boolean tryLock():有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true;如果獲取失敗(即鎖已被其他線程獲取),則返回false。
  • boolean tryLock(long time, TimeUnit unit):和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false,同時可以響應中斷。

Condition類

Condition是Java提供來實現等待/通知的類,Condition類還提供比wait/notify更豐富的功能,Condition對象是由lock對象所創建的。但是同一個鎖可以創建多個Condition的對象,即創建多個對象監視器。這樣的好處就是可以指定喚醒線程。notify喚醒的線程是隨機喚醒一個。
Condition 將 Object 監視器方法(wait、notify 和 notifyAll)分解成截然不同的對象,以便通過將這些對象與任意 Lock 實現組合使用,為每個對象提供多個等待 set (wait-set)。
其中,Lock 替代了 synchronized 方法和語句的使用,Condition 替代了 Object 監視器方法的使用。
在Condition中,用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll(),傳統線程的通信方式,Condition都可以實現,這裡註意,Condition是被綁定到Lock上的,要創建一個Lock的Condition必須用newCondition()方法。

Condition與Object中的wait, notify, notifyAll區別

1.Condition中的await()方法相當於Object的wait()方法,Condition中的signal()方法相當於Object的notify()方法,Condition中的signalAll()相當於Object的notifyAll()方法。
不同的是,Object中的這些方法是和同步鎖捆綁使用的;而Condition是需要與互斥鎖/共用鎖捆綁使用的。
2.Condition它更強大的地方在於:能夠更加精細的控制多線程的休眠與喚醒。對於同一個鎖,我們可以創建多個Condition,在不同的情況下使用不同的Condition。
例如,假如多線程讀/寫同一個緩衝區:當向緩衝區中寫入數據之後,喚醒"讀線程";當從緩衝區讀出數據之後,喚醒"寫線程";並且當緩衝區滿的時候,"寫線程"需要等待;當緩衝區為空時,"讀線程"需要等待。
如果採用Object類中的wait(),notify(),notifyAll()實現該緩衝區,當向緩衝區寫入數據之後需要喚醒"讀線程"時,不可能通過notify()或notifyAll()明確的指定喚醒"讀線程",而只能通過notifyAll喚醒所有線程(但是notifyAll無法區分喚醒的線程是讀線程,還是寫線程)。 但是,通過Condition,就能明確的指定喚醒讀線程。

synchronized和lock的區別

synchronized Lock
存在層次 Java的關鍵字 是一個介面
鎖的釋放 1、以獲取鎖的線程執行完同步代碼,釋放鎖 2、線程執行發生異常,jvm會讓線程釋放鎖 在finally中必須釋放鎖,不然容易造成線程死鎖
鎖的獲取 假設A線程獲得鎖,B線程等待。如果A線程阻塞,B線程會一直等待 Lock可以讓等待鎖的線程響應中斷
鎖狀態 無法判斷 可以判斷有沒有成功獲取鎖
鎖類型 可重入 不可中斷 非公平 可重入 可中斷 公平/非公平

性能方面,JDK1.5中,synchronized是性能低效的。因為這是一個重量級操作,它對性能最大的影響是阻塞的是實現,掛起線程和恢複線程的操作都需要轉入內核態中完成,這些操作給系統的併發性帶來了很大的壓力。相比之下使用Java提供的Lock對象,性能更高一些。多線程環境下,synchronized的吞吐量下降的非常嚴重,而ReentrankLock則能基本保持在同一個比較穩定的水平上。

到了JDK1.6,synchronize加入了很多優化措施,有自適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在JDK1.6上synchronize的性能並不比Lock差。官方也表示,他們也更支持synchronize,在未來的版本中還有優化餘地,所以還是提倡在synchronized能實現需求的情況下,優先考慮使用synchronized來進行同步。

鎖的狀態

Java SE1.6為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java SE1.6里鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。

偏向鎖

在沒有實際競爭的情況下,還能夠針對部分場景繼續優化。如果不僅僅沒有實際競爭,自始至終,使用鎖的線程都只有一個,那麼,維護輕量級鎖都是浪費的。偏向鎖的目標是,減少無競爭且只有一個線程使用鎖的情況下,使用輕量級鎖產生的性能消耗。輕量級鎖每次申請、釋放鎖都至少需要一次CAS,但偏向鎖只有初始化時需要一次CAS。
“偏向”的意思是,偏向鎖假定將來只有第一個申請鎖的線程會使用鎖(不會有任何線程再來申請鎖),因此,只需要在Mark Word中CAS記錄owner(本質上也是更新,但初始值為空),如果記錄成功,則偏向鎖獲取成功,記錄鎖狀態為偏向鎖,以後當前線程等於owner就可以零成本的直接獲得鎖;否則,說明有其他線程競爭,膨脹為輕量級鎖。
偏向鎖無法使用自旋鎖優化,因為一旦有其他線程申請鎖,就破壞了偏向鎖的假定。

輕量級鎖

輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進入同步塊的情況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級為輕量級鎖。輕量級鎖是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用產生的性能消耗。輕量級鎖所適應的場景是線程交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖。
使用輕量級鎖時,不需要申請互斥量,僅僅將Mark Word中的部分位元組CAS更新指向線程棧中的Lock Record,如果更新成功,則輕量級鎖獲取成功,記錄鎖狀態為輕量級鎖;否則,說明已經有線程獲得了輕量級鎖,目前發生了鎖競爭(不適合繼續使用輕量級鎖),接下來膨脹為重量級鎖。

重量級鎖

重量鎖在JVM中又叫對象監視器(Monitor),它很像C中的Mutex,除了具備Mutex(0|1)互斥的功能,它還負責實現了Semaphore(信號量)的功能,也就是說它至少包含一個競爭鎖的隊列,和一個信號阻塞隊列(wait隊列),前者負責做互斥,後一個用於做線程同步。

自旋鎖

自旋鎖原理非常簡單,如果持有鎖的線程能在很短時間內釋放鎖資源,那麼那些等待競爭鎖的線程就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的線程釋放鎖後即可立即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。
但是線程自旋是需要消耗cup的,說白了就是讓cup在做無用功,如果一直獲取不到鎖,那線程也不能一直占用cup自旋做無用功,所以需要設定一個自旋等待的最大時間。
如果持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的線程在最大等待時間內還是獲取不到鎖,這時爭用線程會停止自旋進入阻塞狀態。

自適應自旋鎖

自適應意味著自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定:

  • 如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那麼虛擬機就會認為這次自旋也很有可能再次成功,進而它將允許自旋等待持續相對更長的時間,比如100個迴圈。
  • 相反的,如果對於某個鎖,自旋很少成功獲得過,那在以後要獲取這個鎖時將可能減少自旋時間甚至省略自旋過程,以避免浪費處理器資源。

自適應自旋解決的是“鎖競爭時間不確定”的問題。JVM很難感知到確切的鎖競爭時間,而交給用戶分析就違反了JVM的設計初衷。自適應自旋假定不同線程持有同一個鎖對象的時間基本相當,競爭程度趨於穩定,因此,可以根據上一次自旋的時間與結果調整下一次自旋的時間。

偏向鎖、輕量級鎖、重量級鎖適用於不同的併發場景

偏向鎖:無實際競爭,且將來只有第一個申請鎖的線程會使用鎖。
輕量級鎖:無實際競爭,多個線程交替使用鎖;允許短時間的鎖競爭。
重量級鎖:有實際競爭,且鎖競爭時間長。
另外,如果鎖競爭時間短,可以使用自旋鎖進一步優化輕量級鎖、重量級鎖的性能,減少線程切換。
如果鎖競爭程度逐漸提高(緩慢),那麼從偏向鎖逐步膨脹到重量鎖,能夠提高系統的整體性能。

鎖膨脹的過程:只有一個線程進入臨界區(偏向鎖),多個線程交替進入臨界區(輕量級鎖),多線程同時進入臨界區(重量級鎖)。

AQS

AQS即是AbstractQueuedSynchronizer,一個用來構建鎖和同步工具的框架,包括常用的ReentrantLock、CountDownLatch、Semaphore等。
AbstractQueuedSynchronizer是一個抽象類,主要是維護了一個int類型的state屬性和一個非阻塞、先進先出的線程等待隊列;其中state是用volatile修飾的,保證線程之間的可見性,隊列的入隊和出對操作都是無鎖操作,基於自旋鎖和CAS實現;另外AQS分為兩種模式:獨占模式和共用模式,像ReentrantLock是基於獨占模式模式實現的,CountDownLatch、CyclicBarrier等是基於共用模式。

線程池

如果併發的線程數量很多,並且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因為頻繁創建線程和銷毀線程需要時間。
線程池的產生和資料庫的連接池類似,系統啟動一個線程的代價是比較高昂的,如果在程式啟動的時候就初始化一定數量的線程,放入線程池中,在需要是使用時從池子中去,用完再放回池子里,這樣能大大的提高程式性能,再者,線程池的一些初始化配置,也可以有效的控制系統併發的數量,防止因為消耗過多的記憶體,而把伺服器累趴下。

通過Executors工具類可以創建各種類型的線程池,如下為常見的四種:

  • newCachedThreadPool :大小不受限,當線程釋放時,可重用該線程;
  • newFixedThreadPool :大小固定,無可用線程時,任務需等待,直到有可用線程;
  • newSingleThreadExecutor :創建一個單線程,任務會按順序依次執行;
  • newScheduledThreadPool:創建一個定長線程池,支持定時及周期性任務執行

使用線程池的好處

  • 減少了創建和銷毀線程的次數,每個工作線程都可以被重覆利用,可執行多個任務。
  • 運用線程池能有效的控制線程最大併發數,可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因為消耗過多的記憶體,而把伺服器累趴下(每個線程需要大約1MB記憶體,線程開的越多,消耗的記憶體也就越大,最後死機)。
  • 對線程進行一些簡單的管理,比如:延時執行、定時迴圈執行的策略等,運用線程池都能進行很好的實現

線程池都有哪幾種工作隊列

1、ArrayBlockingQueue
是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
2、LinkedBlockingQueue
一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列
3、SynchronousQueue
一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
4、PriorityBlockingQueue
一個具有優先順序的無限阻塞隊列。

參考

Java 多線程
Java併發:volatile記憶體可見性和指令重排
併發編程的鎖機制:synchronized和lock
淺談偏向鎖、輕量級鎖、重量級鎖

獲取最新資訊,請關註微信公眾號:南強說晚安


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一.Bootstrap 概述Bootstrap 是由 Twitter 公司(全球最大的微博)的兩名技術工程師研發的一個基於HTML、CSS、JavaScript 的開源框架。該框架代碼簡潔、視覺優美,可用於快速、簡單地構建基於 PC 及移動端設備的 Web 頁面需求。2010 年 6 月,Twitt ...
  • 原文出自: "http://cmsblogs.com" 獲取 Document 對象後,會根據該對象和 Resource 資源對象調用 方法,開始註冊 BeanDefinitions 之旅。如下: 首先調用 方法實例化 BeanDefinitionDocumentReader 對象,然後獲取統計前 ...
  • 關鍵字:架構設計 軟體質量保證 資料庫完整性 1、資料庫完整性討論 有許多同學認為開發階段沒必要建立外鍵約束,更不用建立檢查約束,因為會影響單表數據寫入做測試。 這個想法是非常錯誤的,不規範的,不專業的。 首先影不影響測試是無稽之談,說明這類同學開發時不會寫單元測試,通過野路子來測試,質量不保。 然 ...
  • 本地部署時代 在軟體還是“本地部署(on-premise)”的時候,SaaS的版圖被大型玩家把持著,幾乎所有的垂直領域(營銷、支持、銷售、人力)都被微軟、SAP等大公司的解決方案占據。那時候的用戶並沒有什麼“軟體棧”可供選擇。 第一代SaaS冠軍 隨著互聯網的不斷普及,SaaS模式開始發揮作用。第一 ...
  • sleep(休眠) 和 wait(等待) 方法是 Java 多線程中常用的兩個方法,它們有什麼區別及一些該註意的地方有哪些呢?下麵給大家一一分解。 區別1:使用限制 使用 sleep 方法可以讓讓當前線程休眠,時間一到當前線程繼續往下執行,在任何地方都能使用,但需要捕獲 InterruptedExc ...
  • 軟體構造工具包括程式編輯器,編譯器,代碼生成器,解釋器和調試器 ...
  • lambda 表達式 剖析 大前提:捕獲列表裡變數的確定時機。 捕獲列表和參數列表有區別,捕獲列表裡的變數,是在捕獲的時間點就確定了,而不是在lambda調用時確定,參數列表是在調用時才確定。所以當捕獲了一個int i,i=12,然後在lambda後面的代碼又改變i為22,但是當調用lambda的時 ...
  • 原文出自: "http://cmsblogs.com" 在 方法中做了兩件事情,一是調用 獲取 XML 的驗證模式,二是調用 獲取 Document 對象。上篇博客已經分析了獲取 XML 驗證模式( "【死磕Spring】 IOC 之 獲取驗證模型" ),這篇我們分析獲取 Document 對象。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...