Java面試——多執行緒
1、什麼是執行緒?
執行緒是指程式在執行的過程中,能夠執行程式程式碼的一個執行單元。Java語言中,執行緒有四種狀態:執行、就緒、掛起、結束
2、執行緒與程序的區別?
程序是指一段正在執行的程式。而執行緒有事也被稱為輕量級程序,它是程式執行的最小單元,一個程序可以擁有多個執行緒,各個執行緒之間共享程式的記憶體空間(程式碼段、資料段、堆空間)及一些程序級的檔案(列如:開啟的檔案),但是各個執行緒擁有自己的棧空間。在作業系統級別上,程式的執行都是以程序為單位的,而每個程序中通常都會有多個執行緒互不影響地併發執行。
3、為什麼要使用多執行緒?
【1】、提高執行效率,減少程式的響應時間。因為單執行緒執行的過程只有一個有效的操作序列,如果某個操作很耗時(或等待網路響應),此時程式就不會響應滑鼠和鍵盤等操作,如果使用多執行緒,就可以將耗時的執行緒分配到一個單獨的執行緒上執行,從而使程式具備號更好的互動性。
【2】、與程序相比,執行緒的建立和切換開銷更小。因開啟一個新的程序需要分配獨立的地址空間,建立許多資料結構來維護程式碼塊等資訊,而運行於同一個程序內的執行緒共享程式碼段、資料段、執行緒的啟動和切換的開銷比程序要少很多。同時多執行緒在資料共享方面效率非常高。
【3】、目前市場上伺服器配置大多數都是多CPU或多核計算機等,它們本身而言就具有執行多執行緒的能力,如果使用單個執行緒,就無法重複利用計算機資源,造成資源浪費。因此在多CPU計算機上使用多執行緒能提高CPU的利用率。
【4】、利用多執行緒能簡化程式程式的結構,是程式便於理解和維護。一個非常複雜的程序可以分成多個執行緒來執行。
4、同步與非同步有什麼區別?
在多執行緒的環境中,通常會遇到資料共享問題,為了確保共享資源的正確性和安全性,就必須對共享資料進行同步處理(也就是鎖機制)。對共享資料進行同步操作(增刪改),就必須要獲得每個執行緒物件的鎖(this鎖),這樣可以保證同一時刻只有一個執行緒對其操作,其他執行緒要想對其操作需要排隊等候並獲取鎖。當然在等候佇列中優先順序最高的執行緒才能獲得該鎖,從而進入共享程式碼區。
Java語言在同步機制中提供了語言級的支援,可以通過使用synchronize關鍵字來實現同步,但該方法是以很大的系統開銷作為代價的,有時候甚至可能造成死鎖,所以,同步控制並不是越多越好,要避免所謂的同步控制。實現同步的方法有兩種:①、同步方法(this鎖)。②、同步程式碼塊(this鎖或者自定義鎖)當使用this鎖時,就與同步方法共享同一鎖,只有當①釋放,②才可以使用。同時,同步程式碼塊的範圍也小於同步方法,建議使用,相比之下能夠提高效能。
5、如何實現Java多執行緒
Java虛擬機器允許應用程式併發地執行多個執行緒,在Java語言中實現多執行緒的方法有三種,其中前兩種為常用方法:
【1】繼承Thread類,重寫run()方法
Thread本質上也是實現了Runnable介面的一個例項,它代表一個執行緒的例項,並且啟動執行緒的唯一方法就是通過Thread類的start()方法,start()方法是一個本地(native)方法,它將啟動一個新的執行緒,並執行run()方法(執行的是自己重寫了Thread類的run()方法),同時呼叫start()方法並不是執行多執行緒程式碼,而是使得該執行緒變為可執行態(Runnable),什麼時候執行多執行緒程式碼由作業系統決定。
class MyThread extends Thread{//建立執行緒類
public void run(){
System.out.println("Thread Body");//執行緒的函式體
}
}
public class Test{
public static void main(String[] args){
MyThread thread = new Thread
thread.run();//開啟執行緒
}
}
【2】、實現Runnable介面,並實現該結構的run()方法
1)自定義實現Runnable介面,實現run()方法。
2)建立Thread物件,用實現Runnable介面的物件作為引數例項化該Thread物件。
3)呼叫Thread的start()方法。
class MyThread implements Runnable{
pulic void run(){
System.out.println("Thread Body");
}
}
public class Test{
public static void main(String[] args){
MyThread myThread = new MyThread;
Thread thread = new Thread(myThread);
thread.start();//啟動執行緒
}
}
其實,不管是哪種方法,最終都是通過Thread類的API來控制執行緒。
【3】、實現Callable介面,重寫call()方法
Callable介面實際是屬於Executor框架中的功能類,Callable結構與Runnable介面的功能類似,但提供了比Runnable更強大的功能,主要體現在如下三點:
1)、Callable在任務結束後可以提供一個返回值,Runnable無法提供該功能。
2)、Callable中的call()方法可以跑出異常,而Runnable中的run()不能跑出異常。
3)、執行Callable可以拿到一個Future物件,Future物件表示非同步計算的結果,它提供能了檢查計算是否完成的方法。由於執行緒輸入非同步計算模型,因此無法從別的執行緒中得到函式的返回值,在這種情況下,就可以使用Future來監控目標執行緒來呼叫call()方法的情況,當呼叫Future的get()方法以獲取結果時,當前執行緒會阻塞,直到目標執行緒的call()方法結束返回結果。
public class CallableAndFuture{
//建立執行緒類
public static class CallableTest implements Callable{
public String call() throws Exception{
return "Hello World!";
}
}
public static void main(String[] args){
ExecutorService threadPool = Executors.newSingleThreadExecutor();
Future<String> future = threadPool.submit(new CallableTest());
try{
System.out.println("waiting thread to finish");
System.out.println(future.get());
}catch{Exception e}{
e.printStackTrace
}
}
}
輸出結果:waiting thread to finish
Hello World!
建議:當需要實現多執行緒時,一般推薦使用Runnable介面方式,因為Thread類定義了多種方法可以被派生類使用或重寫,但是隻有run()方法必須被重寫,在run()方法中實現這個執行緒的主要功能,這當然也是實現Runnable介面所需的方法。再者,我們很多時候繼承一個類是為了去加強和修改這個類才去繼承的。因此,如果我們沒有必要重寫Thread類中的其他方法,那麼通過繼承Thread類和實現Runnable介面的效果是相同的,這樣的話最好還是使用Runnable介面來建立執行緒。
【引申】:一個類是否可以同時繼承Thread類和實現Runnable介面?
答案是可以的。
public class Test Extend Thread implements Runnable{
public static void main(String[] args){
Thread thread = new Thread(new Test);
thread.start();
}
}
如下,Test實現了Runnable介面,但是並沒有實現介面的run()方法,可能會認為有編譯錯誤,但實際是可以編譯通過的,以為Test從Thread中繼承了run()方法,這個繼承的run()方法被當做Runnable介面的實現,因此是可以編譯通過的,當然也可以自己重寫,重寫後再呼叫run()方式時,那就是自己重寫後的方法了。
6、run()方法與start()方法有什麼區別
通常,系統通過呼叫執行緒類的start()方法啟動一個執行緒,此時該執行緒處於就緒狀態,而非執行狀態,也就意味著這個執行緒可以別JVM呼叫執行,執行的過程中,JVM通過呼叫想成類的run()方法來完成實際的操作,當run()方法結束後,執行緒也就會終止。
如果直接呼叫執行緒類的run()方法,就會被當做一個普通函式呼叫,程式中仍然只有一個主程式,也就是說start()方法能夠非同步呼叫run()方法,但是直接呼叫run()方法卻是同步的,也就無法達到多執行緒的目的。
7、多執行緒資料同步實現的方法有哪些?
當使用多執行緒訪問同一資料時,非常容易出現執行緒安全問題,因此採用同步機制解決。Java提供了三種方法:
【1】、synchronized關鍵字
在Java語言中,每個物件都有一個物件鎖與之相關聯,該鎖表明物件在任何時候只允許被一個執行緒所擁有,當一個執行緒呼叫物件的synchronize程式碼時,需要先獲取這個鎖,然後再去執行相應的程式碼,執行結束後,釋放鎖。
synchronize關鍵字主要有兩種用法(synchronize方法和synchronize程式碼塊)
1)、synchronized方法:在方法的宣告前加synchronize關鍵字:
public synchronize void test();
將需要對同步資源的操作放入test()方法中,就能保證此資源在同一時刻只能被一個執行緒呼叫,從而保證資源的安全性。然而當此方法體規模非常大時,會影響系統的效率。
2)、synchronized塊:既可以把任意的程式碼段宣告為synchronized,也可以指定上鎖的物件,有非常高的靈活性。
synchronized(syncObject){
//訪問syncObject的程式碼塊
}
【2】、wait()方法與notify()方法
當使用synchronized來修飾某個共享資源時,如果執行緒A1在執行synchronized程式碼,執行緒A2也要執行同一物件的統同一synchronize的程式碼,執行緒A2將要等到執行緒A1執行完後執行,這種情況可以使用wai()和notify()。必須是統一把鎖,才生效。
class NumberPrint implements Runnable{
private int number;
public byte res[];
public static int count = 5;
public NumberPrint(int number, byte a[]){
this.number = number;
res = a;
}
public void run(){
synchronized (res){
while(count-- > 0){
try {
res.notify();//喚醒等待res資源的執行緒,把鎖交給執行緒(該同步鎖執行完畢自動釋放鎖)
System.out.println(" "+number);
res.wait();//釋放CPU控制權,釋放res的鎖,本執行緒阻塞,等待被喚醒。
System.out.println("------執行緒"+Thread.currentThread().getName()+"獲得鎖,wait()後的程式碼繼續執行:"+number);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}//end of while
return;
}//synchronized
}
}
public class WaitNotify {
public static void main(String args[]){
final byte a[] = {0};//以該物件為共享資源
new Thread(new NumberPrint((1),a),"1").start();
new Thread(new NumberPrint((2),a),"2").start();
}
}
輸出結果:
1
2
------執行緒1獲得鎖,wait()後的程式碼繼續執行:1
1
------執行緒2獲得鎖,wait()後的程式碼繼續執行:2
2
------執行緒1獲得鎖,wait()後的程式碼繼續執行:1
1
------執行緒2獲得鎖,wait()後的程式碼繼續執行:2
【3】、Lock
JDK5新增加Lock介面以及它的一個實現類ReentrantLock(重入鎖),也可以實現多執行緒的同步;
1)、lock():以阻塞的方式獲取鎖,也就是說,如果獲取到了鎖,就會執行,其他執行緒需要等待,unlock()鎖後別的執行緒才能執行,如果別的執行緒持有鎖,當前執行緒等待,直到獲取鎖後返回。
public int consume(){
int m = 0;
try {
lock.lock();
while(ProdLine.size() == 0){
System.out.println("佇列是空的,請稍候");
empty.await();
}
m = ProdLine.removeFirst();
full.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
return m;
}
}
2)、tryLock()。以非阻塞的方式獲取鎖。只是嘗試性地去獲取一下鎖,如果獲取到鎖,立即返回true,否則,返回false。
3)、tryLock(long timeout,TimeUnit unit)。在給定的時間單元內,獲取到了鎖返回true,否則false。
4)、lockInterruptibly().如果獲取了鎖,立即返回;如果沒有鎖,當前執行緒處於休眠狀態,直到獲取鎖,或者當前執行緒被中斷(會收到InterruptedException異常)。它與lock()方法最大的區別在於如果()方法獲取不到鎖,就會一直處於阻塞狀態,且會忽略Interrupt()方法。
8、sleep()方法與wait()方法有什麼區別?
sleep()是使執行緒暫停執行一段時間的方法。wait()也是一種使執行緒暫停執行的方法,直到被喚醒或等待時間超時。
區別:1)、原理不同:sleep()方法是Thread類的靜態方法,是執行緒用來控制自身流程的,它會使此執行緒暫停執行一段時間,而把執行機會讓給其他執行緒,等到時間一到,此執行緒會自動“甦醒”。
wait()方法是Object類的方法,用於執行緒間通訊,這個方法會使當前執行緒擁有該物件鎖的程序等待,直到其他執行緒呼叫notify()方法(或notifyAll方法)時才“醒”來,不過開發人員可可以給它指定一個時間,自動“醒”來。與wait()方法配套的方法還有notify()和notifyAll()方法。
2)、對鎖的處理機制不同。由於sleep()方法的主要作用是讓執行緒暫停執行一段時間,時間一到則自動恢復,不涉及執行緒間的通訊,因此,呼叫sleep()方法並不會釋放鎖。而wait()方法則不同,呼叫後會釋放掉他所佔用的鎖,從而使執行緒所在物件中的其他synchronized資料可被別的執行緒使用。
3)、使用區域不同,由於wait()的特殊意義,因此它必須放在同步控制方法或者同步程式碼塊中使用,而sleep()則可以放在任何地方使用。
4)、sleep()方法 必須捕獲異常,而wait()、notify()、notifyAll()不需要捕獲異常。在sleep的過程中,有可能被其他物件呼叫它的interrupt(),產生InterruptedException異常。
sleep不會釋放“鎖標誌”,容易導致死鎖問題的發生,因此,一般情況下,不推薦使用sleep()方法。而推薦使用wait()方法
9、sleep()與yield()的區別?
1)、sleep()給其他執行緒執行機會時,不考慮執行緒的優先順序,因此會給低優先順序的執行緒以執行的機會,而yield()方法只會給相同優先順序或更高優先順序的執行緒以執行的機會。
2)、sleep()方法會轉入阻塞狀態,所以,執行sleep()方法的執行緒在指定的時間內不會被執行,而yield()方法只是使當前執行緒重新回到可執行狀態,所以執行yield()方法的執行緒很可能在進入到可執行狀態後馬上又被執行。
10、終止執行緒的方法有哪些?
1)、stop()方法,它會釋放已經鎖定的所有監視資源,如果當前任何一個受監視資源保護的物件處於一個不一致的狀態(執行了一部分),其他執行緒執行緒將會獲取到修改了的部分值,這個時候就可能導致程式執行結果的不確定性,並且這種問題很難被定位。
2)、suspend()方法,容易發生死鎖。因為呼叫suspend()方法不會釋放鎖,這就會導致此執行緒掛起。
鑑於以上兩種方法的不安全性,Java語言已經不建議使用以上兩種方法來終止執行緒了。
3)、一般建議採用的方法是讓執行緒自行結束進入Dead狀態。一個執行緒進入Dead狀態,既執行完run()方法,也就是說提供一種能夠自動讓run()方法結束的方式,在實際中,我們可以通過flag標誌來控制迴圈是否執行,從而使執行緒離開run方法終止執行緒
public class MyThread implements Runnable{
private volatile Boolean flag;
public void stop(){
flag=false;
}
public void run(){
while(flag);//do something
}
}
上述通過stop()方法雖然可以終止執行緒,但同樣也存在問題;當執行緒處於阻塞狀態時(sleep()被呼叫或wait()方法被呼叫或當被I/O阻塞時),上面介紹的方法就不可用了。此時使用interrupt()方法來打破阻塞的情況,當interrupt()方法被呼叫時,會跑出interruptedException異常,可以通過在run()方法中捕獲這個異常來讓執行緒安全退出。
public class MyThread{
public static void main(String[] args){
Thread thread = new Thread(new MyThread);
public void run(){
System.out.println("thread go to sleep");
try{
//用休眠來模擬執行緒被阻塞
Thread.sleep(5000);
System.out.println("thread finish");
} catch (InterruptedException e){
System.out.println("thread is interrupted!);
}
}
}
}
thread.start();
therad.interrupt();
程式執行結果:thread go to sleep
thread is interrupted!
如果I/0停滯,進入非執行狀態,基本上要等到I/O完成才能離開這個狀態。或者通過出發異常,使用readLine()方法在等待網路上的一個資訊,此時執行緒處於阻塞狀態,讓程式離開run()就出發close()方法來關閉流,這個時候就會跑出IOException異常,通過捕獲此異常就可以離開run()。
11、synchronized與Lock有什麼異同?
Java語言中提供了兩種鎖機制的實現對某個共享資源的同步;synchronized和Lock。其中synchronized使用Object類物件本身的notify()、wait()、notifyAll()排程機制,而Lock使用condition包進行執行緒之間的排程,完成synchronized實現的所有功能
1)、用法不一樣。synchronized既可以加在方法上,也可以加在特定的程式碼塊中,括號中表示需要的鎖物件。而Lock需要顯式的指定起始位置和終止位置。synchronized是託管給JVM執行的,而Lock的鎖定是通過程式碼實現,他有比synchronized更精確的執行緒語義。
2)、效能不一樣。在JDK5中增加了一個Lock介面的實現類ReentrantLock。它不僅擁有和synchronized相同的併發性和記憶體語義、還多了鎖投票、定時鎖、等候鎖和中斷鎖。它們的效能在不同的情況下會有所不同;在資源競爭不激烈的情況下,synchronized的效能要優於RenntrantLock,但是資源競爭激烈的情況下,synchronized效能會下降的非常快,而ReentrantLock的效能基本保持不變。
3)、鎖機制不一樣。synchronized獲得鎖和釋放鎖的方式都是在塊結構中,當獲取多個鎖時,必須以相反的順序釋放,並且自動解鎖,而condition中的await()、signal()、signalAll()能夠指定要釋放的鎖。不會因為異常而導致鎖沒有被釋放從而引發死鎖的問題。而Lock則需要開發人員手動釋放,並且必須放在finally塊中釋放,否則會引起死鎖問題。此外,Lock還提供了更強大的功能,他的tryLock()方法可以採用非阻塞的方式去獲取鎖。
雖然synchronized與Lock都可以實現多執行緒的同步,但是最好不要同時使用這兩種同步機制給統一共享資源加鎖(不起作用),因為ReentrantLock與synchronized所使用的機制不同,所以它們執行時獨立的,相當於兩個種類的鎖,在使用的時候互不影響。
面試題:【1】、當一個執行緒進入一個物件的synchronized()方法後,其他執行緒是否能夠進入此物件的其他方法?
答案:其他執行緒可進入此物件的非synchronized修飾的方法。如果其他方法有synchronized修飾,都用的是同一物件鎖,就不能訪問。
【2】、如果其他方法是靜態方法,且被synchronized修飾,是否可以訪問?
答案:可以的,因為static修飾的方法,它用的鎖是當前類的位元組碼,而非靜態方法使用的是this,因此可以呼叫。
12、什麼是守護執行緒?
Java提供了兩種執行緒:守護執行緒和使用者執行緒。守護執行緒又被稱為“服務程序”、“精靈執行緒”、“後臺執行緒”,是指在程式執行時在後臺提供一種通用服務的執行緒,這種執行緒並不屬於程式中不可或缺的部分,通俗點講,每一個守護執行緒都是JVM中非守護執行緒的“保姆”。典型例子就是“垃圾回收器”。只要JVM啟動,它始終在執行,實時監控和管理系統中可以被回收的資源。
使用者執行緒和守護執行緒幾乎一樣,唯一的不同就在於如果使用者執行緒已經全部退出執行,只剩下守護執行緒執行,JVM也就退出了因為當所有非守護執行緒結束時,沒有了守護者,守護執行緒就沒有工作可做,也就沒有繼續執行程式的必要了,程式也就終止了,同時會“殺死”所有的守護執行緒。也就是說,只要有任何非守護執行緒執行,程式就不會終止。
Java語言中,守護執行緒優先順序都較低,它並非只有JVM內部提供,使用者也可以自己設定守護執行緒,方法就是在呼叫執行緒的start()方法之前,設定setDaemon(true)方法,若將引數設定為false,則表示使用者程序模式。需要注意的是,守護執行緒中產生的其它執行緒都是守護執行緒,使用者執行緒也是如此。
class ThreadDemo extends Thread{
public void run(){
System.out.println(Thread.currentThread().getName()+":begin");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":end");
}
}
public class Test{
public static void main(String[] args){
System.out.println("test3:begin");
Thread thread = new ThreadDemo();
thread.setDaemon(true);
thread.start();
System.out.println("test3:end");
}
}
程式執行結果:test3:begin
test3:end
Thread-0:begin
從執行結果上可以發現,沒有輸出Thread-0:end。之所以是這樣的結果,是在啟動執行緒前,將其設定成為了守護執行緒了,當程式中只有守護執行緒時,JVM是可以退出的,也就是說,當JVM中只有守護執行緒執行時,JVM會自動關閉。因此,當test3方法呼叫結束後,mian執行緒將退出,此時執行緒thread還處於休眠狀態沒有執行結果,但是由於此時只有這個守護執行緒在執行,JVM將會關閉,因此不會輸出:“Thread-0:end”。
13、join()方法的作用是什麼?
在Java語言中,join()方法的作用是讓呼叫該方法的執行緒在執行完run()方法後,再執行join方法後面的程式碼。簡單點說就是將兩個執行緒合併,並實現同步功能。具體而言,可以通過執行緒A的join()方法來等待執行緒A的結束,或者使用執行緒A的join(2000)方法來等待執行緒A的結束,但最多隻等2s。示例如下:
class ThreadImp implements Runnable{
public void run(){
try{
System.out.println("Begin ThreadImp");
Thread.sleep(5000);
System.out.println("End ThreadImp");
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
public class JoinTest{
public static void main(String[] args){
Thread t = new Thread(new ThreadImp());
t.start();
try{
t.join(1000);//主執行緒等待1s
if(t.isAlive()){
System.out.println("t has not finished");
}else{
System.out.println("t has finished");
}
System.out.println("joinFinish");
}catch(InterruptedExcetion e){
e.printStackTrace();
}
}
}
執行結果:Begin ThreadImp
t has not finished
joinFinish
End ThreadImp