【Java面試】Java 多線程入門學習

  操作系統的發展使得多個程序能夠同時運行,程序在各自的進程(processes)中運行,相互分離,各自獨立執行,由操作系統來分配資源,如內存、文件句柄、安全證書等。
  
  進程是資源分配的最小單位,每個進程都有獨立的代碼和數據空間(進程上下文),且都可以包含1~n個線程,進程間的切換會有較大的開銷。如果需要的話,進程會通過一些原始的機制相互通信:Socket、信號處理(signal handlers)、共享內存(shared memory),信號量(semaphores),和文件;
  
  線程有些時候被稱爲輕量級進程(lightweight processes),並且大多數現代操作系統把線程作爲時序調度的基本單元,而不是進程。線程允許程序控制流的多重分支同時存在於一個進程,它們共享進程範圍內的資源,如內存和文件句柄,但是每一個線程有其自己的程序計數器(program counter),棧和本地變量。線程也爲多處理器系統中並行地使用硬件提供了一個自然而然的分解,同一程序內的多個線程可以在多CPU的情況下同時調度;


兩種實現多線程的方式

  Java中實現多線程,一般有兩種方式,一種是繼承Thread類,另一種是實現Runnable接口。(有些博客中看到,應該有三種,還有一種是實現Callable接口,並與Future、線程池結合使用);
  
  第一種:擴展java.lang.Thread類
  繼承Thread類的方法是比較常用的一種,如果說只是想起一個線程,沒有什麼其它特殊的要求,那麼可以使用Thread。(推薦使用Runnable,後頭會說使用Runnable的優勢有什麼)。下面來看一個簡單的繼承Thread類的實例:

public class ExtendThreadTest {

    public static void main(String[] args) {
        MyThread thread1 = new MyThread("A");
        thread1.start();
        MyThread thread2 = new MyThread("B");
        thread2.start();
    }

    static class MyThread extends Thread {

        private String name;

        public MyThread(String name) {
            super();
            this.name = name;
        }

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(name + " Thread:" + i);
                try {
                    Thread.sleep(new Random().nextInt(500));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            super.run();
        }
    }
}

運行示例:

這裏寫圖片描述

  程序啓動運行main函數的時候,Java虛擬機(JVM)啓動一個進程,主線程main在main()調用時候被創建。隨着調用main()函數中兩個對象的start()方法,這兩個線程也啓動了,這樣,整個應用就在多線程下運行。從程序運行的結果可以發現,多線程程序是亂序執行。所有的多線程代碼執行順序都是不確定的,每次執行的結果都是隨機的,可以執行試一下;

注意: start()方法的調用後並不是立即執行多線程代碼,而是使得該線程變爲可運行態(Runnable),什麼時候運行是由操作系統決定的。
另外: start()方法不可重複調用,不然會出現java.lang.IllegalThreadStateException異常;


  第二種:實現java.lang.Runnable接口

  採用實現Runnable接口,只需重寫run()方法,下面來看一個簡單的實現Runnable接口的實例:

public class ImplementRunnable implements Runnable {

    private String name;

    public ImplementRunnable(String name) {
        super();
        this.name = name;
    }

    public static void main(String[] args) {
        ImplementRunnable thread1 = new ImplementRunnable("A");
        new Thread(thread1).start();
        ImplementRunnable thread2 = new ImplementRunnable("B");
        new Thread(thread2).start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + " Thread:" + i);
            try {
                Thread.sleep(new Random().nextInt(500));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

  運行結果同第一個示例,ImplementRunnable 類通過實現Runnable接口,使得該類有了多線程類的特徵,所有的多線程代碼都在run()方法裏面。Thread類實際上也是實現了Runnable接口的類。
  在啓動多線程的時候,需要先通過Thread類的構造方法Thread(Runnable target) 構造出對象,然後調用Thread對象的start()方法來運行多線程代碼。
  實際上所有的多線程代碼都是通過運行Thread的start()方法來運行的。因此,不管是擴展Thread類還是實現Runnable接口來實現多線程,最終還是通過Thread對象的API來控制線程的,熟悉Thread類的API是進行多線程編程的基礎。
  
總結:實現Runnable接口比繼承Thread類所具有的優勢:
1. 適合多個有相同的程序代碼的線程去處理同一個資源;
2. 可以避免Java中的單繼承的限制;
3. 增加程序的健壯性,代碼可以被多個線程共享,代碼和數據獨立;
4. 線程池只能放入實現Runable或callable接口的線程,不能直接放入繼承Thread類的線程;

注意: main()方法其實也是一個線程,在Java中所有的線程都是同時啓動的,至於什麼時候,哪個先執行,完全看誰先拿到CPU的資源。


線程狀態轉換

  這裏寫圖片描述
  
1. 新建狀態(New):新創建了一個線程對象,還沒有調用start()方法時,線程處於此狀態;
2. 就緒狀態(Runnable):線程對象創建後,其它線程(如主線程中)調用了該對象的start()方法後,線程進入就緒狀態,該狀態的線程位於可運行線程池中,變的可運行,等待Java運行時系統的線程調度程序(thread scheduler)來獲取CPU的使用權;
3. 運行狀態(Running):就緒狀態的線程獲取了CPU後,執行程序代碼(run()方法中的代碼);
4. 阻塞狀態(Blocked):線程因爲某種原因放棄CPU使用權,暫時停止運行,直到線程進入就緒狀態,纔有機會轉到運行狀態;阻塞的情況分三種:
  (1)、等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。(wait會釋放持有的鎖);
  (2)、同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池中;
  (3)、其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程設置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。(sleep不會釋放持有的鎖);
5. 死亡狀態(Dead):線程執行結束或者因異常退出了run()方法,線程進入死亡狀態,結束生命週期;  


線程調度

線程優先級: Java線程的優先級用整數表示,取值範圍爲1~10,優先級高的線程會獲得更多的機會獲取CPU資源,Thread類有以下三個靜態常量:
1. static int MAX_PRIORITY:線程可以具有的最高優先級,取值爲10;
2. static int MIN_PRIORITY:線程可以具有的最低優先級,取值爲1;
3. static int NORM_PRIORITY:分配給線程的默認優先級,取值爲5;
Thread類的setPriority()和getPriority()方法分別用來設置和獲取線程的優先級。

  每個線程都有默認的優先級。主線程的默認優先級爲Thread.NORM_PRIORITY。線程的優先級有繼承關係,比如A線程中創建了B線程,那麼B將和A具有相同的優先級。JVM提供了10個線程優先級,但與常見的操作系統都不能很好的映射。如果希望程序能移植到各個操作系統中,應該僅僅使用Thread類的三個靜態常量作爲優先級,這樣能保證同樣的優先級採用了同樣的調度方式。

線程睡眠: Thread.sleep(long millis)方法,使線程轉到阻塞狀態。millis參數設定睡眠的時間長短,以毫秒爲單位,當睡眠結束後,就轉爲就緒狀態,sleep()平臺移植性好。

線程等待: Object類中的wait()方法,使得當前的線程等待,直到其他線程調用此對象的 notify() 或 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行爲等價於調用 wait(o) 一樣。

線程讓步: Thread.yield() 方法,暫停當前正在執行的線程對象,把執行機會讓給相同或者更高優先級的線程。

線程加入: join()方法,等待其他線程終止。在當前線程中調用另一個線程的join()方法,則當前線程轉入阻塞狀態,直到另一個線程運行結束,當前線程再由阻塞轉爲就緒狀態。

線程喚醒: Object類中的notify()方法,喚醒在此對象監視器上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,並在對實現做出決定時發生。線程通過調用其中一個 wait ()方法,在對象的監視器上等待。 直到當前的線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程。被喚醒的線程將以常規方式與在該對象上主動同步的其他所有線程進行競爭;類似的方法還有一個notifyAll(),喚醒在此對象監視器上等待的所有線程。


常用函數

sleep(long millis): 在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行),讓出CUP的使用、目的是不讓當前線程獨自霸佔該進程所獲的CPU資源,以留一定時間給其他線程執行的機會;

  sleep()是Thread類的Static(靜態)方法;因此他不能改變對象的機鎖,所以當在一個Synchronized塊中調用Sleep()方法是,線程雖然休眠了,但是對象的機鎖並木有被釋放,其他線程無法訪問這個對象(即使睡着也持有對象鎖)。
  在sleep()休眠時間期滿後,該線程不一定會立即執行,這是因爲其它線程可能正在運行而且沒有被調度爲放棄執行,除非此線程具有更高的優先級。

yield(): 暫停當前正在執行的線程對象,並執行其他線程。

  yield()應該做的是讓當前運行線程回到可運行狀態,以允許具有相同優先級的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因爲讓步的線程還有可能被線程調度程序再次選中。
  
  結論: yield()從未導致線程轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致線程從運行狀態轉到可運行狀態,但有可能沒有效果。
  
  sleep()和yield()的區別 
  
  sleep()和yield()的區別:sleep()使當前線程進入停滯狀態,所以執行sleep()的線程在指定的時間內肯定不會被執行;yield()只是使當前線程重新回到可執行狀態,所以執行yield()的線程有可能在進入到可執行狀態後馬上又被執行。
  sleep 方法使當前運行中的線程睡眼一段時間,進入不可運行狀態,這段時間的長短是由程序設定的,yield 方法使當前線程讓出 CPU 佔有權,但讓出的時間是不可設定的。實際上,yield()方法對應瞭如下操作:先檢測當前是否有相同優先級的線程處於可運行狀態,如有,則把 CPU 的佔有權交給此線程,否則,繼續運行原來的線程。所以yield()方法稱爲“退讓”,它把運行機會讓給了同等優先級的其他線程。
  另外,sleep()方法允許較低優先級的線程獲得運行機會,但 yield()方法執行時,當前線程仍處在可運行狀態,所以,較低優先級的線程不可能獲得CPU佔有權。在一個運行系統中,如果較高優先級的線程沒有調用 sleep 方法,又沒有受到 I\O 阻塞,那麼,較低優先級線程只能等待所有較高優先級的線程運行結束,纔有機會運行。

join(): 等待該線程終止。這裏需要理解的就是該線程是指的主線程等待子線程的終止。也就是在子線程調用了join()方法後面的代碼,只有等到子線程結束了才能執行。

  爲什麼要用join()方法?

  在很多情況下,主線程生成並起動了子線程,如果子線程裏要進行大量耗時的運算或網絡請求操作,主線程往往將於子線程之前結束,但是如果主線程處理完其他的事務後,需要用到子線程的處理結果,也就是主線程需要等待子線程執行完成之後再結束,這個時候就要用到join()方法了。可以寫個小程序試一試加不加join()方法的區別。

interrupt(): 不要以爲它是中斷某個線程!它只是向線程發送一箇中斷信號,讓線程在無限等待時(如死鎖時)能拋出,從而結束線程,但是如果你吃掉了這個異常,那麼這個線程還是不會中斷的!

wait(): 該方法的作用是將當前運行的線程掛起(即讓其進入阻塞狀態),直到其他線程調用notify()或notifyAll()方法來喚醒該線程。
  
  注意: wait()方法的使用必須在同步的範圍內,Obj.wait(),與Obj.notify()必須要與synchronized(Obj)一起使用,也就是wait,與notify是針對已經獲取了Obj鎖進行操作,從語法角度來說就是Obj.wait(),Obj.notify必須在synchronized(Obj){…}語句塊內,並且必須先調用notify()後調用wait(),否則就會拋出IllegalMonitorStateException異常,wait()方法的作用就是阻塞當前線程等待notify/notifyAll方法,或等待超時後自動喚醒。但有一點需要注意,notify()調用後,並不是馬上就釋放對象鎖的,而是在相應的synchronized(Obj){…}語句塊執行結束,自動釋放鎖。
  
  下面是Object.wait(),Object.notify()的應用小示例(多線程交替輸出奇偶數問題),爲了控制線程執行的順序,那麼就必須要確定喚醒、等待的順序。如下:

public class SynchronizedThread {
    public static void main(String args[]) {
        Num num = new Num(0);
        new Thread(new ThEdd(num)).start();
        new Thread(new ThOdd(num)).start();
    }
}

class Num {
    public int num;
    public Num(int num) {
        this.num = num;
    }
    // 打印奇數,此方法加鎖
    public synchronized void printOdd() {
        System.out.println("OddNum :" + (num++));
        try {
            // 喚醒下一個等待線程
            this.notifyAll();
            // 釋放方法自身對象鎖
            this.wait();
            Thread.sleep(500);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    // 打印偶數,此方法加鎖
    public synchronized void printEdd() {
        System.out.println("EvenNum:" + (num++));
        try {
            this.notifyAll();
            this.wait();
            Thread.sleep(500);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//打印 奇數的線程
class ThOdd implements Runnable {
    private Num num;
    public ThOdd(Num num) {
        this.num = num;
    }
    public void run() {
        while (true) {
            num.printOdd();
        }
    }
}
// 打印偶數的線程
class ThEdd implements Runnable {
    private Num num;
    public ThEdd(Num num) {
        this.num = num;
    }
    public void run() {
        while (true) {
            num.printEdd();
        }
    }
}

運行示例:

這裏寫圖片描述
  
  wait()和sleep()的區別:
  兩者最簡單的區別是,wait()方法是Object的方法,依賴於同步,而sleep()方法是Thread類的方法,可以直接調用,更深層次的區別在於sleep()方法只是暫時讓出CPU的執行權,並不釋放鎖,而wait()方法則需要釋放鎖。下面示例所示:

public class SleepAndWait {

    public synchronized void sleepMethod() {
        System.out.println("Sleep start-----");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Sleep end-----");
    }

    public synchronized void waitMethod() {
        System.out.println("Wait start-----");
        synchronized (this) {
            try {
                wait(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Wait end-----");
    }

    public static void main(String[] args) {
        final SleepAndWait test1 = new SleepAndWait();

        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    test1.sleepMethod();
                }
            }).start();
        }

        try {
            Thread.sleep(5000);// 暫停五秒,等上面程序執行完成
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-----分割線-----");

        final SleepAndWait test2 = new SleepAndWait();

        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {

                @Override

                public void run() {
                    test2.waitMethod();
                }
            }).start();
        }
    }
}

運行示例:

這裏寫圖片描述

  結果的區別很明顯,通過sleep()方法實現的暫停,程序是順序進入同步塊的,只有當上一個線程執行完成的時候,下一個線程才能進入同步方法,sleep()暫停期間一直持有monitor對象鎖,其他線程是不能進入的。而wait方法則不同,當調用wait方法後,當前線程會釋放持有的monitor對象鎖,因此,其他線程還可以進入到同步方法,線程被喚醒後,需要競爭鎖,獲取到鎖之後再繼續執行。


常見線程名詞

  主線程: JVM調用程序main()所產生的線程。
  
  當前線程: 這個是容易混淆的概念。一般指通過Thread.currentThread()來獲取的進程。
  
  後臺線程: 指爲其他線程提供服務的線程,也稱爲守護線程。JVM的垃圾回收線程就是一個後臺線程。可以通過Thread類的isDaemon()和setDaemon()方法來判斷和設置一個線程是否爲後臺線程。
  
  前臺線程: 是指接受後臺線程服務的線程,其實前臺與後臺線程是聯繫在一起,就像傀儡和幕後操縱者一樣的關係。傀儡是前臺線程、幕後操縱者是後臺線程。由前臺線程創建的線程默認也是前臺線程。


線程同步

  首先,我們要知道爲什麼要線程同步?
  
  因爲當我們有多個線程要同時訪問一個變量或對象時,如果這些線程中既有讀又有寫操作時,就會導致變量值或對象的狀態出現混亂,從而導致程序異常。
  
  線程同步的使用方法有:
  1、同步方法: 指使用synchronized關鍵字修飾的方法。 由於java的每個對象都有一個內置鎖,當用此關鍵字修飾方法時,內置鎖會保護整個方法。在調用該方法前,需要獲得內置鎖,否則就處於阻塞狀態。如果一個對象有多個synchronized方法,只要一個線程訪問了其中的一個synchronized方法,其它線程不能同時訪問這個對象中任何一個synchronized方法。此時,不同的對象實例的synchronized方法是不相干擾的。也就是說,其它線程照樣可以同時訪問相同類的另一個對象實例中的synchronized方法。

    // synchronized關鍵字修飾的方法,它鎖定的是調用這個同步方法的對象
    public  synchronized void method(Parm parm){  
        ...... 
    }  

  另外,synchronized還可用與同步static方法,如下:

    public class Test{
        // 同步的static方法
        public synchronized static void methodA() {  
            // ......
        }
        public void methodB() {
            synchronized(Test.class)   //  class literal(類名稱字面常量)  
        }
    }  

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

  可以推斷:如果一個類中定義了一個synchronized的static方法A,也定義了一個synchronized 的instance方法B,那麼這個類的同一對象Obj在多線程中分別訪問A和B兩個方法時,不會構成同步,因爲它們的鎖是不一樣。A方法的鎖是Obj這個對象,而B的鎖是Obj所屬的那個Class類。

  2、同步代碼塊 :指使用synchronized關鍵字修飾的語句塊(synchronized修飾的大括號內的語句塊)。被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步。

    public void method(Parm parm){  
        // synchronized關鍵字修飾的代碼塊,它鎖定的是parm這個對象,只有拿到這個鎖才能運行此代碼塊
        synchronized(parm){
             ...... 
        }
    }

  注: 同步是一種高開銷的操作,因此應該儘量減少同步的內容和無畏的同步控制。通常沒有必要同步整個方法,使用synchronized代碼塊同步關鍵代碼即可。
  
  同步方法和同步語句塊,如果再細的分類的話,synchronized可作用於instance變量、object reference(對象引用)、static函數和class literals(類名稱字面常量)身上。

  3、使用特殊域變量(Volatile)實現線程同步
  
  volatile不能保證原子操作,因此volatile不能代替synchronized。此外volatile會組織編譯器對代碼優化,因此能不使用它就不適用它。它的原理是每次線程要訪問volatile修飾的變量時都是從內存中讀取,而不是從緩存當中讀取,因此每個線程訪問到的變量值都是一樣的,這樣就保證了同步。
  
  a、volatile關鍵字爲域變量的訪問提供了一種免鎖機制。
  b、使用volatile修飾域相當於告訴虛擬機該域可能會被其他線程更新。
  c、因此每次使用該域就要重新計算,而不是使用寄存器中的值。
  d、volatile不會提供任何原子操作,它也不能用來修飾final類型的變量。
  
  注意:
  1、編寫線程安全的類,需要時刻注意對多個線程競爭訪問資源的邏輯和安全做出正確的判斷,對“原子”操作做出分析,並保證原子操作期間別的線程無法訪問競爭資源。
  
  2、線程同步方法是通過鎖來實現,每個對象都有切僅有一個鎖,這個鎖與一個特定的對象關聯,線程一旦獲取了對象鎖,其他訪問該對象的線程就無法再訪問該對象的其他非同步方法。
  

線程數據傳遞

  在同步開發模式下,當我們調用一個函數時,通過這個函數的參數將數據傳入,並通過這個函數的返回值來返回最終的計算結果。但在多線程的異步開發模式下,數據的傳遞和返回與同步開發模式有很大的區別。由於線程的運行和結束是不可預料的,因此,在傳遞和返回數據時就無法象函數一樣通過函數參數和return語句來返回數據。
  
  1、通過構造方法傳遞數據
  
  在創建線程時,必須要建立一個Thread類的或其子類的實例。因此,我們不難想到在調用start方法之前通過線程類的構造方法將數據傳入線程。並將傳入的數據使用類變量保存起來,以便線程使用(其實就是在run方法中使用)。
  
  2、通過變量和方法傳遞數據
  
  向對象中傳入數據一般有兩次機會,第一次機會是在建立對象時通過構造方法將數據傳入,另外一次機會就是在類中定義一系列的public的方法或變量(也可稱之爲字段)。然後在建立完對象後,通過對象實例逐個賦值。上述兩種都比較好理解,不在贅述。
  
  3、通過回調函數傳遞數據
  
  上面討論的兩種向線程中傳遞數據的方法是最常用的。但這兩種方法都是main方法中主動將數據傳入線程類的。這對於線程來說,是被動接收這些數據的。然而,在有些應用中需要在線程運行的過程中動態地獲取數據,如在下面代碼的run方法中產生了3個隨機數,然後通過Work類的process方法求這三個隨機數的和,並通過Data類的value將結果返回。  

public class CallBackThread extends Thread {
    private Work work;

    public CallBackThread(Work work) {
        this.work = work;
    }

    public void run() {
        java.util.Random random = new java.util.Random();
        Data data = new Data();
        int n1 = random.nextInt(1000);
        int n2 = random.nextInt(2000);
        int n3 = random.nextInt(3000);
        int[] num = { n1, n2, n3 };
        work.process(data, num); // 使用回調函數
        System.out.println(String.valueOf(n1) + " + " + 
                String.valueOf(n2) + " + " + String.valueOf(n3)
                + " = " + data.value);
    }

    public static void main(String[] args) {
        Thread thread = new CallBackThread(new Work());
        thread.start();
    }
}

class Data {
    public int value = 0;
}

class Work {
    public void process(Data data, int[] num) {
        for (int n : num) {
            data.value += n;
        }
    }
}

運行示例:

這裏寫圖片描述

參考文獻:http://blog.csdn.net/evankaka/article/details/44153709

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