Java併發編程:3-Thread類的使用

前言:

前面大體瞭解了線程的建立和生命週期,線程在生命週期中並非固定處於某一個狀態而是隨着代碼的執行在不一樣狀態之間切換。本篇經過對Thread類中方法的講解來展現線程生命週期的變化,同時也會對Thread類自己進行理解。java

面試問題

Q :wait和sleep方法的區別?
Q :爲何wait和notify/notifyAll要定義在Object中?面試

1.Thread中的屬性

public class Thread implements Runnable {
        private volatile String name;
        private boolean  daemon = false;
        private Runnable target;
        private volatile Interruptible blocker;
        volatile Object parkBlocker;
        private volatile int threadStatus = 0;

        private int priority;
        public final static int MIN_PRIORITY = 1;
        public final static int NORM_PRIORITY = 5;
        public final static int MAX_PRIORITY = 10;

        private volatile UncaughtExceptionHandler uncaughtExceptionHandler;
        private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
        ...
    }

  name :是表示Thread的名字,能夠經過Thread類的構造器中的參數來指定線程名字,經過getName()來獲取線程的名稱,建議根據任務或功能對線程進行合理命名,以便調試。 編程

  dameon :表示線程是不是守護線程,默認爲false,守護線程是爲非守護線程服務的,在後臺默默的完成一些系統性的服務,好比垃圾回收線程,JIT線程,若是JVM中只剩守護線程,JVM會直接退出。守護線程的設置務必在線程啓動前完成。windows

Thread t=new Thread();
    t.setDaemon(true);
    t.start();

  target :用來存放須要執行的任務。也就是構造方法中傳入的Runnable實現,FutureTask也會存在這裏。 安全

  blocker :中斷標誌位,用於判斷是否被中斷,中斷能夠理解爲打斷,若是須要中止一個正在執行任務的線程,能夠經過線程的實例方法interrupt()或者Thread.interrupted()來設置其中斷標誌。數據結構

  parkBlocker :和中斷標誌位相似,不過修改這個狀態的方法不在Threa中,而是經過JUC包下的LockSupport來操做。多線程

  threadStatus :線程當前的狀態。NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING和TERMINATED。併發

  priority :表示線程的優先級,優先級不是誰先誰後,而是權重,優先級高的線程更容易搶到cpu時間片,優先級分爲1-10共10個等級,1表示最低優先級,5是默認級別。setPriority()用來設定線程的優先級,須要在線程start()調用以前進行設定。 ide

  uncaughtExceptionHandler :未捕獲異常的處理器,因爲線程的本質特性,沒法在當前線程捕獲到從其餘線程中逃逸的異常,一旦異常逃逸出run方法,它就會向外傳播到控制檯,而咱們一般須要記錄異常日誌,因此就須要對線程作運行時的異常處理可使用實例方法setUncaughtExceptionHandler() 來配置未捕獲異常處理器。工具

public void test() {
        try {
            new Thread(() -> {
                throw new RuntimeException();
            }).start();
        } catch (Exception e) {
            //不能捕獲到其餘線程的異常,因此下面這行代碼不會被打印
            System.out.println("出錯了");
        }
    }

    public static class MyUnCatchExceptionHandler implements Thread.UncaughtExceptionHandler {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("this is MyUnCatchExceptionHandler ");
            System.out.println("catch " + e + " 記錄日誌");
        }
    }

    //經過對線程實例設置unCaughtExceptionHandler,
    public void test2() {
        Thread thread = new Thread(() -> {
            throw new RuntimeException();
        });
        thread.setUncaughtExceptionHandler(new MyUnCatchExceptionHandler());
        thread.start();
    }

  JDK5 以後可使用Executor的ThreadFactory來解決這個問題,經過給 Thread實例 設置一個(實現 UnCatchExceptionHandler接口)未捕獲異常處理器,也可使用 Thread.setDefaultUnCatchExceptionHandler() 來配置默認的未捕獲異常處理器。

//經過在ThreadFactory中設置defaultUnCatchExceptionHandler
    public void test3() {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool(new MyThreadFactory());
        cachedThreadPool.execute(() -> {
            throw new RuntimeException();
        });
    }


    public static class MyThreadFactory implements ThreadFactory {
        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            //thread.setUncaughtExceptionHandler(new MyUnCatchExceptionHandler());
            Thread.setDefaultUncaughtExceptionHandler(new MyUnCatchExceptionHandler());
            return thread;
        }
    }

  defaultUncaughtExceptionHandler :Thread類提供的默認未捕獲異常的處理器。

2.Thread中的方法

  經過Thread中的屬性,大體瞭解了Thread類的結構,下面咱們經過線程狀態轉換圖來學習Thread類中的方法。

6-線程狀態轉換圖.jpg

2.1start()、run()和stop()

  在咱們實例化一個Thread對象後,這個對象處於初始狀態,也就是threadStatus爲NEW,此時這個對象只是堆中的一個普通Java對象,雖然被稱爲線程對象,但其實在操做系統中並無與之對應的線程,只有當調用該對象的start,操做系統纔會建立一個新線程,咱們能夠經過斷點進行查看。

在執行thread.start()以前

8-線程建立時機.jpg

在執行thread.start()以後

9-線程建立時機2.jpg

  Java的線程是不容許啓動兩次的,第二次調用會拋出IllegalThreadStateException,這是一種運行時異常。

  run()不須要咱們手動調用,經過start()方法啓動線程以後,當線程得到CPU執行時間,便進入run()方法體去執行具體的任務。直接調用run方法會被當前線程看成一次普通的方法調用,歸屬於當前的線程棧。

  在run()方法正常執行完成後,線程會處於終止狀態。但也老是會有例外狀況,若是須要提早終止一個正在運行的線程,可使用interrupt 方式或者stop()方法:

  • (1) 使用stop方法強行終止線程,不推薦使用,stop會當即釋放掉該線程所持有的鎖,可能沒法正常釋放本身所持有的資源,形成未知錯誤。若是修改了一半就被stop掉,那數據也只會被修改一半,可能產生不可預料的結果;
  • (2) 使用interrupt的方式,interrupt()方法並未真正中止線程,只不過在線程中修改了blocker標記,此時可使用拋異常的方式使線程中止。

  windows的線程是搶佔式的,意味着線程能夠強制結束其餘線程,例如經過任務管理器結束一個無響應的應用程序。
Java的線程工做方式是協做式,這樣設計是爲了讓線程自身可以在線程關閉前處理本身的數據。

2.2suspend()和resume()

不推薦使用
  若是想讓一個線程暫停執行,而不是終止這個線程,可使用suspend()將線程掛起,須要線程繼續執行時使用resume()。正常狀況下是先suspend()再resume(),若是將這兩個方法的調用順序調換,那麼線程將永遠被掛起,而且suspend()不會釋放鎖,這種狀況下則會發生死鎖。並且被suspend掛起的線程狀態顯示爲"RUNNABLE"狀態,這給排查bug帶來困難。
  所以在JUC中提供了LockSupport類來代替suspend()和resume(),能夠看到線程狀態轉換圖中的LockSupport.park()和LockSupport.unpark(),後面會的對LockSupport的實現進行詳細的講解。

public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            int i = 0;
            while (true) {
                    System.out.println(i++);
            }
        });
        thread.start();
        TimeUnit.NANOSECONDS.sleep(1);
        //先掛起再繼續執行
        thread.resume();
        thread.suspend();
    }
    //Output
    // 這種狀況程序會一直執行,不停的打印i的值
public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            int i = 0;
            while (true) {
                    System.out.println(i++);
            }
        });
        thread.start();
        TimeUnit.NANOSECONDS.sleep(1);
        //先繼續執行再掛起
        thread.suspend();
        thread.resume();
    }
    //Output
    // 這種狀況線程會被掛起,控制檯只會顯示在掛起前打印的值,

2.3sleep()和TimeUnit

  static sleep(long millis) 的做用是當前正在執行的線程睡眠一段時間,出 CPU 讓其去執行其餘的任務,睡眠結束後獲取到時間片纔會繼續執行任務。
  調用sleep(),會拋出編譯期異常InterruptedException,你須要捕獲或者將該異常繼續上拋。sleep方法不會釋放鎖,也就是說若是當前線程持有對某個對象的鎖,其餘線程沒法訪問這個對象。因爲sleep(long millis)中控制睡眠時長的單位是毫秒級,這樣可讀性比較差,建議使用TimeUnit:

  • TimeUnit.DAYS 日的工具類
  • TimeUnit.HOURS 時的工具類
  • TimeUnit.MINUTES 分的工具類
  • TimeUnit.SECONDS 秒的工具類
  • TimeUnit.MILLISECONDS 毫秒的工具類
public void test() throws InterruptedException {
        //休眠一天
        Thread.sleep(1000*60*60*24);
        TimeUnit.DAYS.sleep(1);
    }

2.4interrupt()、isInterrupted()和Thread.interrupted()

  Java沒有提供任何機制來安全的終止線程,但它提供了中斷(Interruption),這是一種協做機制,可以使一個線程終止另外一個線程的當前工做。咱們不多但願某個任務、線程或服務當即中止,由於這種當即中止會使共享的數據結構處於不一致的狀態。經過協做的方式,咱們可讓要退出的程序清理當前正在執行的工做,而後再結束,這提供了更好的靈活性,由於任務自己的代碼比發出取消請求的代碼更清楚如何清除工做。
  Java經過協做式中斷,經過推遲中斷請求的處理,開發人員能制定更靈活的中斷策略,使程序在響應性和健壯性之間實現合理的平衡。須要使用中斷的方法都要求拋出或捕獲處理InterruptedException異常。

interrupt()
  設置中斷狀態爲true,若是線程處於就緒狀態則不會直接中斷,而是將線程狀態改成中斷狀態,須要手動去檢測線程的中斷狀態,若是線程被阻塞則能拋出InterruptedException異常,當拋出InterruptedException異常或者調用Thread.interrupted()時,中狀態將被複位。

//錯誤寫法  雖然將線程設置爲中斷狀態,但內部程序一直在執行
    public void run(){
      //線程處於就緒狀態
      while(true){
        ...
      }
    }
    thred.interrupt()

    //當線程被中斷後,會執行完當前的操做後,進入下一輪循環的時候中止
    public void run(){
      while(true){
        if(Thread.currentThread().isInterrupted()){
          System.out.println("Interrupted!");
          break;
        }
        ...
      }
    }
    thred.interrupt()

isInterrupted()
判斷線程是否被中斷

public boolean isInterrupted() {
      //不清除中斷狀態
      return isInterrupted(false);
    }

Thread.interrupted()
判斷是否被中斷,並清除當前中斷狀態,實現Runnable接口的只能調用這個方法。

public static boolean Thread.interrupted(){ 
     return currentThread().isInterrupted(true);
   }

沒法響應中斷的阻塞

  • 執行同步的SocketI/O沒法響應中斷。
  • InputStream和OutputStream的read和write等方法都不會響應中斷,但能夠經過關閉底層的套接字,使因read或write等方法被阻塞的線程拋出一個SocketException。
  • 等待得到內置鎖(synchronized)而阻塞,沒法響應中斷。但使用Lock類中提供了lockInterruptibly方法,該方法容許在等待一個鎖的同時仍能響應中斷。

2.5wait()和notify()/notifyAll()

調用這三個方法的前提是調用者持有鎖,否則會拋出IllegalMonitorStateException異常。

Object lock =new Object();
    synchronized(lock){
        lock.wait();
    }
    ...
    synchronized(lock){
        lock.notify();
        lock.notifyAll();
        ...
    }

  wait() 會釋放當前持有的鎖,讓出CPU,使線程進入等待狀態。
  notify() 喚醒一個等待該鎖的線程,而後繼續執行,直至退出臨界區(鎖住notify()的區域),鎖纔會被釋放,等待該鎖的線程才能去搶鎖。
  notifyAll() 喚醒全部在對象鎖上等待的線程。

以前展現的全部方法都是定義在Thread中的,可是這三個被定義在Object對象中。

public class Object {
        public final void wait() throws InterruptedException {
                wait(0);
        }
        public final native void notify();
        public final native void notifyAll();
        ...
    }

爲何 wait() 和notify() / notifyAll() 方法要放在同步塊中?

  在多線程環境下有著名問題「Lost wake-up」。線程進入等待狀態後,丟失了喚醒操做,致使線程永遠處於等待狀態。
假若有兩個線程,一個消費者線程,一個生產者線程。生產者線程的任務生產商品簡化爲count+1,然後喚醒消費者;消費者則是判斷有無商品,有則消費商品,無則進入等待。

10-LostWeakUp.jpg

  在消費者執行的過程當中,先判斷了count的狀態,隨後發生上下文切換,生產者執行了所有操做,執行權切換到消費者,繼續執行,但此時的count已經被修改成1,因此以前的判斷失效,消費者沒有從新判斷count狀態,就繼續執行,進入等待後沒有喚醒操做,致使無限制等待。

  問題的根源在於,消費者在檢查count到調用wait()之間,count就可能被改掉了。這就是一種最多見的競態條件「先檢查後執行(check-Then-Act)」。還有一種常見的是「讀取-修改-寫入」,爲了確保線程安全性,這類複合操做必須以原子方式執行來確保線程的安全性,在下一篇會着重介紹Java線程安全及加鎖機制。synchronized是Java提供的內置鎖,它能夠保證一組操做的原子性,應用在剛纔的場景下就是,檢查count和調用wait()之間,count不會修改。這樣才能避免「Lost Weak-up」問題的發生。所以wait()必須在同步塊中調用。

爲何 wait() 和notify() / notifyAll() 方法定義在Object而不是Thread類中?

  經過上一個問題咱們瞭解到wait和notify執行的前提是須要持有鎖,而Java中鎖能夠是任意的對象,因此這三個方法定義在Object中。

sleep() 方法和 wait() 方法區別?

  • sleep不會釋放鎖,使得線程仍然能夠同步控制;wait會釋放鎖,進入線程等待池中等待。
  • sleep能夠在任何地方使用;wait/notify/notifyAll只能在同步控制方法或者同步控制塊中使用。
  • sleep一般被用於暫停執行;wait一般被用於線程間交互。
  • sleep是Thread類中的方法;wait是Object類中的方法。
  • sleep會自動喚醒,若是想要提早喚醒,可使用interrupt方法中斷;調用wait()方法的線程,不會本身喚醒,須要線程調用notify/notifyAll方法喚醒。

2.6yeild()和join()

yeild()

public static native void yield();

  當一個線程拿到CPU時間片後,調用yeild()方法使得線程交出當前時間片,從新與擁有相同優先級或更高優先級的線程競爭,競爭成功後仍是會執行的,yield()方法不能控制具體的交出,只能讓其餘線程有更多獲取 CPU 執行時間的機會,yeild不會釋放鎖。
  yield()方法會增長髮生上下文切換的機率,併發調試的時候更容易將問題暴露出來。

sleep與yield的區別

  • sleep方法給其它線程運行機會時不考慮線程的優先級,所以會給低優先級的線程以運行的機會;yield方法只會給相同或更高優先級的線程以運行的機會。
  • sleep方法以後轉入阻塞狀態;yield方法以後轉入就緒狀態。
  • sleep方法聲明拋出InterruptedException;yield方法沒有聲明任何異常。
  • sleep方法具備更好的可移植性且sleep更容易被控制;(yield很差控制,只是瞬間放棄CPU的執行權,有可能立刻又搶回接着執行)。
  • sleep和yield方法將在當前正在執行的線程上運行,因此在其它處於等待狀態的線程上調用這些方法是沒有意義的。這就是爲何這些方法是靜態的。

join()
能夠理解爲等待一個線程執行結束,先看一個簡單的實例。

public void test() throws InterruptedException {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    try {
                        TimeUnit.SECONDS.sleep(3);
                        System.out.println("線程執行結束");
                    } catch (InterruptedException ignore) {}
                }
            };
            thread.start();
            thread.join();  
            //等待thread線程執行完成或者拋出異常,在等待期間執行這行代碼的線程是阻塞的
            System.out.println("...");  

            //Output
            // 線程執行結束     3s後打印
            // ...
        }

Java中提供了3種join的方法。

public final void join() throws InterruptedException {
                join(0);    //調用無參join則會一直等待,直至線程執行結束
            }

        public final synchronized void join(long millis)
            throws InterruptedException {
                long base = System.currentTimeMillis();
                long now = 0;

                if (millis < 0) {
                    throw new IllegalArgumentException("timeout value is negative");
                }

                if (millis == 0) {
                    while (isAlive()) {
                        wait(0);    //join的經過調用wait()實現,所以join會釋放鎖
                                    //線程執行完畢後,系統會調用notifyAll()  
                                    //所以建議不要在Thread實例上使用wait()和notify()  
                    }
                } else {
                    while (isAlive()) {
                        long delay = millis - now;
                        if (delay <= 0) {
                            break;
                        }
                        wait(delay);
                        now = System.currentTimeMillis() - base;
                    }
                }
            }

        public final synchronized void join(long millis, int nanos)
                                              throws InterruptedException {...

Reference

  《Java 併發編程實戰》
  《Java 編程思想(第4版)》
  https://blog.csdn.net/justlov...
  https://blog.csdn.net/qq_3550...

感謝閱讀!

相關文章
相關標籤/搜索