Java多線程(一)--線程的一些基本概念

這周趕項目,暫停了一下微博。結果今天看到簡書app的圖標竟然有負罪感!趁著周末,更新一波。。。

從本文開始,我們開始分析一個新的Java的知識點--多線程。要研究這個,首先我們要知道,什么是線程。

線程的定義

進程是指一個內存中運行的應用程序,每個進程都有自己獨立的一塊內存空間,擁有自己的數據和代碼。

線程是指進程中的一個任務,一個進程中可以運行多個線程。線程是屬于某個進程,進程中的多個線程共享此進程的內存。

拿手機舉個例子,我們拿著手機可以一邊聽歌一邊看微信,此時,微信和聽歌軟件就構成了多進程。即同一時刻,有不同的進程在工作。而多線程就是指在同一個程序中,同時執行多個任務,通常,每個任務稱為一個線程。線程跟進程的區別是,每個進程都有自己獨立的數據空間,而同一類的線程共享數據。多進程是為了提高CPU的使用率,而多線程是為了提高應用程序的使用率。

線程的狀態

線程的狀態如下:

image

線程的狀態分為五種:

新建狀態:當使用某種方式創建一個線程對象后,該線程就是新建狀態

就緒狀態:當已創建的線程的start()方法被調用后,就進入就緒狀態。這種狀態也叫做"可執行狀態"。在這種狀態下,該線程隨時等待被CPU調度執行(注意,這個時候不是立即執行,而是等待CPU的調度)。

可運行狀態:在Java虛擬機中執行的線程處于此狀態。即線程取得CPU的權限,開始執行。線程只能從就緒狀態進入運行狀態。(在任何給定時刻,一個可運行的線程可能正在運行也可能沒有運行)

阻塞狀態:當線程因為某種關系,無法獲得CPU的使用權限時,就處于這種狀態。比如調用的wait()方法(等待)、有同步鎖(阻塞)及調用了sleep()方法(計時等待)等。

死亡狀態:以退出的線程處于此狀態。退出可能是線程執行完畢,或者是發生了異常等。

當一個線程開始運行的時候,它并不是始終運行的。因為Java中多線程是搶占式調度,即多個線程搶占時間片來執行任務。當一個線程的時間片用完后,它就會被系統剝奪其運行權限,并與其他線程共同爭奪下一個時間片的使用權。

線程的創建

創建線程的方式有:繼承Thread類、實現Runnable接口及通過Callable和Future新建一個線程。

繼承Thread類

創建的步驟為:

1、繼承Thread類

2、重寫run方法

3、實例化我們寫的Thread類的子類,并調用start方法啟動線程

具體代碼如下:

//繼承Thread類
public class MyThread extends Thread{

   //重寫run方法(線程的執行部分)
   @Override
    public void run() {
       //自己的代碼邏輯
       ........
    }

}

public class Main{

    public static void main(String [] args){
    
        //實例化一個MyThread類的子類
        MyThread myThread = new MyThread();
        //調用start方法啟動線程
        myThread.start();
        
    }

}

實現Runnable接口

創建步驟為:

1、定義一個類,實現Runnable接口,重寫run方法;

2、創建一個Runnable的實現類的實例,并以此為參數創建一個Thread類

3、調用Thread類的start方法,啟動線程

還可以創建一個實現Runnable接口的匿名類,或者創建一個實現Runnable接口的Java Lambda表達式(JDK8之后)。

具體代碼如下:

//定義一個類,實現Runnable接口
public class MyRunnable implements Runnable {

    //重寫run方法
    @Override
    public void run() {
        //自己的代碼邏輯
        .......
    }
}

public class Main{

    public static void main(String [] args){
    
        //實例化一個Runnable實現類的實例
        MyRunnable myRunnable = new MyRunnable();
        //以myRunnable為參數實例化一個Thread類
        Thread thread = new Thread(myRunnable);
        //調用start方法啟動線程
        thread();
        
        /**
          *--------分割線------
          */
          //創建Runnable的匿名實現
          Runnable myRunnable =new Runnable(){
                public void run(){
                     //自己的代碼邏輯
                      .......                 
                      }
              }
             
          //Runnable的Lambda實現
          Runnable runnable =() -> { //自己的代碼邏輯};      
        
}

注意:

1、使用"myThread.run();"時,run()方法并非是由剛創建的新線程執行,而是被創建新線程的當前線程所執行了。想要讓創建的新線程執行run()方法,必須調用新線程的start()方法。且start()方法不可以多次調用。

2、調用start()方法后,并不是讓線程立刻執行,而是將線程變為可執行狀態,等待CPU的調度。

問題來了,當用此方式創建線程后,線程執行的run()方法是Runnable接口中的還是Thread類中的?

我們看一下Thread類的定義及其run()方法:

public class Thread implements Runnable {

private Runnable target;

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
       ......
        this.target = target;
       ......
    }

@Override
public void run() {
    if (target != null) {
        target.run();
     }
  }

}

可以看到,在run()方法中,會判斷target是否為空,不為空則執行Runnable的run()方法,否則此方法不執行任何操作并返回。也是因為如此,Java提示Thread的子類要重寫此方法。

實現Runnable接口比繼承Thread類的優勢:

1、避免了Java中的單繼承限制

2、適合多個相同的程序代碼的線程去處理同一個共享資源

3、代碼可以被多個線程共享且代碼和數據獨立,增加了程序的健壯性

通過Callable和FutureTask

創建步驟為:

1、創建一個Callable接口的實現類,并實現call()方法

2、使用FutureTask類包裝Callable實現類的對象,封裝了Callable的call()方法的返回值

3、以FutureTask對象為Thread的參數創建線程,并啟動線程

4、調用FutureTask對象的get()方法,獲取線程執行結束后的返回值

代碼如下:

//創建一個Callable接口的實現類,并實現call()方法
public class MyThread implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        //自己的代碼邏輯
        return value;
    }
}

public class Main{

    public static void main(String [] args){
    
        //使用FutureTask類包裝Callable實現類的對象
        MyThread myThread = new MyThread();

        FutureTask futureTask = new FutureTask<Integer>(myThread);

        //以FutureTask對象為Thread的參數創建線程
        Thread thread = new Thread(futureTask);

        thread.start();  
        
        //獲取值
        try {
            //get()方法會阻塞,直到子線程執行結束才返回
            int x = (int) futureTask.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }      
    }

}

其中,Callable的類型參數為返回值的類型,Future保存異步計算的結果。

在計算過程中,可以使用isDone方法判斷Future任務是否結束(包括正常結束或者中途退出),返回true表示完成,返回false表示未完成。可以用cancal方法取消計算。

一般推薦使用實現Runable或Callable接口的方式來創建多線程。因為這樣既可以繼承其他類,而且多線程可以共享一個target,即多線程可以共同處理同一份資源。

線程的優先級及守護線程

線程優先級

每個線程都有優先級,且默認情況下,線程繼承其父類的優先級。我們可以使用setPriority方法為線程設置優先級。可以將線程的優先級設置在MIN_PRIORITY(1)和MAX_PRIORITY(10)之間。NORM_PRIORITY表示線程優先級為5,為默認優先級。數字越大,表示優先級越高。高優先級的線程被CPU調用的概率大于低優先級的線程。不過要注意的是線程優先級無法保證線程的執行順序,它是依賴于平臺的。比如在Linux下,線程優先級僅適用于Java6之后,在這之前線程優先級沒有作用。

守護線程

在Java中,線程可以分為用戶線程和守護線程,可以使用Thread類的setDaemon方法將一個線程設置為守護線程。守護線程在后臺運行,當JVM中沒有其他非守護線程時,守護線程會和JVM一起結束。守護線程的作用是為其他線程提供服務,比如我們熟悉的GC就是這樣。要注意的是,不要用守護線程訪問文件或數據庫等資源。因為守護線程可能在任何時候發生中斷,而這個時候,我們對資源文件的讀寫有可能還沒有完成。

有時候主線程都結束了,守護線程還在執行,這是因為線程結束是需要時間的。

Thread類的部分方法

start()方法

start方法為將線程由新建狀態轉變為可運行狀態,其源碼如下:

//表示Java線程狀態的工具,初始化為線程未啟動
private volatile int threadStatus = 0;

//線程是否運行的標志
boolean started = false;

//此線程的線程組
private ThreadGroup group;

public synchronized void start() {

        //判斷線程是否未啟動或者已經運行,滿足一項則拋出異常
        if (threadStatus != 0 || started)
            throw new IllegalThreadStateException();

        //添加次線程到其線程組
        group.add(this);
        
        //將運行狀態置為false
        started = false;
        try {
              //調用本地方法啟動線程
            nativeCreate(this, stackSize, daemon);
            //將線程的運行狀態置為true
            started = true;
        } finally {
            try {
                   //如果啟動失敗,從其線程組中移除此線程
                   //此線程組的狀態將回滾,就像從未嘗試啟動線程一樣。該線程再次被視為線程組的未啟動成員,允許隨后嘗試啟動該線程。
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                            }
        }
    }

可以看到,當一個線程是未啟動或者已運行時,調用start方法將拋出異常。當線程啟動失敗后,會將其從線程組中移除,以讓其有機會重新啟動。

sleep()方法

使當前正在執行的線程休眠(暫時停止執行),該線程不會失去任何監視器的所有權。源碼如下:

private static final int NANOS_PER_MILLI = 1000000;

public static void sleep(long millis) throws InterruptedException {
     
        Thread.sleep(millis, 0);
    }

//實際調用的方法    
public static void sleep(long millis, int nanos)
    throws InterruptedException {
        //判斷傳入的毫秒和納秒是否有錯誤
        if (millis < 0) {
            throw new IllegalArgumentException("millis < 0: " + millis);
        }
        if (nanos < 0) {
            throw new IllegalArgumentException("nanos < 0: " + nanos);
        }
        if (nanos > 999999) {
            throw new IllegalArgumentException("nanos > 999999: " + nanos);
        }
        //零睡眠
        if (millis == 0 && nanos == 0) {
            //如果線程為中斷狀態,則拋出異常并返回
            if (Thread.interrupted()) {
              throw new InterruptedException();
            }
            return;
        }
        //返回運行的Java虛擬機的高分辨率時間源的當前值,以納秒計。
        long start = System.nanoTime();
        //將傳入的時間轉為納秒級
        long duration = (millis * NANOS_PER_MILLI) + nanos;
         //獲取鎖
        Object lock = currentThread().lock;

        //等待可能會提前返回,所以循環直到睡眠時間結束。
        synchronized (lock) {
            while (true) {
                //調用本地方法
                sleep(lock, millis, nanos);

                long now = System.nanoTime();
                long elapsed = now - start;

                if (elapsed >= duration) {
                    break;
                }

                duration -= elapsed;
                start = now;
                millis = duration / NANOS_PER_MILLI;
                nanos = (int) (duration % NANOS_PER_MILLI);
            }
        }
    }

由上面的源碼可以看出,我們最終調用的是sleep(long millis, int nanos)方法。在sleep方法中,是通過循環不斷判斷當前時間跟起始時間的差值,直到這個值大于等于我們傳入的休眠時間,則線程可以繼續工作。在此期間,當前線程為阻塞狀態。

yield()方法

線程執行此方法的作用是暫停當前正在執行的線程,使其他具有相同優先級的線程獲得運行的機會。但是在實際中,我們不能保證其功能可以完全實現,因為yield是將線程從運行狀態變為可運行狀態,在這種情況下,當前線程可能會被CPU再次選中。此方法為本地方法,jdk中源碼如下:

public static native void yield();

join()方法

此方法為讓一個線程加入到另一個線程的后面,在前面的線程沒有結束的時候,后面的線程不被執行。調用此方法會導致線程棧發生變化,當然,這些變化都是瞬時的。

//負責此線程的join / sleep / park操作的同步對象
private final Object lock = new Object();

public final void join() throws InterruptedException {
        //調用join(long millis)方法
        join(0);
    }
    
public final void join(long millis, int nanos)
    throws InterruptedException {
        synchronized(lock) {
        //判斷傳入的參數是否正確
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
        //根據條件判斷millis是否要加1
        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }
          //調用join(long millis)方法
        join(millis);
        }
    }
 
//真正被調用的方法    
public final void join(long millis) throws InterruptedException {
        synchronized(lock) {
        //返回當前時間
        long base = System.currentTimeMillis();
        long now = 0;
          //判斷傳入的參數是否正確
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
          //以下就是根據isAlive(線程是否存活),調用wait方法的循環
        if (millis == 0) {
            while (isAlive()) {
                lock.wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                lock.wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
        }
    }      

wait()方法的作用:導致當前線程等待,直到另一個線程調用此對象的notify()方法或notifyAll()方法或指定的等待時間已經過去。

由源碼可知,我們可以自己設置等待時間。但是如果我們不設置等待時間或者設置的等待時間為0,則線程會永遠等待。直到被其join的線程結束后,會調用this.notifyAll方法,使其結束等待。

未捕獲異常處理器

UncaughtExceptionHandler:是在Java Thread類中定義的,當Thread由于未捕獲的異常而突然終止時調用的處理程序接口。這個接口只有一個方法:

//當給定線程由于給定的未捕獲異常而終止時調用的方法。Java虛擬機將忽略此方法拋出的任何異常
void uncaughtException(Thread t, Throwable e);

當一個線程由于未捕獲的異常而即將終止時,Java虛擬機將使用getUncaughtExceptionHandler向線程查詢其UncaughtExceptionHandler并將調用處理程序的uncaughtException方法,將線程和異常作為參數傳遞。如果某個線程沒有顯式設置其UncaughtExceptionHandler,則其ThreadGroup對象將充當其UncaughtExceptionHandler。如果ThreadGroup對象沒有處理異常的特殊要求,它可以將調用轉發到getDefaultUncaughtExceptionHandler默認的未捕獲異常處理程序。我們可以用setUncaughtExceptionHandler方法為任何線程設置一個處理器。也可以用Thread類的靜態方法setDefaultUncaughtExceptionHandler為所有線程設置一個默認的處理器。我們可以通過實現Thread.UncaughtExceptionHandler接口并重寫其uncaughtException方法來自定義一個未捕獲異常處理器。

小結

本文主要是簡單的介紹一下線程的相關概念,使大家對線程有基本的了解。本文中涉及到的鎖及介紹的相關方法,會在后期的分析中一一講解。

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

推薦閱讀更多精彩內容

  • 進程和線程 進程 所有運行中的任務通常對應一個進程,當一個程序進入內存運行時,即變成一個進程.進程是處于運行過程中...
    勝浩_ae28閱讀 5,137評論 0 23
  • 進程和線程 進程 所有運行中的任務通常對應一個進程,當一個程序進入內存運行時,即變成一個進程.進程是處于運行過程中...
    小徐andorid閱讀 2,832評論 3 53
  • 單任務 單任務的特點是排隊執行,也就是同步,就像再cmd輸入一條命令后,必須等待這條命令執行完才可以執行下一條命令...
    Steven1997閱讀 1,207評論 0 6
  • 前言 多線程并發編程是Java編程中重要的一塊內容,也是面試重點覆蓋區域,所以學好多線程并發編程對我們來說極其重要...
    嘟爺MD閱讀 7,334評論 21 272
  • ??小感動 1.慎思園的阿姨每次看見我都超開心,我覺得像家人需要我不時回去看看~今天還非要送給我小橘子,覺得好溫暖...
    wutacooper閱讀 184評論 0 0