互聯(lián)網(wǎng)校招面試必備——Java多線程

本文首發(fā)于我的個人博客:尾尾部落

本文是我刷了幾十篇一線互聯(lián)網(wǎng)校招java后端開發(fā)崗位的面經(jīng)后總結(jié)的多線程相關(guān)題目,雖然有點小長,但是面試前看一看,相信能幫你輕松啃下多線程這塊大骨頭。

什么是進(jìn)程,什么是線程?為什么需要多線程編程?
進(jìn)程間的通信方式、線程間的通信方式
實現(xiàn)多線程的三種方法
三種創(chuàng)建多線程方法的對比
線程狀態(tài)
線程控制
wait、notify、notifyAll的區(qū)別
sleep() 和 wait() 有什么區(qū)別?
鎖類型
什么是樂觀鎖和悲觀鎖
樂觀鎖的實現(xiàn)方式(CAS)
CAS的缺點
實現(xiàn)一個死鎖
如何確保 N 個線程可以訪問 N 個資源同時又不導(dǎo)致死鎖?
volatile
volatile使用建議
volatile和synchronized區(qū)別
synchronized
synchronized的三種應(yīng)用方式
Lock
Lock接口中獲取鎖的方法
Condition類
Condition與Object中的wait, notify, notifyAll區(qū)別
synchronized和lock的區(qū)別
鎖的狀態(tài)
偏向鎖、輕量級鎖、重量級鎖、自旋鎖、自適應(yīng)自旋鎖
偏向鎖、輕量級鎖、重量級鎖適用于不同的并發(fā)場景
AQS
線程池
使用線程池的好處
線程池都有哪幾種工作隊列

什么是進(jìn)程,什么是線程?為什么需要多線程編程?

進(jìn)程是執(zhí)行著的應(yīng)用程序,而線程是進(jìn)程內(nèi)部的一個執(zhí)行序列。一個進(jìn)程可以有多個線程。線程又叫做輕量級進(jìn)程。
進(jìn)程是具有一定獨立功能的程序關(guān)于某個數(shù)據(jù)集合上的一次運行活動,是操作系統(tǒng)進(jìn)行資源分配和調(diào)度的一個獨立單位;線程是進(jìn)程的一個實體,是 CPU 調(diào)度和分派的基本單位,是比進(jìn)程更小的能獨立運行的基本單位。線程的劃分尺度小于進(jìn)程,這使得多線程程序的并發(fā)性高;進(jìn)程在執(zhí)行時通常擁有獨立的內(nèi)存單元,而線程之間可以共享內(nèi)存。使用多線程的編程通常能夠帶來更好的性能和用戶體驗,但是多線程的程序?qū)τ谄渌绦蚴遣挥押玫模驗樗加昧烁嗟?CPU 資源。

進(jìn)程間的通信方式

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

線程間的通信方式

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

線程間的通信目的主要是用于線程同步,所以線程沒有像進(jìn)程通信中的用于數(shù)據(jù)交換的通信機(jī)制。

實現(xiàn)多線程的三種方法

  • 繼承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();
        }
}
  • 實現(xiàn)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) {
                // 創(chuàng)建一個Runnable接口實現(xiàn)類的對象
                thread2 th1 = new thread2("線程A:");
                thread2 th2 = new thread2("線程B:");
                // 將此對象作為形參傳遞給Thread類的構(gòu)造器中,創(chuàng)建Thread類的對象,此對象即為一個線程
                Thread myth1 = new Thread(th1);
                Thread myth2 = new Thread(th2);
                // 調(diào)用start()方法,啟動線程并執(zhí)行run()方法
                myth1.start();
                myth2.start();
        }
}
  • 通過Callable和Future創(chuàng)建線程
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()+" 的循環(huán)變量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();
        }
    }
}

三種創(chuàng)建多線程方法的對比

1、采用實現(xiàn)Runnable、Callable接口的方式創(chuàng)建多線程時,線程類只是實現(xiàn)了Runnable接口或Callable接口,還可以繼承其他類。缺點是編程稍微復(fù)雜,如果要訪問當(dāng)前線程,則必須使用Thread.currentThread()方法。
2、使用繼承Thread類的方式創(chuàng)建多線程時,編寫簡單,如果需要訪問當(dāng)前線程,則無需使用Thread.currentThread()方法,直接使用this即可獲得當(dāng)前線程。缺點是線程類已經(jīng)繼承了Thread類,所以不能再繼承其他父類。
3、Runnable和Callable的區(qū)別
(1) Callable規(guī)定重寫call(),Runnable重寫run()。
(2) Callable的任務(wù)執(zhí)行后可返回值,而Runnable的任務(wù)是不能返回值的。
(3) call方法可以拋出異常,run方法不可以。
(4) 運行Callable任務(wù)可以拿到一個Future對象,表示異步計算的結(jié)果。它提供了檢查計算是否完成的方法,以等待計算的完成,并檢索計算的結(jié)果。通過Future對象可以了解任務(wù)執(zhí)行情況,可取消任務(wù)的執(zhí)行,還可獲取執(zhí)行結(jié)果。

線程狀態(tài)

image
  • 新建狀態(tài):新建線程對象,并沒有調(diào)用start()方法之前
  • 就緒狀態(tài):調(diào)用start()方法之后線程就進(jìn)入就緒狀態(tài),但是并不是說只要調(diào)用start()方法線程就馬上變?yōu)楫?dāng)前線程,在變?yōu)楫?dāng)前線程之前都是為就緒狀態(tài)。值得一提的是,線程在睡眠和掛起中恢復(fù)的時候也會進(jìn)入就緒狀態(tài)。
  • 運行狀態(tài):線程被設(shè)置為當(dāng)前線程,獲得CPU后,開始執(zhí)行run()方法,就是線程進(jìn)入運行狀態(tài)。
  • 阻塞狀態(tài):處于運行的狀態(tài)的線程,除非執(zhí)行時間非常非常非常短,否則它會因為系統(tǒng)對資源的調(diào)度而被中斷進(jìn)入阻塞狀態(tài)。比如說調(diào)用sleep()方法后線程就進(jìn)入阻塞狀態(tài)。
  • 死亡狀態(tài):處于運行狀態(tài)的線程,當(dāng)它主動或者被動結(jié)束,線程就處于死亡狀態(tài)。結(jié)束的形式,通常有以下幾種:1. 線程執(zhí)行完成,線程正常結(jié)束;2. 線程執(zhí)行過程中出現(xiàn)異常或者錯誤,被動結(jié)束;3. 線程主動調(diào)用stop方法結(jié)束線程。

線程控制

  • join():等待。讓一個線程等待另一個線程完成才繼續(xù)執(zhí)行。如A線程線程執(zhí)行體中調(diào)用B線程的join()方法,則A線程被阻塞,知道B線程執(zhí)行完為止,A才能得以繼續(xù)執(zhí)行。
  • sleep():睡眠。讓當(dāng)前的正在執(zhí)行的線程暫停指定的時間,并進(jìn)入阻塞狀態(tài)。
  • yield():線程讓步。將線程從運行狀態(tài)轉(zhuǎn)換為就緒狀態(tài)。當(dāng)某個線程調(diào)用 yiled() 方法從運行狀態(tài)轉(zhuǎn)換到就緒狀態(tài)后,CPU 會從就緒狀態(tài)線程隊列中只會選擇與該線程優(yōu)先級相同或優(yōu)先級更高的線程去執(zhí)行。
  • setPriority():改變線程的優(yōu)先級。每個線程在執(zhí)行時都具有一定的優(yōu)先級,優(yōu)先級高的線程具有較多的執(zhí)行機(jī)會。每個線程默認(rèn)的優(yōu)先級都與創(chuàng)建它的線程的優(yōu)先級相同。main線程默認(rèn)具有普通優(yōu)先級。參數(shù)priorityLevel范圍在1-10之間,常用的有如下三個靜態(tài)常量值:MAX_PRIORITY:10;MIN_PRIORITY:1;NORM_PRIORITY:5。

PS: 具有較高線程優(yōu)先級的線程對象僅表示此線程具有較多的執(zhí)行機(jī)會,而非優(yōu)先執(zhí)行。

  • setDaemon(true):設(shè)置為后臺線程。后臺線程主要是為其他線程(相對可以稱之為前臺線程)提供服務(wù),或“守護(hù)線程”。如JVM中的垃圾回收線程。當(dāng)所有的前臺線程都進(jìn)入死亡狀態(tài)時,后臺線程會自動死亡。

sleep() 和 yield() 兩者的區(qū)別:
① sleep()方法會給其他線程運行的機(jī)會,不考慮其他線程的優(yōu)先級,因此會給較低優(yōu)先級線程一個運行的機(jī)會。yield()方法只會給相同優(yōu)先級或者更高優(yōu)先級的線程一個運行的機(jī)會。
② 當(dāng)線程執(zhí)行了 sleep(long millis) 方法,將轉(zhuǎn)到阻塞狀態(tài),參數(shù)millis指定睡眠時間。當(dāng)線程執(zhí)行了yield()方法,將轉(zhuǎn)到就緒狀態(tài)。
③ sleep() 方法聲明拋出InterruptedException異常,而 yield() 方法沒有聲明拋出任何異常。

wait、notify、notifyAll的區(qū)別

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

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

  • 如果線程調(diào)用了對象的 wait()方法,那么線程便會處于該對象的等待池中,等待池中的線程不會去競爭該對象的鎖。
  • 當(dāng)有線程調(diào)用了對象的 notifyAll()方法(喚醒所有 wait 線程)或 notify()方法(只隨機(jī)喚醒一個 wait 線程),被喚醒的的線程便會進(jìn)入該對象的鎖池中,鎖池中的線程會去競爭該對象鎖。也就是說,調(diào)用了notify后只要一個線程會由等待池進(jìn)入鎖池,而notifyAll會將該對象等待池內(nèi)的所有線程移動到鎖池中,等待鎖競爭
  • 優(yōu)先級高的線程競爭到對象鎖的概率大,假若某線程沒有競爭到該對象鎖,它還會留在鎖池中,唯有線程再次調(diào)用 wait()方法,它才會重新回到等待池中。而競爭到對象鎖的線程則繼續(xù)往下執(zhí)行,直到執(zhí)行完了 synchronized 代碼塊,它會釋放掉該對象鎖,這時鎖池中的線程會繼續(xù)競爭該對象鎖。

小結(jié)

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

sleep() 和 wait() 有什么區(qū)別?

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

鎖類型

  • 可重入鎖:廣義上的可重入鎖指的是可重復(fù)可遞歸調(diào)用的鎖,在外層使用鎖之后,在內(nèi)層仍然可以使用,并且不發(fā)生死鎖(前提得是同一個對象或者class),這樣的鎖就叫做可重入鎖。即在執(zhí)行對象中所有同步方法不用再次獲得鎖。ReentrantLock和synchronized都是可重入鎖。舉個簡單的例子,當(dāng)一個線程執(zhí)行到某個synchronized方法時,比如說method1,而在method1中會調(diào)用另外一個synchronized方法method2,此時線程不必重新去申請鎖,而是可以直接執(zhí)行方法method2。
  • 可中斷鎖:在等待獲取鎖過程中可中斷。synchronized就不是可中斷鎖,而Lock是可中斷鎖。
  • 公平鎖: 按等待獲取鎖的線程的等待時間進(jìn)行獲取,等待時間長的具有優(yōu)先獲取鎖權(quán)利。非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進(jìn)行的,這樣就可能導(dǎo)致某個或者一些線程永遠(yuǎn)獲取不到鎖。synchronized是非公平鎖,它無法保證等待的線程獲取鎖的順序。對于ReentrantLock和ReentrantReadWriteLock,默認(rèn)情況下是非公平鎖,但是可以設(shè)置為公平鎖。
  • 讀寫鎖:對資源讀取和寫入的時候拆分為2部分處理,一個讀鎖和一個寫鎖。讀的時候可以多線程一起讀,寫的時候必須同步地寫。ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現(xiàn)了這個接口。可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。

什么是樂觀鎖和悲觀鎖

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

樂觀鎖的實現(xiàn)方式(CAS)

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

樂觀鎖是一種思想,CAS是這種思想的一種實現(xiàn)方式。

CAS的缺點

  1. ABA問題

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

  1. 循環(huán)時間長開銷很大

自旋CAS(不成功,就一直循環(huán)執(zhí)行,直到成功)如果長時間不成功,會給CPU帶來非常大的執(zhí)行開銷。

  1. 只能保證一個共享變量的原子操作。

當(dāng)對一個共享變量執(zhí)行操作時,我們可以使用循環(huán)CAS的方式來保證原子操作,但是對多個共享變量操作時,循環(huán)CAS就無法保證操作的原子性,這個時候就可以用鎖來保證原子性。

實現(xiàn)一個死鎖

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

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

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

volatile關(guān)鍵字

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

  • 防止重排序

問題:操作系統(tǒng)可以對指令進(jìn)行重排序,多線程環(huán)境下就可能將一個未初始化的對象引用暴露出來,從而導(dǎo)致不可預(yù)料的結(jié)果
解決原理:volatile關(guān)鍵字通過提供“內(nèi)存屏障”的方式來防止指令被重排序,為了實現(xiàn)volatile的內(nèi)存語義,編譯器在生成字節(jié)碼時,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。
1、在每個volatile寫操作前插入StoreStore屏障,在寫操作后插入StoreLoad屏障。
2、在每個volatile讀操作前插入LoadLoad屏障,在讀操作后插入LoadStore屏障。

  • 實現(xiàn)可見性

問題:可見性問題主要指一個線程修改了共享變量值,而另一個線程卻看不到
解決原理:(1)修改volatile變量時會強(qiáng)制將修改后的值刷新的主內(nèi)存中。
(2)修改volatile變量后會導(dǎo)致其他線程工作內(nèi)存中對應(yīng)的變量值失效。因此,再讀取該變量值的時候就需要重新從讀取主內(nèi)存中的值。

  • 注:volatile并不保證變量更新的原子性

volatile使用建議

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

volatile和synchronized區(qū)別

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

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

synchronized

synchronized可以保證方法或者代碼塊在運行時,同一時刻只有一個方法可以進(jìn)入到臨界區(qū),同時它還可以保證共享變量的內(nèi)存可見性。Synchronized主要有以下三個作用:保證互斥性、保證可見性、保證順序性。

synchronized的三種應(yīng)用方式

  • 修飾實例方法,作用于當(dāng)前實例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前實例的鎖。實現(xiàn)原理:指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先持有monitor(虛擬機(jī)規(guī)范中用的是管程一詞), 然后再執(zhí)行方法,最后再方法完成(無論是正常完成還是非正常完成)時釋放monitor。

    public synchronized void increase(){
        i++;
    }
    
  • 修飾靜態(tài)方法,作用于當(dāng)前類對象加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類對象的鎖

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

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

Lock

Lock是一個接口,它的的實現(xiàn)類提供了比synchronized更廣泛意義上鎖操作,它允許用戶更靈活的代碼結(jié)構(gòu),更多的不同特效。Lock的實現(xiàn)類主要有ReentrantLock和ReentrantReadWriteLock。

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

Lock接口中獲取鎖的方法

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

Condition類

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

Condition與Object中的wait, notify, notifyAll區(qū)別

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

synchronized和lock的區(qū)別

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

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

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

鎖的狀態(tài)

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

偏向鎖

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

輕量級鎖

輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進(jìn)入同步塊的情況下,當(dāng)?shù)诙€線程加入鎖爭用的時候,偏向鎖就會升級為輕量級鎖。輕量級鎖是在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用產(chǎn)生的性能消耗。輕量級鎖所適應(yīng)的場景是線程交替執(zhí)行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導(dǎo)致輕量級鎖膨脹為重量級鎖。
使用輕量級鎖時,不需要申請互斥量,僅僅將Mark Word中的部分字節(jié)CAS更新指向線程棧中的Lock Record,如果更新成功,則輕量級鎖獲取成功,記錄鎖狀態(tài)為輕量級鎖;否則,說明已經(jīng)有線程獲得了輕量級鎖,目前發(fā)生了鎖競爭(不適合繼續(xù)使用輕量級鎖),接下來膨脹為重量級鎖。

重量級鎖

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

自旋鎖

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

自適應(yīng)自旋鎖

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

  • 如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運行中,那么虛擬機(jī)就會認(rèn)為這次自旋也很有可能再次成功,進(jìn)而它將允許自旋等待持續(xù)相對更長的時間,比如100個循環(huán)。
  • 相反的,如果對于某個鎖,自旋很少成功獲得過,那在以后要獲取這個鎖時將可能減少自旋時間甚至省略自旋過程,以避免浪費處理器資源。

自適應(yīng)自旋解決的是“鎖競爭時間不確定”的問題。JVM很難感知到確切的鎖競爭時間,而交給用戶分析就違反了JVM的設(shè)計初衷。自適應(yīng)自旋假定不同線程持有同一個鎖對象的時間基本相當(dāng),競爭程度趨于穩(wěn)定,因此,可以根據(jù)上一次自旋的時間與結(jié)果調(diào)整下一次自旋的時間。

偏向鎖、輕量級鎖、重量級鎖適用于不同的并發(fā)場景

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

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

AQS

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

線程池

如果并發(fā)的線程數(shù)量很多,并且每個線程都是執(zhí)行一個時間很短的任務(wù)就結(jié)束了,這樣頻繁創(chuàng)建線程就會大大降低系統(tǒng)的效率,因為頻繁創(chuàng)建線程和銷毀線程需要時間。
線程池的產(chǎn)生和數(shù)據(jù)庫的連接池類似,系統(tǒng)啟動一個線程的代價是比較高昂的,如果在程序啟動的時候就初始化一定數(shù)量的線程,放入線程池中,在需要是使用時從池子中去,用完再放回池子里,這樣能大大的提高程序性能,再者,線程池的一些初始化配置,也可以有效的控制系統(tǒng)并發(fā)的數(shù)量,防止因為消耗過多的內(nèi)存,而把服務(wù)器累趴下。

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

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

使用線程池的好處

  • 減少了創(chuàng)建和銷毀線程的次數(shù),每個工作線程都可以被重復(fù)利用,可執(zhí)行多個任務(wù)。
  • 運用線程池能有效的控制線程最大并發(fā)數(shù),可以根據(jù)系統(tǒng)的承受能力,調(diào)整線程池中工作線線程的數(shù)目,防止因為消耗過多的內(nèi)存,而把服務(wù)器累趴下(每個線程需要大約1MB內(nèi)存,線程開的越多,消耗的內(nèi)存也就越大,最后死機(jī))。
  • 對線程進(jìn)行一些簡單的管理,比如:延時執(zhí)行、定時循環(huán)執(zhí)行的策略等,運用線程池都能進(jìn)行很好的實現(xiàn)

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

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

參考

Java 多線程
Java并發(fā):volatile內(nèi)存可見性和指令重排
并發(fā)編程的鎖機(jī)制:synchronized和lock
淺談偏向鎖、輕量級鎖、重量級鎖

獲取最新資訊,請關(guān)注微信公眾號:南強(qiáng)說晚安

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容