Java基礎知識復習筆記(3)--線程基礎

一、線程概念

1. 操作系統中的線程
現在的操作系統是多任務操作系統,多線程是實現多任務的一種方式,在操作系統中,每一個進程都有操作系統分配給它的獨立內存空間,線程是進程中的一個執行流程,一個進程中可以啟動多個線程。線程總是屬于某一個進程,進程中的多線程共享進程的內存。
2. Java中的線程
在Java中,線程指兩件不同的事情:一是java.lang.Thread類的一個實例,二是線程的執行。

  • 使用java.lang.Thread類或者java.lang.Runnable接口編寫代碼來定義、實例化和啟動新線程。
  • 一個Thread類的實例只是一個對象,像Java中的任何其他對象一樣,具有變量和方法,生死與堆上。
  • Java中每個線程都有一個自己的調用棧,即使不在程序中創建任何新線程,線程也在后臺運行著。一旦創建一個新的線程,就產生一個新的調用棧。

線程棧:
Java為了實現平臺無關性, 必須解決不同操作系統中進程,線程的差異,因此Java建立了一套自己的進程與線程機制。 這套機制與windows系統的頗為相似,但是底層實現確實根據不同平臺的機制進行實現。
線程棧存儲的信息是指某時刻線程中方法調度的信息,當前調用的方法總是位于棧頂。 當某個方法被調用時,此方法的相關信息壓入棧頂。

  • 一個Java應用總是從main()方法開始運行,main()方法運行在一個線程內,它被稱為主線程。
  • 線程總體分兩類:用戶線程和守候線程。當所有用戶線程執行完畢的時候,JVM自動關閉。但是守候線程卻不獨立于JVM,守候線程一般是由操作系統或者用戶自己創建的。

守護線程的作用是為其他線程的運行提供服務,比如說GC線程。其實用戶線程和守護線程本質上來說去沒啥區別的,唯一的區別之處就在虛擬機的離開:如果用戶線程全部撤離,那么守護線程也就沒啥線程好服務的了,所以虛擬機也就退出了。

二、Java線程的創建和啟動

1. 線程的創建
Java創建線程有兩種方法,
第一種:繼承java.lang.Thread類,重寫run方法;
第二種,實現java.lang.Runnable接口,實現run方法。

兩種生成線程對象的區別:
1.兩種方法均需執行線程的start方法為線程分配必須的系統資源、調度線程運行并執行線程的run方法。
2.在具體應用中,采用哪種方法來構造線程體要視情況而定。通常,當一個線程已繼承了另一個類時,就應該用第二種方法來構造,即實現Runnable接口。

2. 線程的實例化
第一種方法的創建的線程,直接new該線程類即可。

//Thread的構造函數
public Thread( );
public Thread(Runnable target);
public Thread(String name);
public Thread(Runnable target, String name);
public Thread(ThreadGroup group, Runnable target);
public Thread(ThreadGroup group, String name);
public Thread(ThreadGroup group, Runnable target, String name);
public Thread(ThreadGroup group, Runnable target, String name, long stackSize);

第二種方法創建的線程,需要調用Thread的構造方法進行實例化。

//實現runnable接口的類,調用Thread的構造方法進行實例化
Thread(Runnable target) 
Thread(Runnable target, String name) 
Thread(ThreadGroup group, Runnable target) 
Thread(ThreadGroup group, Runnable target, String name) 
Thread(ThreadGroup group, Runnable target, String name, long stackSize)

3. 啟動線程

  • 在線程的Thread對象上調用start()方法來啟動線程,可不是run()喲。
  • run()方法沒有任何特別之處。它只是新線程知道調用的方法名稱(和簽名)。因此,在Runnable上或者Thread上調用run方法是合法的,但并不啟動新的線程。
  • 在調用start()方法之前:線程處于新狀態中,新狀態指有一個Thread對象,但還沒有一個真正的線程。
  • 而在調用start()方法后,才會啟動執行線程,該線程從新狀態變為可運行狀態,當線程獲得執行機會后,再運行run()方法。

4. 創建線程例子
通過繼承Thread創建線程

package tym.ThreadBase.create;
/** *Created by TyiMan on 2016/5/14. */
public class MyThread extends Thread {   
  public MyThread() {       
    super();    
  }    
  public MyThread(String name) {       
    super(name);    
  }    
  public void run() {        
    for (int i = 0; i < 3; i++)                    
      System.out.println(Thread.currentThread().getName() + " " + i);    
  }
}

通過實現Runnable接口創建線程

package tym.ThreadBase.create;
/** * Created by TyiMan on 2016/5/14. */
public class MyRunnable implements Runnable {    
  @Override    
  public void run() {        
    for(int i = 0;i<3;i++)        
      System.out.println(Thread.currentThread().getName()+" "+i);     
  }
}

主函數實例化線程并且運行

package tym.ThreadBase.create;
/** * Created by TyiMan on 2016/5/14. */
public class Main {    
  public static void main(String[]args){       
    Thread thread1 = new MyThread();        
    Thread thread2 = new Thread(new MyRunnable());        
    Thread thread3 = new MyThread("MyThread");        
    Thread thread4 = new Thread(new MyRunnable(),"MyRunnable");       

    thread1.start();        
    thread2.start();       
    thread3.start();        
    thread4.start();    
  }
}

執行結果:

//執行結果是按照順序的,如果循環次數更多,他們的輸出就會這么有序了
Thread-0 0
Thread-0 1
Thread-0 2
Thread-1 0
Thread-1 1
Thread-1 2
MyThread 0
MyThread 1
MyThread 2
MyRunnable 0
MyRunnable 1
MyRunnable 2

一些常見問題
1、線程的名字,名字有兩個來源,一個是虛擬機自己給的名字,一個是你自己的定的名字(構造函數傳線程名字或者setName方法)。在沒有指定線程名字的情況下,虛擬機總會為線程指定名字,并且主線程的名字總是main,非主線程的名字Thread-number(該number將是自動增加的,并被所有的Thread對象所共享,因為它是static的成員變量)。
2、獲取當前線程的對象的方法是:Thread.currentThread()
3、想成的運行,只能保證:每個線程都將啟動,每個線程都將運行直到完成。一系列線程以某種順序啟動并不意味著將按該順序執行。對于任何一組啟動的線程來說,調度程序不能保證其執行次序,持續時間也無法保證。
4、當線程目標run()方法結束時該線程完成。
5、一旦線程啟動,它就永遠不能再重新啟動。只有一個新的線程可以被啟動,并且只能一次。一個可運行的線程或死線程可以被重新啟動。
6、線程的調度是JVM的一部分,在一個CPU的機器上上,實際上一次只能運行一個線程。一次只有一個線程棧執行。JVM線程調度程序決定實際運行哪個處于可運行狀態的線程。眾多可運行線程中的某一個會被選中做為當前線程。可運行線程被選擇運行的順序是沒有保障的。

三、線程的生命周期

與人有生老病死一樣,線程要經歷新建、就緒、運行、死亡和阻塞這5種不同的狀態。

線程的生命周期

新建(new Thread)
當創建Thread類的一個實例(對象)時,此線程進入新建狀態(未被啟動)。例如:Thread t1=new Thread();

就緒(runnable)
線程已經被啟動,正在等待被分配給CPU時間片,也就是說此時線程正在就緒隊列中排隊等候得到CPU資源。例如:t1.start();

運行(running)
線程獲得CPU資源正在執行任務(調用run()方法),此時除非此線程自動放棄CPU資源或者有優先級更高的線程進入,線程將一直運行到結束。

死亡(dead)
當線程執行完畢或被其它線程殺死,線程就進入死亡狀態,這時線程不可能再進入就緒狀態等待執行。
自然終止:正常運行run()方法后終止
異常終止:調用stop()方法讓一個線程終止運行

阻塞(blocked)
由于某種原因導致正在運行的線程讓出CPU并暫停自己的執行,即進入阻塞狀態。
正在睡眠:sleep(long t) 方法可使線程進入睡眠方式。一個睡眠著的線程在指定的時間過去可進入就緒狀態。
正在等待:調用wait()方法。(調用notify()方法回到就緒狀態)
被另一個線程所阻塞:調用suspend()方法。(調用resume()方法恢復)

下面給出了Thread類中和各個狀態相關的方法。

// 開始線程
public void start( );
public void run( );
// 掛起和喚醒線程
public void resume( );     // 不建議使用
public void suspend( );    // 不建議使用
public static void sleep(long millis);
public static void sleep(long millis, int nanos);
public static void yied() //可以對當前線程進行臨時暫停(讓線程將資源釋放出來)
// 終止線程
public void stop( );       // 不建議使用
public void interrupt( );
// 得到線程狀態
public boolean isAlive( );
public boolean isInterrupted( );
public static boolean interrupted( );
//join方法讓線程加入執行,執行某一線程join方法的線程會被凍結,
//等待某一線程執行結束,該線程才會恢復到可運行狀態
public void join( ) throws InterruptedException;

四、線程的同步與鎖

線程的同步是為了防止多個線程訪問一個數據對象時,對數據造成的破壞。
Java線程鎖的原理

  • Java中每個對象都有一個內置鎖,當程序運行到非靜態的synchronized同步方法上時,自動獲得與正在執行代碼類的當前實例(this實例)有關的鎖。
  • 獲得一個對象的鎖也稱為獲取鎖、鎖定對象、在對象上鎖定或在對象上同步。當程序運行到synchronized同步方法或代碼塊時該對象鎖才起作用。
  • 一個對象只有一個鎖。所以,如果一個線程獲得該鎖,就沒有其他線程可以獲得鎖,直到第一個線程釋放(或返回)鎖。這也意味著任何其他線程都不能進入該對象上的synchronized方法或代碼塊,直到該鎖被釋放。
    *釋放鎖是指持鎖線程退出了synchronized同步方法或代碼塊。

Java線程同步的synchronized關鍵字的使用

關于同步與鎖的要點:
1)只能同步方法,而不能同步變量和類;
2)每個對象只有一個鎖;當提到同步時,應該清楚在什么上同步?也就是說,在哪個對象上同步?
3)不必同步類中所有的方法,類可以同時擁有同步和非同步方法。
4)如果兩個線程要執行一個類中的synchronized方法,并且兩個線程使用相同的實例來調用方法,那么一次只能有一個線程能夠執行方法,另一個需要等待,直到鎖被釋放。也就是說:如果一個線程在對象上獲得一個鎖,就沒有任何其他線程可以進入(該對象的)類中的任何一個同步方法。
5)如果線程擁有同步和非同步方法,則非同步方法可以被多個線程自由訪問而不受鎖的限制。
6)線程睡眠時,它所持的任何鎖都不會釋放。
7)線程可以獲得多個鎖。比如,在一個對象的同步方法里面調用另外一個對象的同步方法,則獲取了兩個對象的同步鎖。
8)同步損害并發性,應該盡可能縮小同步范圍。同步不但可以同步整個方法,還可以同步方法中一部分代碼塊。
9)在使用同步代碼塊時候,應該指定在哪個對象上同步,也就是說要獲取哪個對象的鎖。要保證多個線程的同步,被鎖定的對象,在它們之間是共享的(就是多個線程使用的同一個對象的鎖)

(1)同步方法
使用synchronized關鍵字修飾方法。 當用此關鍵字修飾方法時,內置鎖會保護整個方法。在調用該方法前,需要獲得內置鎖,否則就處于阻塞狀態。

public class TestSynClass extends Thread{{

 public synchronized void synMethod(){
  //some codes
 }
}

(2)同步代碼塊
使用synchronized關鍵字修飾代碼語句塊。 被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步。使用synchronized關鍵字將需要互斥的代碼包含起來,并上一把鎖。并且鎖定的對象必須是多個線程之間共享的對象。(如下面實例中第三個就是無效的同步代碼塊)

public class TestSynClass extends Thread{
 private Object object = new Object();

 public void synThisClass(){
  synchronized(this){
   //some code
  }
 }
 
 public void synOtherObject(){
  synchronized(object){
   //some code
  }
 }

  public void synDifferentObject(){
    //這個方法是不能實現同步的,因為每次運行都會生成一個新的Object對象
    //不同調用者調用的是不同對象
    synchronized(new Object){
      //some code
    }
  }
}

(3)synchronized作用于static 函數
要同步靜態方法,一是在靜態方法上加synchronized關鍵字,另一個是在整個類對象的鎖,這個對象是就是這個類(XXX.class)。
public class TestSynClass extends Thread{{

 public synchronized static void methodA()      { 
   //some code
  }    
  public static void methodB()    {       
    synchronized(TestSynClass.class) {
      //some code
    }  
  }
}

線程同步小結
1、線程同步的目的是為了保護多個線程反問一個資源時對資源的破壞。
2、線程同步方法是通過鎖來實現,每個對象都有切僅有一個鎖,這個鎖與一個特定的對象關聯,線程一旦獲取了對象鎖,其他訪問該對象的線程就無法再訪問該對象的其他同步方法。
3、對于靜態同步方法,鎖是針對這個類的,鎖對象是該類的Class對象。靜態和非靜態方法的鎖互不干預。一個線程獲得鎖,當在一個同步方法中訪問另外對象上的同步方法時,會獲取這兩個對象鎖。
4、對于同步,要時刻清醒在哪個對象上同步,這是關鍵。
5、編寫線程安全的類,需要時刻注意對多個線程競爭訪問資源的邏輯和安全做出正確的判斷,對“原子”操作做出分析,并保證原子操作期間別的線程無法訪問競爭資源。
6、當多個線程等待一個對象鎖時,沒有獲取到鎖的線程將發生阻塞。
7、死鎖是線程間相互等待鎖鎖造成的,在實際中發生的概率非常的小。真讓你寫個死鎖程序,并不一定好使。但是,一旦程序發生死鎖,程序將死掉。

五、線程的交互與調度(線程狀態轉換實例)

1. 線程的交互
線程交互的方法

  • void notify() 喚醒在此對象監視器上等待的單個線程。
  • void notifyAll() 喚醒在此對象監視器上等待的所有線程,應該在同步塊中調用。
  • void wait()導致當前的線程等待,直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法。
    當然,wait()還有另外兩個重載方法:
    void wait(long timeout)導致當前的線程等待,直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量。
    void wait(long timeout, int nanos) 導致當前的線程等待,直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者其他某個線程中斷當前線程,或者已超過某個實際時間量。

關于線程/通知要關鍵點
(1)必須從同步環境內調用wait()、notify()、notifyAll()方法。線程不能調用對象上等待或通知的方法,除非它擁有那個對象的鎖。
(2)wait()、notify()、notifyAll()都是Object的實例方法。與每個對象具有鎖一樣,每個對象可以有一個線程列表,他們等待來自該信號(通知)。線程通過執行對象上的wait()方法獲得這個等待列表。從那時候起,它不再執行任何其他指令,直到調用對象的notify()方法為止。如果多個線程在同一個對象上等待,則將只選擇一個線程(不保證以何種順序)繼續執行。如果沒有線程等待,則不采取任何特殊操作。

線程交互的實例

  • wait()/notify()的使用

當在對象上調用wait()方法時,執行該代碼的線程立即放棄它在對象上的鎖。然而調用notify()時,并不意味著這時線程會放棄其鎖。如果線程榮然在完成同步代碼,則線程在移出之前不會放棄鎖。因此,只要調用notify()并不意味著這時該鎖變得可用。

主函數實例化線程,然后調用線程的wait()函數,等待線程計算1到100的和的結果。

package tym.ThreadBase.waitAndnotify;
/** * Created by TyiMan on 2016/5/16. */
public class TestWaitNotifyMain {   
  public static void main(String[] args){        
    TestThread thread = new TestThread();        
    thread.start();        
    synchronized (thread){            
      try{                
        System.out.println("等待對象b完成計算……");               
        thread.wait();           
      } catch (InterruptedException e) {                
        e.printStackTrace();            
      }            
      System.out.println("線程計算結果為 total is "+thread.total);        
    }    
  }
}

TestThread類的run方法計算1到100的和,計算完后,調用notify()函數。

package tym.ThreadBase.waitAndnotify;
/** * Created by TyiMan on 2016/5/16. */
public class TestThread extends Thread {    
  int total ;    
  @Override    
  public void run(){        
    synchronized (this){           
      for(int i = 0;i<=100;i++){                
       total +=i;            
      }            
      notify();        
    }    
  }
}
  • wait()/notifyAll()的使用
    在多數情況下,最好通知等待某個對象的所有線程。如果這樣做,可以在對象上使用notifyAll()讓所有在此對象上等待的線程沖出等待區,返回到可運行狀態。

Calculator計算1到100的和,計算完喚醒所有其他線程。

package tym.ThreadBase.waitAndnotify;
/** * Created by TyiMan on 2016/5/16. */
public class Calculator extends Thread {    
  int total;   
  @Override    
  public void run() {        
    synchronized (this) {            
      for (int i = 0; i < 101; i++) {                
        total += i;            
      } 
      notifyAll();        
    }   
  }
}

ResultReader類等待結果,被喚醒后顯示Calculator類的計算結果。

package tym.ThreadBase.waitAndnotify;

/**
 * Created by TyiMan on 2016/5/16.
 */
public class ReaderResult extends Thread {
    Calculator calculator;

    public ReaderResult(Calculator c) {
        this.calculator = c;
    }

    @Override
    public void run() {
        synchronized (calculator) {
            try {
                System.out.println(Thread.currentThread() + "等待計算結果。。。");
                calculator.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread() + "計算結果為:" + calculator.total);
        }
    }

    public static void main(String[] args) {
        Calculator calculator = new Calculator();

        //啟動三個線程,分別獲取計算結果
        new ReaderResult(calculator).start();
        new ReaderResult(calculator).start();
        new ReaderResult(calculator).start();
        //啟動計算線程
        calculator.start();
     }
}

最后輸出結果:

Thread[Thread-1,5,main]等待計算結果。。。
Thread[Thread-2,5,main]等待計算結果。。。
Thread[Thread-3,5,main]等待計算結果。。。
Thread[Thread-3,5,main]計算結果為:5050
Thread[Thread-2,5,main]計算結果為:5050
Thread[Thread-1,5,main]計算結果為:5050

問題注意
實際上,上面代碼中,我們期望的是讀取結果的線程在計算線程調用notifyAll()之前等待即可。 但是,如果計算線程先執行,并在讀取結果線程等待之前調用了notify()方法,那么又會發生什么呢?
問題分析
這種情況是可能發生的。因為無法保證線程的不同部分將按照什么順序來執行。幸運的是當讀取線程運行時,它只能馬上進入等待狀態,它沒有做任何事情來檢查等待的事件是否已經發生。因此,如果計算線程已經調用了notifyAll()方法,那么它就不會再次調用notifyAll(),并且等待的讀取線程將永遠保持等待。這當然是開發者所不愿意看到的問題。
問題解決
當等待的事件發生時,需要能夠檢查notifyAll()通知事件是否已經發生。通常是利用某種循環,該循環檢查某個條件表達式,只有當正在等待的事情還沒有發生的情況下,它才繼續等待。

2. 線程的調度——休眠
線程休眠的目的是使線程讓出CPU的最簡單的做法之一,線程休眠時候,會將CPU資源交給其他線程,以便能輪換執行,當休眠一定時間后,線程會蘇醒,進入準備狀態等待執行。
線程休眠的方法是Thread.sleep(long millis)Thread.sleep(long millis, int nanos),均為靜態方法,那調用sleep休眠的哪個線程呢?簡單說,哪個線程調用sleep,就休眠哪個線程。
sleep()實例代碼

package tym.ThreadBase.waitAndnotify;

/**
 * Created by TyiMan on 2016/5/16.
 */
public class Test {
  public static void main(String[] args) {
    Thread t1 = new MyThread1();
    Thread t2 = new Thread(new MyRunnable());
    t1.start();
    t2.start();
  }
}

class MyThread1 extends Thread {
  public void run() {
    for (int i = 0; i < 3; i++) {
        System.out.println("線程1第" + i + "次執行!");
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
  }
}

class MyRunnable implements Runnable {
  public void run() {
    for (int i = 0; i < 3; i++) {
        System.out.println("線程2第" + i + "次執行!");
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
  }
}

輸出結果:

線程2第0次執行!
線程1第0次執行!
線程1第1次執行!
線程2第1次執行!
線程2第2次執行!
線程1第2次執行!

3. 線程調度——優先級
void setPriority(int newPriority)函數設置線程優先級
與線程休眠類似,線程的優先級仍然無法保障線程的執行次序。只不過,優先級高的線程獲取CPU資源的概率較大,優先級低的并非沒機會執行。

  • 線程的優先級用1-10之間的整數表示,數值越大優先級越高,默認的優先級為5。
  • 在一個線程中開啟另外一個新線程,則新開線程稱為該線程的子線程,子線程初始優先級與父線程相同。

setPriority()代碼實例

package tym.ThreadBase.waitAndnotify;

/**
 * Created by TyiMan on 2016/5/16.
 */
public class Test {
  public static void main(String[] args) {
    Thread t1 = new MyThread1();
    Thread t2 = new Thread(new MyRunnable());
    t1.setPriority(10);
    t2.setPriority(1);

    t2.start();
    t1.start();
  }
}

class MyThread1 extends Thread {
  public void run() {
    for (int i = 0; i < 10; i++) {
        System.out.println("線程1第" + i + "次執行!");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
  }
}

class MyRunnable implements Runnable {
  public void run() {
    for (int i = 0; i < 10; i++) {
        System.out.println("線程2第" + i + "次執行!");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
  }
}

執行結果:

線程1第0次執行! 
線程2第0次執行! 
線程2第1次執行! 
線程1第1次執行! 
線程2第2次執行! 
線程1第2次執行! 
線程1第3次執行! 
線程2第3次執行! 
線程2第4次執行! 
線程1第4次執行! 
線程1第5次執行! 
線程2第5次執行! 
線程1第6次執行! 
線程2第6次執行! 
線程1第7次執行! 
線程2第7次執行! 
線程1第8次執行!
線程2第8次執行!
線程1第9次執行!
線程2第9次執行! 

4. 線程的調度——讓步

線程的讓步含義就是使當前運行著線程讓出CPU資源,但是然給誰不知道,僅僅是讓出,線程狀態回到可運行狀態。

  • 線程的讓步使用Thread.yield()方法,yield() 為靜態方法,功能是暫停當前正在執行的線程對象,并執行其他線程。

yield()代碼實例

package tym.ThreadBase.waitAndnotify;

/**
 * Created by TyiMan on 2016/5/16.
 */
public class Test {
  public static void main(String[] args) {
    Thread t1 = new MyThread1();
    Thread t2 = new Thread(new MyRunnable());

    t2.start();
    t1.start();
  }
}

class MyThread1 extends Thread {
  public void run() {
    for (int i = 0; i < 10; i++) {
        System.out.println("線程1第" + i + "次執行!");
    }
  }
}

class MyRunnable implements Runnable {
  public void run() {
    for (int i = 0; i < 10; i++) {
        System.out.println("線程2第" + i + "次執行!");
        Thread.yield();
    }
  }
}

執行結果:

線程2第0次執行!
線程1第0次執行!
線程2第1次執行!
線程1第1次執行!
線程2第2次執行!
線程1第2次執行!
線程2第3次執行!
線程1第3次執行!
線程2第4次執行!
線程1第4次執行!
線程2第5次執行!
線程1第5次執行!
線程2第6次執行!
線程1第6次執行!
線程2第7次執行!
線程1第7次執行!
線程2第8次執行!
線程1第8次執行!
線程2第9次執行!
線程1第9次執行!

在使用synchronized關鍵字時候,應該盡可能避免在synchronized方法或synchronized塊中使用sleep或者yield方法,因為synchronized程序塊占有著對象鎖,sleep()讓程序睡眠還不釋放鎖,不但嚴重影響效率,也不合邏輯。在同步程序塊內調用yeild()方法讓出CPU資源也沒有意義,因為它占用著鎖,其他互斥線程還是無法訪問同步程序塊。當然與同步程序塊無關的線程可以獲得更多的執行時間。

5. 線程的調度——合并

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

  • join()的作用是:“等待該線程終止”,這里需要理解的就是該線程是指的主線程等待子線程的終止。也就是在子線程調用了join()方法后面的代碼,只有等到子線程結束了才能執行。
  • join()為非靜態方法,定義如下:
    void join()等待該線程終止。
    void join(long millis)等待該線程終止的時間最長為 millis 毫秒。
    void join(long millis, int nanos)等待該線程終止的時間最長為 millis 毫秒 + nanos 納秒。

join()代碼實例

package tym.ThreadBase.waitAndnotify;

/**
 * Created by TyiMan on 2016/5/16.
 */
public class Test {
  public static void main(String[] args) {
    Thread t1 = new MyThread1();
    t1.start();

    System.out.println("主線程開始執行!");
    try {
        //t1線程合并到主線程中,主線程停止執行過程,轉而執行t1線程,直到t1執行完畢后繼續。
        t1.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("主線程執行完畢!");
  }
}

class MyThread1 extends Thread {
public void run() {
    for (int i = 0; i < 2; i++) {
        System.out.println("線程1第" + i + "次循環!");
    }
  }
}

執行結果:

主線程第0次執行!
線程1第0次執行!
線程1第1次執行!
主線程第1次執行!

6. 守護線程

守護線程與普通線程寫法上基本么啥區別,調用線程對象的方法setDaemon(true),則可以將其設置為守護線程。
守護線程使用的情況較少,但并非無用,舉例來說,JVM的垃圾回收、內存管理等線程都是守護線程。還有就是在做數據庫應用時候,使用的數據庫連接池,連接池本身也包含著很多后臺線程,監控連接個數、超時時間、狀態等等。

setDaemon方法的詳細說明:
public final void setDaemon(boolean on)將該線程標記為守護線程或用戶線程。當正在運行的線程都是守護線程時,Java 虛擬機退出。
該方法必須在啟動線程前調用。
該方法首先調用該線程的 checkAccess 方法,且不帶任何參數。這可能拋出 SecurityException(在當前線程中)。
參數: on - 如果為 true,則將該線程標記為守護線程。
拋出
IllegalThreadStateException - 如果該線程處于活動狀態。
SecurityException - 如果當前線程無法修改該線程。

setDaemon()代碼實例

package tym.ThreadBase.waitAndnotify;

/**
 * Created by TyiMan on 2016/5/16.
 */
public class Test {
  public static void main(String[] args) {
    Thread t1 = new MyCommon();
    Thread t2 = new Thread(new MyDaemon());
    t2.setDaemon(true);        //設置為守護線程

    t2.start();
    t1.start();
  }
}

class MyCommon extends Thread {
  public void run() {
    for (int i = 0; i < 5; i++) {
        System.out.println("線程1第" + i + "次執行!");
        try {
            Thread.sleep(7);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
  }
}

class MyDaemon implements Runnable {
  public void run() {
    for (long i = 0; i < 9999999L; i++) {
        System.out.println("后臺線程第" + i + "次執行!");
        try {
            Thread.sleep(7);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
  }
}

執行結果:守護進程隨著主進程結束而結束

后臺線程第0次執行!
線程1第0次執行!
線程1第1次執行!
后臺線程第1次執行!
后臺線程第2次執行!
線程1第2次執行!
后臺線程第3次執行!
線程1第3次執行!
后臺線程第4次執行!
線程1第4次執行!
后臺線程第5次執行!

六、線程基礎總結

該部分為Java線程比較基礎的部分,接下來會繼續整理一些更加深入的知識。想到Java多線程基礎,我們應該知道自己應該了解:

  • Java線程的兩種實現方式集成Thread類和實現Runnable接口,調用start()函數來啟動線程。
  • Java線程新建[new]、就緒[start()]、運行[執行run()]、阻塞[sleep(),join(),wait()、喚醒notify(),等待鎖]、死亡[結束所有操作,stop()或destroy(),發生異常終止]的生命周期。
  • 了解線程同步的原理和作用、synchronized關鍵字的使用方法:同步方法和同步代碼塊,靜態方法的同步。
  • 了解Java線程交互與調度函數wait(), notify(), notifyAll(), sleep(), setPriority(), yield(), join(), setDaemon()方法和使用。

七、參考引用

http://lavasoft.blog.51cto.com/62575/27069/
http://blog.csdn.net/csh624366188/article/details/7318245
http://www.cnblogs.com/riskyer/p/3263032.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 本文主要講了java中多線程的使用方法、線程同步、線程數據傳遞、線程狀態及相應的一些線程函數用法、概述等。 首先講...
    李欣陽閱讀 2,482評論 1 15
  • Java多線程學習 [-] 一擴展javalangThread類 二實現javalangRunnable接口 三T...
    影馳閱讀 2,981評論 1 18
  • 寫在前面的話: 這篇博客是我從這里“轉載”的,為什么轉載兩個字加“”呢?因為這絕不是簡單的復制粘貼,我花了五六個小...
    SmartSean閱讀 4,776評論 12 45
  • 該文章轉自:http://blog.csdn.net/evankaka/article/details/44153...
    加來依藍閱讀 7,378評論 3 87
  • 一:java概述:1,JDK:Java Development Kit,java的開發和運行環境,java的開發工...
    ZaneInTheSun閱讀 2,686評論 0 11