天天看點

java面試之多線程&并發篇

作者:朱聰蛋兒
java面試之多線程&并發篇

1、Java中實作多線程有幾種方法

繼承Thread類;

實作Runnable接口;

實作Callable接口通過FutureTask包裝器來建立Thread線程;

使用ExecutorService、Callable、Future實作有傳回結果的多線程(也就是使用了ExecutorService來管理前面的三種方式)。

2、如何停止一個正在運作的線程

1、使用退出标志,使線程正常退出,也就是當run方法完成後線程終止。

2、使用stop方法強行終止,但是不推薦這個方法,因為stop和suspend及resume一樣都是過期廢棄的

方法。

3、使用interrupt方法中斷線程。

class MyThread extends Thread {

volatile boolean stop = false;

public void run() {

while (!stop) {

System.out.println(getName() + " is running");

try {

sleep(1000);

} catch (InterruptedException e) {

System.out.println("week up from blcok...");

stop = true; // 在異常處理代碼中修改共享變量的狀态

}

}

System.out.println(getName() + " is exiting...");

}

}

class InterruptThreadDemo3 {

public static void main(String[] args) throws InterruptedException {

MyThread m1 = new MyThread();

System.out.println("Starting thread...");

m1.start();

Thread.sleep(3000);

System.out.println("Interrupt thread...: " + m1.getName());

m1.stop = true; // 設定共享變量為true

m1.interrupt(); // 阻塞時退出阻塞狀态

Thread.sleep(3000); // 主線程休眠3秒以便觀察線程m1的中斷情況

System.out.println("Stopping application...");

}

}

3、notify()和notifyAll()有什麼差別?

notify可能會導緻死鎖,而notifyAll則不會

任何時候隻有一個線程可以獲得鎖,也就是說隻有一個線程可以運作synchronized 中的代碼

使用notifyall,可以喚醒

所有處于wait狀态的線程,使其重新進入鎖的争奪隊列中,而notify隻能喚醒一個。

wait() 應配合while循環使用,不應使用if,務必在wait()調用前後都檢查條件,如果不滿足,必須調用

notify()喚醒另外的線程來處理,自己繼續wait()直至條件滿足再往下執行。

class MyThread extends Thread {

volatile boolean stop = false;

public void run() {

while (!stop) {

System.out.println(getName() + " is running");

try {

sleep(1000);

} catch (InterruptedException e) {

System.out.println("week up from blcok...");

stop = true; // 在異常處理代碼中修改共享變量的狀态

}

}

System.out.println(getName() + " is exiting...");

}

}

class InterruptThreadDemo3 {

public static void main(String[] args) throws InterruptedException {

MyThread m1 = new MyThread();

System.out.println("Starting thread...");

m1.start();

Thread.sleep(3000);

System.out.println("Interrupt thread...: " + m1.getName());

m1.stop = true; // 設定共享變量為true

m1.interrupt(); // 阻塞時退出阻塞狀态

Thread.sleep(3000); // 主線程休眠3秒以便觀察線程m1的中斷情況

System.out.println("Stopping application...");

}

}notify() 是對notifyAll()的一個優化,但它有很精确的應用場景,并且要求正确使用。不然可能導緻死

鎖。正确的場景應該是 WaitSet中等待的是相同的條件,喚醒任一個都能正确處理接下來的事項,如果

喚醒的線程無法正确處理,務必確定繼續notify()下一個線程,并且自身需要重新回到WaitSet中.

4、sleep()和wait() 有什麼差別?

對于sleep()方法,我們首先要知道該方法是屬于Thread類中的。而wait()方法,則是屬于Object類中

的。

sleep()方法導緻了程式暫停執行指定的時間,讓出cpu該其他線程,但是他的監控狀态依然保持者,當

指定的時間到了又會自動恢複運作狀态。在調用sleep()方法的過程中,線程不會釋放對象鎖。

當調用wait()方法的時候,線程會放棄對象鎖,進入等待此對象的等待鎖定池,隻有針對此對象調用

notify()方法後本線程才進入對象鎖定池準備,擷取對象鎖進入運作狀态。

5、volatile 是什麼?可以保證有序性嗎?

一旦一個共享變量(類的成員變量、類的靜态成員變量)被volatile修飾之後,那麼就具備了兩層語

義:

1)保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他

線程來說是立即可見的,volatile關鍵字會強制将修改的值立即寫入主存。

2)禁止進行指令重排序。

volatile 不是原子性操作

什麼叫保證部分有序性?

當程式執行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果

已經對後面的操作可見;在其後面的操作肯定還沒有進行;

x = 2; //語句1

y = 0; //語句2

flag = true; //語句3

x = 4; //語句4

y = -1; //語句5

由于flflag變量為volatile變量,那麼在進行指令重排序的過程的時候,不會将語句3放到語句1、語句2前

面,也不會講語句3放到語句4、語句5後面。但是要注意語句1和語句2的順序、語句4和語句5的順序是

不作任何保證的。

使用 Volatile 一般用于 狀态标記量 和 單例模式的雙檢鎖

6、Thread 類中的start() 和 run() 方法有什麼差別?

start()方法被用來啟動新建立的線程,而且start()内部調用了run()方法,這和直接調用run()方法的效果

不一樣。當你調用run()方法的時候,隻會是在原來的線程中調用,沒有新的線程啟動,start()方法才會

啟動新線程。

7、為什麼wait, notify 和 notifyAll這些方法不在thread類裡面?

x = 2; //語句1

y = 0; //語句2

flag = true; //語句3

x = 4; //語句4

y = -1; //語句5明顯的原因是JAVA提供的鎖是對象級的而不是線程級的,每個對象都有鎖,通過線程獲得。如果線程需

要等待某些鎖那麼調用對象中的wait()方法就有意義了。如果wait()方法定義在Thread類中,線程正在

等待的是哪個鎖就不明顯了。簡單的說,由于wait,notify和notifyAll都是鎖級别的操作,是以把他們

定義在Object類中因為鎖屬于對象。

8、為什麼wait和notify方法要在同步塊中調用?

1. 隻有在調用線程擁有某個對象的獨占鎖時,才能夠調用該對象的wait(),notify()和notifyAll()方法。

2. 如果你不這麼做,你的代碼會抛出IllegalMonitorStateException異常。

3. 還有一個原因是為了避免wait和notify之間産生競态條件。

wait()方法強制目前線程釋放對象鎖。這意味着在調用某對象的wait()方法之前,目前線程必須已經獲得

該對象的鎖。是以,線程必須在某個對象的同步方法或同步代碼塊中才能調用該對象的wait()方法。

在調用對象的notify()和notifyAll()方法之前,調用線程必須已經得到該對象的鎖。是以,必須在某個對

象的同步方法或同步代碼塊中才能調用該對象的notify()或notifyAll()方法。

調用wait()方法的原因通常是,調用線程希望某個特殊的狀态(或變量)被設定之後再繼續執行。調用

notify()或notifyAll()方法的原因通常是,調用線程希望告訴其他等待中的線程:"特殊狀态已經被設定"。

這個狀态作為線程間通信的通道,它必須是一個可變的共享狀态(或變量)。

9、Java中interrupted 和 isInterruptedd方法的差別?

interrupted() 和 isInterrupted()的主要差別是前者會将中斷狀态清除而後者不會。Java多線程的中斷機

制是用内部辨別來實作的,調用Thread.interrupt()來中斷一個線程就會設定中斷辨別為true。當中斷線

程調用靜态方法Thread.interrupted()來檢查中斷狀态時,中斷狀态會被清零。而非靜态方法

isInterrupted()用來查詢其它線程的中斷狀态且不會改變中斷狀态辨別。簡單的說就是任何抛出

InterruptedException異常的方法都會将中斷狀态清零。無論如何,一個線程的中斷狀态有有可能被其

它線程調用中斷來改變。

10、Java中synchronized 和 ReentrantLock 有什麼不同?

相似點:

這兩種同步方式有很多相似之處,它們都是加鎖方式同步,而且都是阻塞式的同步,也就是說當如果一

個線程獲得了對象鎖,進入了同步塊,其他通路該同步塊的線程都必須阻塞在同步塊外面等待,而進行

線程阻塞和喚醒的代價是比較高的.

差別:

這兩種方式最大差別就是對于Synchronized來說,它是java語言的關鍵字,是原生文法層面的互斥,需

要jvm實作。而ReentrantLock它是JDK 1.5之後提供的API層面的互斥鎖,需要lock()和unlock()方法配

合try/fifinally語句塊來完成。

Synchronized進過編譯,會在同步塊的前後分别形成monitorenter和monitorexit這個兩個位元組碼指

令。在執行monitorenter指令時,首先要嘗試擷取對象鎖。如果這個對象沒被鎖定,或者目前線程已經

擁有了那個對象鎖,把鎖的電腦加1,相應的,在執行monitorexit指令時會将鎖電腦就減1,當計

算器為0時,鎖就被釋放了。如果擷取對象鎖失敗,那目前線程就要阻塞,直到對象鎖被另一個線程釋

放為止。

由于ReentrantLock是java.util.concurrent包下提供的一套互斥鎖,相比Synchronized,

ReentrantLock類提供了一些進階功能,主要有以下3項:1.等待可中斷,持有鎖的線程長期不釋放的時候,正在等待的線程可以選擇放棄等待,這相當于

Synchronized來說可以避免出現死鎖的情況。

2.公平鎖,多個線程等待同一個鎖時,必須按照申請鎖的時間順序獲得鎖,Synchronized鎖非公平鎖,

ReentrantLock預設的構造函數是建立的非公平鎖,可以通過參數true設為公平鎖,但公平鎖表現的性

能不是很好。

3.鎖綁定多個條件,一個ReentrantLock對象可以同時綁定對個對象。

11、有三個線程T1,T2,T3,如何保證順序執行?

在多線程中有多種方法讓線程按特定順序執行,你可以用線程類的join()方法在一個線程中啟動另一個

線程,另外一個線程完成該線程繼續執行。為了確定三個線程的順序你應該先啟動最後一個(T3調用

T2,T2調用T1),這樣T1就會先完成而T3最後完成。

實際上先啟動三個線程中哪一個都行,

因為在每個線程的run方法中用join方法限定了三個線程的執行順序。

public class JoinTest2 {

// 1.現在有T1、T2、T3三個線程,你怎樣保證T2在T1執行完後執行,T3在T2執行完後執行

public static void main(String[] args) {

final Thread t1 = new Thread(new Runnable() {

@Override

public void run() {

System.out.println("t1");

}

});

final Thread t2 = new Thread(new Runnable() {

@Override

public void run() {

try {

// 引用t1線程,等待t1線程執行完

t1.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println("t2");

}

});

Thread t3 = new Thread(new Runnable() {

@Override

public void run() {

try {

// 引用t2線程,等待t2線程執行完

t2.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println("t3");

}

});

t3.start();//這裡三個線程的啟動順序可以任意,大家可以試下!

t2.start();

t1.start();

}

}

12、SynchronizedMap和ConcurrentHashMap有什麼差別?

SynchronizedMap()和Hashtable一樣,實作上在調用map所有方法時,都對整個map進行同步。而

ConcurrentHashMap的實作卻更加精細,它對map中的所有桶加了鎖。是以,隻要有一個線程通路

map,其他線程就無法進入map,而如果一個線程在通路ConcurrentHashMap某個桶時,其他線程,

仍然可以對map執行某些操作。

是以,ConcurrentHashMap在性能以及安全性方面,明顯比Collections.synchronizedMap()更加有優

勢。同時,同步操作精确控制到桶,這樣,即使在周遊map時,如果其他線程試圖對map進行資料修

改,也不會抛出ConcurrentModifificationException。

13、什麼是線程安全

線程安全就是說多線程通路同一代碼,不會産生不确定的結果。

在多線程環境中,當各線程不共享資料的時候,即都是私有(private)成員,那麼一定是線程安全的。

但這種情況并不多見,在多數情況下需要共享資料,這時就需要進行适當的同步控制了。

線程安全一般都涉及到synchronized, 就是一段代碼同時隻能有一個線程來操作 不然中間過程可能會

産生不可預制的結果。

如果你的代碼所在的程序中有多個線程在同時運作,而這些線程可能會同時運作這段代碼。如果每次運

行結果和單線程運作的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。

14、Thread類中的yield方法有什麼作用?

Yield方法可以暫停目前正在執行的線程對象,讓其它有相同優先級的線程執行。它是一個靜态方法而且

隻保證目前線程放棄CPU占用而不能保證使其它線程一定能占用CPU,執行yield()的線程有可能在進入

到暫停狀态後馬上又被執行。

15、Java線程池中submit() 和 execute()方法有什麼差別?

兩個方法都可以向線程池送出任務,execute()方法的傳回類型是void,它定義在Executor接口中, 而

submit()方法可以傳回持有計算結果的Future對象,它定義在ExecutorService接口中,它擴充了

Executor接口,其它線程池類像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有這些方

法。

16、說一說自己對于 synchronized 關鍵字的了解

synchronized關鍵字解決的是多個線程之間通路資源的同步性,synchronized關鍵字可以保證被它修

飾的方法或者代碼塊在任意時刻隻能有一個線程執行。

另外,在 Java 早期版本中,synchronized屬于重量級鎖,效率低下,因為螢幕鎖(monitor)是依

賴于底層的作業系統的 Mutex Lock 來實作的,Java 的線程是映射到作業系統的原生線程之上的。如果

要挂起或者喚醒一個線程,都需要作業系統幫忙完成,而作業系統實作線程之間的切換時需要從使用者态

轉換到核心态,這個狀态之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什麼早期的

synchronized 效率低的原因。慶幸的是在 Java 6 之後 Java 官方對從 JVM 層面對synchronized 較大優

化,是以現在的 synchronized 鎖效率也優化得很不錯了。JDK1.6對鎖的實作引入了大量的優化,如自

旋鎖、适應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。

17、說說自己是怎麼使用 synchronized 關鍵字,在項目中用到了

嗎synchronized關鍵字最主要的三種使用方式:

修飾執行個體方法: 作用于目前對象執行個體加鎖,進入同步代碼前要獲得目前對象執行個體的鎖

修飾靜态方法: 也就是給目前類加鎖,會作用于類的所有對象執行個體,因為靜态成員不屬于任何一個執行個體

對象,是類成員( static 表明這是該類的一個靜态資源,不管new了多少個對象,隻有一份)。是以如

果一個線程A調用一個執行個體對象的非靜态 synchronized 方法,而線程B需要調用這個執行個體對象所屬類的

靜态 synchronized 方法,是允許的,不會發生互斥現象,因為通路靜态 synchronized 方法占用的鎖

是目前類的鎖,而通路非靜态 synchronized 方法占用的鎖是目前執行個體對象鎖。

修飾代碼塊: 指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。

總結: synchronized 關鍵字加到 static 靜态方法和 synchronized(class)代碼塊上都是是給 Class 類上

鎖。synchronized 關鍵字加到執行個體方法上是給對象執行個體上鎖。盡量不要使用 synchronized(String a) 因

為JVM中,字元串常量池具有緩存功能!

18、什麼是線程安全?Vector是一個線程安全類嗎?

如果你的代碼所在的程序中有多個線程在同時運作,而這些線程可能會同時運作這段代碼。如果每次運

行結果和單線程運作的結果是一樣的,而且其他的變量 的值也和預期的是一樣的,就是線程安全的。一

個線程安全的計數器類的同一個執行個體對象在被多個線程使用的情況下也不會出現計算失誤。很顯然你可

以将集合類分 成兩組,線程安全和非線程安全的。Vector 是用同步方法來實作線程安全的, 而和它相似

的ArrayList不是線程安全的。

19、 volatile關鍵字的作用?

一旦一個共享變量(類的成員變量、類的靜态成員變量)被volatile修飾之後,那麼就具備了兩層語

義:

保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他

線程來說是立即可見的。

禁止進行指令重排序。

volatile本質是在告訴jvm目前變量在寄存器(工作記憶體)中的值是不确定的,需要從主存中讀

取;synchronized則是鎖定目前變量,隻有目前線程可以通路該變量,其他線程被阻塞住。

volatile僅能使用在變量級别;synchronized則可以使用在變量、方法、和類級别的。

volatile僅能實作變量的修改可見性,并不能保證原子性;synchronized則可以保證變量的修改可

見性和原子性。

volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞。

volatile标記的變量不會被編譯器優化;synchronized标記的變量可以被編譯器優化。

20、常用的線程池有哪些?

newSingleThreadExecutor:建立一個單線程的線程池,此線程池保證所有任務的執行順序按照

任務的送出順序執行。

newFixedThreadPool:建立固定大小的線程池,每次送出一個任務就建立一個線程,直到線程達

到線程池的最大大小。

newCachedThreadPool:建立一個可緩存的線程池,此線程池不會對線程池大小做限制,線程池

大小完全依賴于作業系統(或者說JVM)能夠建立的最大線程大小。

newScheduledThreadPool:建立一個大小無限的線程池,此線程池支援定時以及周期性執行任

務的需求。

newSingleThreadExecutor:建立一個單線程的線程池。此線程池支援定時以及周期性執行任務

的需求。

21、簡述一下你對線程池的了解

(如果問到了這樣的問題,可以展開的說一下線程池如何用、線程池的好處、線程池的啟動政策)合理

利用線程池能夠帶來三個好處。

第一:降低資源消耗。通過重複利用已建立的線程降低線程建立和銷毀造成的消耗。

第二:提高響應速度。當任務到達時,任務可以不需要等到線程建立就能立即執行。

第三:提高線程的可管理性。線程是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系

統的穩定性,使用線程池可以進行統一的配置設定,調優和監控。

22、Java程式是如何執行的

我們日常的工作中都使用開發工具(IntelliJ IDEA 或 Eclipse 等)可以很友善的調試程式,或者是通過打

包工具把項目打包成 jar 包或者 war 包,放入 Tomcat 等 Web 容器中就可以正常運作了,但你有沒有

想過 Java 程式内部是如何執行的?其實不論是在開發工具中運作還是在 Tomcat 中運作,Java 程式的

執行流程基本都是相同的,它的執行流程如下:

先把 Java 代碼編譯成位元組碼,也就是把 .java 類型的檔案編譯成 .class 類型的檔案。這個過程的

大緻執行流程:Java 源代碼 -> 詞法分析器 -> 文法分析器 -> 語義分析器 -> 字元碼生成器 -> 最終

生成位元組碼,其中任何一個節點執行失敗就會造成編譯失敗;

把 class 檔案放置到 Java 虛拟機,這個虛拟機通常指的是 Oracle 官方自帶的 Hotspot JVM;

Java 虛拟機使用類加載器(Class Loader)裝載 class 檔案;

類加載完成之後,會進行位元組碼效驗,位元組碼效驗通過之後 JVM 解釋器會把位元組碼翻譯成機器碼

交由作業系統執行。但不是所有代碼都是解釋執行的,JVM 對此做了優化,比如,以 Hotspot 虛

拟機來說,它本身提供了 JIT(Just In Time)也就是我們通常所說的動态編譯器,它能夠在運作時

将熱點代碼編譯為機器碼,這個時候位元組碼就變成了編譯執行。Java 程式執行流程圖如下:

java面試之多線程&并發篇

23、說一說自己對于 synchronized 關鍵字的了解

synchronized關鍵字解決的是多個線程之間通路資源的同步性,synchronized關鍵字可以保證被它修

飾的方法或者代碼塊在任意時刻隻能有一個線程執行。

另外,在 Java 早期版本中,synchronized屬于重量級鎖,效率低下,因為螢幕鎖(monitor)是依

賴于底層的作業系統的 Mutex Lock 來實作的,Java 的線程是映射到作業系統的原生線程之上的。如果

要挂起或者喚醒一個線程,都需要作業系統幫忙完成,而作業系統實作線程之間的切換時需要從使用者态

轉換到核心态,這個狀态之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什麼早期的

synchronized 效率低的原因。慶幸的是在 Java 6 之後 Java 官方對從 JVM 層面對synchronized 較大優

化,是以現在的 synchronized 鎖效率也優化得很不錯了。JDK1.6對鎖的實作引入了大量的優化,如自

旋鎖、适應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。

24、說說自己是怎麼使用 synchronized 關鍵字,在項目中用到了

synchronized關鍵字最主要的三種使用方式:

修飾執行個體方法,作用于目前對象執行個體加鎖,進入同步代碼前要獲得目前對象執行個體的鎖

修飾靜态方法,作用于目前類對象加鎖,進入同步代碼前要獲得目前類對象的鎖 。也就是給目前

類加鎖,會作用于類的所有對象執行個體,因為靜态成員不屬于任何一個執行個體對象,是類成員( static

表明這是該類的一個靜态資源,不管new了多少個對象,隻有一份,是以對該類的所有對象都加了

鎖)。是以如果一個線程A調用一個執行個體對象的非靜态 synchronized 方法,而線程B需要調用這個

執行個體對象所屬類的靜态 synchronized 方法,是允許的,不會發生互斥現象,因為通路靜态

synchronized 方法占用的鎖是目前類的鎖,而通路非靜态 synchronized 方法占用的鎖是目前

執行個體對象鎖。修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。 和

synchronized 方法一樣,synchronized(this)代碼塊也是鎖定目前對象的。synchronized 關鍵字

加到 static 靜态方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖。這裡再提一下:

synchronized關鍵字加到非 static 靜态方法上是給對象執行個體上鎖。另外需要注意的是:盡量不要

使用 synchronized(String a) 因為JVM中,字元串常量池具有緩沖功能!

下面我已一個常見的面試題為例講解一下 synchronized 關鍵字的具體使用。

面試中面試官經常會說:“單例模式了解嗎?來給我手寫一下!給我解釋一下雙重檢驗鎖方式實作單利

模式的原理呗!”

雙重校驗鎖實作對象單例(線程安全)

public class Singleton {

private volatile static Singleton uniqueInstance;

private Singleton() {

}

public static Singleton getUniqueInstance() {

//先判斷對象是否已經執行個體過,沒有執行個體化過才進入加鎖代碼

if (uniqueInstance == null) {

//類對象加鎖

synchronized (Singleton.class) {

if (uniqueInstance == null) {

uniqueInstance = new Singleton();

}

}

}

return uniqueInstance;

}

}

另外,需要注意 uniqueInstance 采用 volatile 關鍵字修飾也是很有必要。

uniqueInstance 采用 volatile 關鍵字修飾也是很有必要的, uniqueInstance = new Singleton(); 這段

代碼其實是分為三步執行:

1. 為 uniqueInstance 配置設定記憶體空間

2. 初始化 uniqueInstance

3. 将 uniqueInstance 指向配置設定的記憶體位址

但是由于 JVM 具有指令重排的特性,執行順序有可能變成 1->3->2。指令重排在單線程環境下不會出先

問題,但是在多線程環境下會導緻一個線程獲得還沒有初始化的執行個體。例如,線程 T1 執行了 1 和 3,

此時 T2 調用 getUniqueInstance() 後發現 uniqueInstance 不為空,是以傳回 uniqueInstance,但此

時 uniqueInstance 還未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保證在多線程環境下也能正常運作。

25、 講一下 synchronized 關鍵字的底層原理

synchronized 關鍵字底層原理屬于 JVM 層面。

① synchronized 同步語句塊的情況

public class SynchronizedDemo {

public void method() {

synchronized (this) {

System.out.println("synchronized 代碼塊");

}

}

}

通過 JDK 自帶的 javap 指令檢視 SynchronizedDemo 類的相關位元組碼資訊:首先切換到類的對應目錄

執行 javac SynchronizedDemo.java 指令生成編譯後的 .class 檔案,然後執行 javap -c -s -v -l

SynchronizedDemo.class 。

java面試之多線程&并發篇

從上面我們可以看出:

synchronized 同步語句塊的實作使用的是 monitorenter 和 monitorexit 指令,其中

monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置。

當執行 monitorenter 指令時,線程試圖擷取鎖也就是擷取 monitor(monitor對象存在于每個Java對象

的對象頭中,synchronized 鎖便是通過這種方式擷取鎖的,也是為什麼Java中任意對象可以作為鎖的

原因) 的持有權.當計數器為0則可以成功擷取,擷取後将鎖計數器設為1也就是加1。相應的在執行

monitorexit 指令後,将鎖計數器設為0,表明鎖被釋放。如果擷取對象鎖失敗,那目前線程就要阻塞等

待,直到鎖被另外一個線程釋放為止。

② synchronized 修飾方法的的情況

public class SynchronizedDemo2 {

public synchronized void method() {

System.out.println("synchronized 方法");

}

}

java面試之多線程&并發篇

synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的确實是

ACC_SYNCHRONIZED 辨別,該辨別指明了該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED

通路标志來辨識一個方法是否聲明為同步方法,進而執行相應的同步調用

26、 為什麼要用線程池?

線程池提供了一種限制和管理資源(包括執行一個任務)。 每個線程池還維護一些基本統計資訊,例如

已完成任務的數量。

這裡借用《Java并發程式設計的藝術》提到的來說一下使用線程池的好處:

降低資源消耗。 通過重複利用已建立的線程降低線程建立和銷毀造成的消耗。

提高響應速度。 當任務到達時,任務可以不需要的等到線程建立就能立即執行。

提高線程的可管理性。 線程是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系

統的穩定性,使用線程池可以進行統一的配置設定,調優和監控。

27、 實作Runnable接口和Callable接口的差別

如果想讓線程池執行任務的話需要實作的Runnable接口或Callable接口。 Runnable接口或Callable接

口實作類都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執行。兩者的差別在于

Runnable 接口不會傳回結果但是 Callable 接口可以傳回結果。

備注: 工具類 Executors 可以實作 Runnable 對象和 Callable 對象之間的互相轉換。

( Executors.callable(Runnable task) 或 Executors.callable(Runnable task,Object

resule) )。

28、 執行execute()方法和submit()方法的差別是什麼呢?

1) execute() 方法用于送出不需要傳回值的任務,是以無法判斷任務是否被線程池執行成功與否;

2)submit()方法用于送出需要傳回值的任務。線程池會傳回一個future類型的對象,通過這個future

對象可以判斷任務是否執行成功,并且可以通過future的get()方法來擷取傳回值,get()方法會阻塞目前

線程直到任務完成,而使用 get(long timeout,TimeUnit unit) 方法則會阻塞目前線程一段時間

後立即傳回,這時候有可能任務沒有執行完。

29、 如何建立線程池

《阿裡巴巴Java開發手冊》中強制線程池不允許使用 Executors 去建立,而是通過

ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明确線程池的運作規則,規避資源耗盡

的風險**

Executors 傳回線程池對象的弊端如下:

FixedThreadPool 和 SingleThreadExecutor : 允許請求的隊列長度為

Integer.MAX_VALUE,可能堆積大量的請求,進而導緻OOM。

CachedThreadPool 和 ScheduledThreadPool : 允許建立的線程數量為

Integer.MAX_VALUE ,可能會建立大量線程,進而導緻OOM。

方式一:通過構造方法實作

java面試之多線程&并發篇

方式二:通過Executor 架構的工具類Executors來實作 我們可以建立三種類型的

ThreadPoolExecutor:

FixedThreadPool : 該方法傳回一個固定線程數量的線程池。該線程池中的線程數量始終不變。

當有一個新的任務送出時,線程池中若有空閑線程,則立即執行。若沒有,則新的任務會被暫存在

一個任務隊列中,待有線程空閑時,便處理在任務隊列中的任務。

SingleThreadExecutor: 方法傳回一個隻有一個線程的線程池。若多餘一個任務被送出到該線

程池,任務會被儲存在一個任務隊列中,待線程空閑,按先入先出的順序執行隊列中的任務。

CachedThreadPool: 該方法傳回一個可根據實際情況調整線程數量的線程池。線程池的線程數

量不确定,但若有空閑線程可以複用,則會優先使用可複用的線程。若所有線程均在工作,又有新

的任務送出,則會建立新的線程處理任務。所有線程在目前任務執行完畢後,将傳回線程池進行複

用。

對應Executors工具類中的方法如圖所示:

java面試之多線程&并發篇