1. 程式人生 > >Java面試——多執行緒

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