Java多線程面試攻略(一)

一、多線程相關概念

1、線程:①每個線程都有一個程序運行的入口、順序執行序列和程序的出口,但是線程不能夠獨立執行,必須依存在進程中

②線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源,如程序計數器,一組寄存器和線程棧,但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。

③多個線程可以共享同一段代碼。

2、線程棧:線程棧是線程獨有的,保存其運行狀態和局部變量,線程棧在線程開始的時候初始化,每個線程的棧互相獨立。

3、進程:每個進程都有獨立的代碼和數據空間(進程上下文,也叫堆內存),一個進程包含1— N個線程。堆內存在操作系統對進程進行初始化的時候分配,運行過程中也可以向系統申請額外的內存空間,但是記得用完了要還給操作系統,要不然就會發生著名的“內存泄漏”現象。

4、並行:操作系統(也可以說多核CPU)同時執行多個程序,是真正的同時,也就是同一時間。

5、併發:通過cpu調度算法,在用戶看來是同時執行,實際上從cpu調度層面不是真正的同時。

6、同步:Java中的同步指的是通過人爲的控制和調度,保證對於共享資源的多線程訪問是線程安全的,來保證結果的準確。通常在代碼上加入synchronized關鍵字來實現同步。

7、線程安全:經常用來描述一段代碼的狀態。指在併發的情況之下,該代碼經過多線程使用,線程的調度順序不影響執行結果。這個時候使用多線程,我們只需要關注系統的內存,cpu是不是夠用即可。反過來,線程不安全就意味着線程的調度順序會影響最終結果。

8、監視器:java會爲每個object對象分配一個monitor,當一個線程調用一個對象的同步方法時,JVM會檢查該對象的monitor。如果monitor沒有被佔用,那麼這個線程就得到了monitor的佔有權,可以繼續執行該對象的同步方法;如果monitor被其他線程所佔用,那麼該線程將被掛起,直到monitor被釋放。當線程退出同步方法調用時,該線程會釋放monitor,這將允許其他等待的線程獲得monitor以使對同步方法的調用執行下去。

9、守護(後臺)線程:指爲其他線程提供服務的線程,比如JVM的垃圾回收線程就是一個守護線程。守護線程會隨着主線程的結束而結束。

10、死鎖:多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由於線程被無限期地阻塞,因此程序不可能正常終止。

java 死鎖產生的四個必要條件:

  • 1、互斥使用,即當資源被一個線程使用(佔有)時,別的線程不能使用
  • 2、不可搶佔,資源請求者不能強制從資源佔有者手中奪取資源,資源只能由資源佔有者主動釋放。
  • 3、請求和保持,即當資源請求者在請求其他的資源的同時保持對原有資源的佔有。
  • 4、循環等待,即存在一個等待隊列:P1佔有P2的資源,P2佔有P3的資源,P3佔有P1的資源。這樣就形成了一個等待環路。

當上述四個條件都成立的時候,便形成死鎖。當然,死鎖的情況下如果打破上述任何一個條件,便可讓死鎖消失。下面用java代碼來模擬一下死鎖的產生。

解決死鎖問題的方法是:一種是用synchronized,一種是用Lock顯式鎖實現。

二、線程狀態轉換

下面的這個圖非常重要!你如果看懂了這個圖,那麼對於多線程的理解將會更加深刻!

 

1、新建狀態(New):新創建了一個線程對象。

2、就緒狀態(Runnable):線程對象創建後,其他線程調用了該對象的start()方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權。

3、運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。

4、阻塞狀態(Blocked):阻塞狀態是線程因爲某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,纔有機會轉到運行狀態。阻塞的情況分三種:

(一)、等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。(wait會釋放持有的鎖)

(二)、同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池中。

(三)、其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。(注意,sleep是不會釋放持有的鎖)

5、死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。

 

三、線程常用函數(也可以叫線程調度方法)

1、setPriority()和getPriority():Thread類可以設置和獲取線程的優先級,Java線程有優先級,優先級高的線程會獲得較多的運行機會。

Java線程的優先級用整數表示,取值範圍是1~10,Thread類有以下三個靜態常量:

static int MAX_PRIORITY:線程可以具有的最高優先級,取值爲10。

static int MIN_PRIORITY: 線程可以具有的最低優先級,取值爲1。

static int NORM_PRIORITY: 分配給線程的默認優先級,取值爲5,主線程默認的優先級。

線程的優先級有繼承關係,比如A線程中創建了B線程,那麼B將和A具有相同的優先級。

JVM提供了10個線程優先級,但與常見的操作系統都不能很好的映射。如果希望程序能移植到各個操作系統中,應該僅僅使用Thread類有以下三個靜態常量作爲優先級,這樣能保證同樣的優先級採用了同樣的調度方式。

 

2、sleep(long millis)方法:sleep()使當前線程進入停滯狀態(阻塞當前線程),讓出CUP的使用、目的是不讓當前線程獨自霸佔該進程所獲的CPU資源,以留一定時間給其他線程執行的機會。在sleep()休眠時間期滿後,該線程不一定會立即執行,這是因爲其它線程可能正在運行而且沒有被調度爲放棄執行,除非此線程具有更高的優先級。 

共同點: 

  • 他們都是在多線程的環境下,都可以在程序的調用處阻塞指定的毫秒數,並返回。 
  • wait()和sleep()都可以通過interrupt()方法 打斷線程的暫停狀態 ,從而使線程立刻拋出InterruptedException。 

   如果線程A希望立即結束線程B,則可以對線程B對應的Thread實例調用interrupt方法。如果此刻線程B正在wait/sleep /join,則線程B會立刻拋出InterruptedException,在catch() {} 中直接return即可安全地結束線程。 
   需要注意的是,InterruptedException是線程自己從內部拋出的,並不是interrupt()方法拋出的。對某一線程調用 interrupt()時,如果該線程正在執行普通的代碼,那麼該線程根本就不會拋出InterruptedException。但是,一旦該線程進入到 wait()/sleep()/join()後,就會立刻拋出InterruptedException 。


不同點: 

  • Thread類的方法:sleep(),yield()等    ;  Object的方法:wait()和notify()等 
  • 每個對象都有一個鎖來控制同步訪問。Synchronized關鍵字可以和對象的鎖交互,來實現線程的同步。sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他線程可以使用同步控制塊或者方法。 
  •  wait,notify和notifyAll只能在同步控制方法或者同步控制塊裏面使用,而sleep可以在任何地方使用 
  • sleep必須捕獲異常,而wait,notify和notifyAll不需要捕獲異常

所以sleep()和wait()方法的最大區別是:
    sleep()睡眠時,保持對象鎖,仍然佔有該鎖;
    而wait()睡眠時,釋放對象鎖。
但是wait()和sleep()都可以通過interrupt()方法打斷線程的暫停狀態,從而使線程立刻拋出InterruptedException(但不建議使用該方法)。
 

3、wait()方法,notify()方法 :Object類的wait()方法,對象方法Obj.wait()的作用是讓需要Obj鎖的線程等待,直到其他線程調用Obj的 notify() 方法或 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行爲等價於調用 wait(0) 一樣。

Obj.wait()與Obj.notify()必須要與synchronized(Obj)一起使用,也就是說wait和notify必須是對已經被獲取了的Obj鎖進行操作,從語法角度來說就是Obj.wait(),Obj.notify必須在synchronized(Obj){...}語句塊內,否則會在時扔出”java.lang.IllegalMonitorStateException“異常。

從功能上來說wait就是說線程在獲取對象鎖後,主動釋放對象鎖,同時本線程休眠。直到有其它線程調用對象的notify()喚醒該線程,才能繼續讓線程繼續執行。

Obj.notify()就是對某個需要Obj鎖的線程進行喚醒操作。但有一點需要注意的是notify()調用後,並不是馬上就釋放對象鎖的,而是在相應的synchronized(){}語句塊執行結束,自動釋放鎖後,JVM會在需要對應對象鎖的等待隊列的線程中隨機選取一線程,賦予其對象鎖,喚醒線程,繼續執行。

/* 
經典面試題:建立三個線程,A線程打印10次A,B線程打印10次B,C線程打印10次C,要求線程同時運行,交替打印10次ABC。
這個問題用Object的wait(),notify()就可以很方便的解決
*/
 class Thread1 implements  Runnable {

     private String name;
     private Object prev;
     private Object self;

     private Thread1(String name, Object prev, Object self) {
         this.name = name;
         this.prev = prev;
         this.self = self;
     }

     @Override
     public void run() {

         int count = 10;
         while (count > 0) {
             synchronized (prev) {
                 synchronized (self) {
                     System.out.print(name);
                     count--;

                     self.notify();
                 }
                 try {
                     prev.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }

         }
     }

         public static void main(String[] args) throws Exception {
             Object a = new Object();
             Object b = new Object();
             Object c = new Object();
             Thread1 pa = new Thread1("A", c, a);
             Thread1 pb = new Thread1("B", a, b);
             Thread1 pc = new Thread1("C", b, c);


             new Thread(pa).start();
             Thread.sleep(100);  //確保按順序A、B、C執行
             new Thread(pb).start();
             Thread.sleep(100);
             new Thread(pc).start();
             Thread.sleep(100);
         }

     }
/*結果:ABCABCABCABCABCABCABCABCABCABC*/

4、yield() 方法:Thread.yield() 方法將暫停當前正在執行的線程對象,把CPU 佔有權讓給相同或者更高優先級的線程,但是實際中無法保證yield()達到讓步的目的,因爲讓步的線程還有可能被線程調度程序再次選中。

class Thread1 extends Thread {
     public Thread1(String name) {
         super(name);
     }

     @Override
     public void run() {
         for (int i = 1; i <= 5; i++) {
             System.out.println("" + this.getName() + "-----" + i);
             // 當i爲3時,該線程就會把CPU時間讓掉,讓其他或者自己的線程執行(也就是誰先搶到誰執行)
             if (i ==3) {
                 this.yield();
             }
         }

     }
 }
public class thread {

    public static void main(String[] args) {
        Thread1 yt1 = new Thread1("張三");
        Thread1 yt2 = new Thread1("李四");
        yt1.start();
        yt2.start();
    }

}

 

5、join()方法:thread1.join()方法阻塞調用此方法的線程(calling thread),直到線程thread1完成,此線程再繼續;通常用於在main()主線程內,等待其它線程完成再結束main()主線程。簡單點說就是一個線程阻塞另一個線程,等我運行完了你再運行,如果你正好在運行那就先掛起,好好待著,看我運行。

爲什麼要用join()方法?

在很多情況下,主線程生成並起動了子線程,如果子線程裏要進行大量的耗時的運算,主線程往往將於子線程之前結束,但是如果主線程處理完其他的事務後,需要用到子線程的處理結果,也就是主線程需要等待子線程執行完成之後再結束,這個時候就要用到join()方法了。

 class Thread1 extends Thread {
        private String name;
        public Thread1(String name) {
            super(name);
            this.name=name;
        }
        public void run() {
            System.out.println(Thread.currentThread().getName() + " 線程運行開始!");
            for (int i = 0; i < 5; i++) {
                System.out.println("子線程"+name + "運行 : " + i);
                try {
                    sleep((int) Math.random() * 10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " 線程運行結束!");
        }
}

public class thread {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"主線程運行開始!");
        Thread1 mTh1=new Thread1("A");
        Thread1 mTh2=new Thread1("B");
        mTh1.start();
        mTh2.start();
        System.out.println(Thread.currentThread().getName()+ "主線程運行結束!");
     /*   System.out.println(Thread.currentThread().getName()+"主線程運行開始!");
        Thread1 mTh1=new Thread1("A");
        Thread1 mTh2=new Thread1("B");
        mTh1.start();
        mTh2.start();
        try {
            mTh1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            mTh2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+ "主線程運行結束!");*/
    }

}
/*            加join:                             不加join:
        main主線程運行開始!                    main主線程運行開始!
        A 線程運行開始!                        main主線程運行結束!
        B 線程運行開始!                        B 線程運行開始!
        子線程B運行 : 0                        子線程B運行 : 0
        子線程A運行 : 0                        A 線程運行開始!
        子線程B運行 : 1                        子線程B運行 : 1
        子線程A運行 : 1                        子線程A運行 : 1
        子線程A運行 : 2                        子線程B運行 : 2
        子線程A運行 : 3                        子線程A運行 : 2
        子線程B運行 : 2                        子線程B運行 : 3
        子線程A運行 : 4                        子線程A運行 : 3
        子線程B運行 : 3                        子線程B運行 : 4
        A 線程運行結束!                        子線程A運行 : 4
        子線程B運行 : 4                        B 線程運行結束!
        B 線程運行結束!                        A 線程運行結束!
        main主線程運行結束!
        */

 

6、interrupt()方法 :強制中斷某個線程,這種結束方式比較粗暴,如果t線程打開了某個資源還沒來得及關閉,也就是run方法還沒有執行完就強制結束線程,會導致資源無法關閉,所以很少用。

7、currentThread()方法 :Thread.currentThread()可以獲取當前線程的引用,一般都是在沒有線程對象又需要獲得線程信息時通過Thread.currentThread()獲取當前代碼段所在線程的引用。

四、線程同步

1、synchronized關鍵字的作用域有二種: 
1)作用域是某個對象實例內,synchronized aMethod(){}可以防止多個線程同時訪問這個對象的synchronized方法(如果一個對象有多個synchronized方法,只要一個線程訪問了其中的一個synchronized方法,其它線程不能同時訪問這個對象中任何一個synchronized方法)。這時,不同的對象實例的synchronized方法是不相干擾的。也就是說,其它線程照樣可以同時訪問相同類的另一個對象實例中的synchronized方法。
2)作用域是某個類的範圍,synchronized static aStaticMethod{}防止多個線程同時訪問這個類中的synchronized static 方法。它可以對類的所有對象實例起作用。 

2、除了方法前用synchronized關鍵字,synchronized關鍵字還可以用於方法中的某個代碼塊中。

3、synchronized關鍵字是不能繼承的,也就是說,基類的方法synchronized f(){} 在繼承類中並不自動是synchronized f(){},而是變成了f(){}。繼承類需要你顯式的指定它的某個方法爲synchronized方法 。

 

總的說來,synchronized關鍵字可以作爲函數的修飾符,也可修飾函數內的代碼塊,也就是平時說的同步方法和同步代碼塊 。synchronized可以把實例對象或類名(我懷疑類也是個對象,牽扯到了反射)作爲鎖。

在進一步闡述之前,我們需要明確幾點:

A.無論synchronized關鍵字加在方法上還是對象上,它取得的鎖都是對象,而不是把一段代碼或函數當作鎖――而且同步方法很可能還會被其他線程的對象訪問。

B.每個對象只有一個鎖(lock)與之相關聯。

C.實現同步是要很大的系統開銷作爲代價的,甚至可能造成死鎖,所以儘量避免無謂的同步控制。

 

接着來討論synchronized用到不同地方對代碼產生的影響:

 

假設P1、P2是同一個類的不同對象,這個類中定義了以下幾種情況的同步塊或同步方法,P1、P2就都可以調用它們。

 

1.  把synchronized當作函數修飾符時,示例代碼如下:

Public synchronized void methodAAA()     {      //…..    }

這也就是同步方法,那這時synchronized鎖定的是哪個對象呢?它鎖定的是調用這個同步方法對象。也就是說,當一個對象P1在不同的線程中執行這個同步方法時,它們之間會形成互斥,達到同步的效果。但是這個對象所屬的Class所產生的另一對象P2卻可以任意調用這個被加了synchronized關鍵字的方法。

上邊的示例代碼等同於如下代碼:

public void methodAAA()   {

synchronized (this)   {       //…..       }

}

這裏的this指的是什麼呢?它指的就是調用這個方法的對象,假設是P1。可見同步方法實質是將synchronized作用於實例。那個拿到了P1對象鎖的線程,纔可以調用P1的同步方法,而對P2而言,P1這個鎖與它毫不相干,程序也可能在這種情形下襬脫同步機制的控制,造成數據混亂!!

2.同步塊,示例代碼如下:

public void method3(SomeObject so)  {  

synchronized(so){    //…..    }

}

這時,鎖就是so這個對象,誰拿到這個鎖誰就可以運行它所控制的那段代碼。當有一個明確的對象作爲鎖時,就可以這樣寫程序,但當沒有明確的對象作爲鎖,只是想讓一段代碼同步時,可以創建一個特殊的對象來充當鎖:

class Foo implements Runnable  {

    private byte[] lock = new byte[0];  // 特殊的實例變量

    Public void methodA(){

       synchronized(lock) { //… }

}

//…..

}

注:零長度的byte數組對象創建起來將比任何對象都經濟――查看編譯後的字節碼:生成零長度的byte[]對象只需3條操作碼,而Object lock = new Object()則需要7行操作碼。

3.將synchronized作用於static 函數,示例代碼如下:

Class Foo{

public synchronized static void methodAAA()   {    //這是同步靜態函數    }

public void methodBBB(){     synchronized(Foo.class) { //… }        }

}

   代碼中的methodBBB()方法是把class作爲鎖的情況,它和同步的static函數產生的效果是一樣的,取得的鎖很特別,是當前調用這個方法的對象所屬的類(Class,而不再是由這個Class產生的某個具體對象了)。

 

總結:

1、線程同步的目的是爲了保護多個線程反問一個資源時對資源的破壞。

2、線程同步方法是通過鎖來實現,每個對象都有切僅有一個鎖,這個鎖與一個特定的對象關聯,線程一旦獲取了對象鎖,其他訪問該對象的線程就無法再訪問該對象的其他非同步方法。

3、對於靜態同步方法,鎖是針對這個類的,鎖對象是該類的Class對象。靜態和非靜態方法的鎖互不干預。一個線程獲得鎖,當在一個同步方法中訪問另外對象上的同步方法時,會獲取這兩個對象鎖。

4、對於同步,要時刻清醒在哪個對象上同步,這是關鍵。

5、編寫線程安全的類,需要時刻注意對多個線程競爭訪問資源的邏輯和安全做出正確的判斷,對“原子”操作做出分析,並保證原子操作期間別的線程無法訪問競爭資源。

6、當多個線程等待一個對象鎖時,沒有獲取到鎖的線程將發生阻塞。

7、死鎖是線程間相互等待鎖鎖造成的,在實際中發生的概率非常的小。真讓你寫個死鎖程序,不一定好使,呵呵。但是,一旦程序發生死鎖,程序將死掉。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章