多線程方法詳解

在這裏插入圖片描述
參考:https://juejin.im/post/5ccfc04051882540ab167f86

1.New:       新建態:  new Thread ~ thread.start期間
2.Runnable:  可執行態: 可被CPU調度執行期間。
3.Running     運行態: 線程獲取CPU權限進行執行
4.Blocked     阻塞狀態: 阻塞狀態是線程因爲某種原因放棄CPU使用權,暫時停止運行。
    |---等待阻塞:通過調用線程的wait()方法,讓線程等待某工作的完成。
    |---同步阻塞:線程在獲取synchronized同步鎖失敗(因爲鎖被其它線程所佔用)|---其他阻塞:通過調用線程的sleep()join()或發出了I/O請求時
5.Dead        死亡狀態: 線程執行完了或者因異常退出了run()方法,該線程結束生命週期。

1.wait() 與 notify/notifyAll 方法必須在同步代碼塊中使用

wait() 與 notify/notifyAll() 是Object類的方法,在執行兩個方法時,要先獲得鎖。那麼怎麼獲得鎖呢?

  • synchronized 關鍵字鎖住的是當前對象。這也是稱爲對象鎖的原因。
  • wait() 與 notify/notifyAll() 經常`與synchronized搭配使用`,即在synchronized修飾的同步代碼塊或方法裏面調用wait() 與 notify/notifyAll()方法。
  • 參考:https://www.cnblogs.com/hapjin/p/5452663.html

    2.wait() 與 notify/notifyAll() 的執行過程

    由於 wait() 與 notify/notifyAll() 是放在同步代碼塊中的,因此線程在執行它們時,肯定是進入了臨界區中的,即該線程肯定是獲得了鎖的。

  • 當線程執行wait()時,會把當前的鎖釋放,然後讓出CPU,進入等待狀態。
  • 當執行notify/notifyAll方法時,會喚醒一個處於等待該 對象鎖 的線程,然後繼續往下執行,直到執行完退出對象鎖鎖住的區域(synchronized修飾的代碼塊)後再釋放鎖

  • 3.sleep()和yield()的區別

      yield()應該做的是讓當前運行線程回到可運行狀態,以允許具有相同優先級的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因爲讓步的線程還有可能被線程調度程序再次選中。
      yield操作並不會永遠放棄CPU,僅僅只是放棄了此次時間片把剩下的時間讓給別的線程

  • sleep()使當前線程進入停滯狀態,所以執行sleep()的線程在指定的時間內肯定不會被執行;
  • yield()只是使當前線程重新回到可執行狀態,所以執行yield()的線程有可能在進入到可執行狀態後馬上又被執行;
  • sleep 方法使當前運行中的線程睡眼一段時間,進入不可運行狀態,這段時間的長短是由程序設定的
  • yield 方法使當前線程讓出 CPU 佔有權,但`讓出的時間是不可設定的`。實際上,yield()方法對應瞭如下操作:先檢測當前是否有相同優先級的線程處於同可運行狀態,如有,則把 CPU 的佔有權交給此線程,否則,繼續運行原來的線程。所以yield()方法稱爲“退讓”,它把運行機會讓給了同等優先級的其他線程;
  • sleep 方法允許較低優先級的線程獲得運行機會
  • yield() 方法執行時,當前線程仍處在可運行狀態,所以,不可能讓出較低優先級的線程些時獲得 CPU 佔有權。
  •   在一個運行系統中,如果較高優先級的線程沒有調用 sleep 方法,又沒有受到 I\O 阻塞,那麼,較低優先級線程只能等待所有較高優先級的線程運行結束,纔有機會運行。


    4.wait和sleep區別

    • sleep()方法
        sleep()使當前線程進入停滯狀態(阻塞當前線程),讓出CUP的使用、目的是不讓當前線程獨自霸佔該進程所獲的CPU資源,以留一定時間給其他線程執行的機會;
         sleep()是Thread類的Static(靜態)的方法;因此他不能改變對象的機鎖,所以當在一個Synchronized塊中調用Sleep()方法是,線程雖然休眠了,但是對象的機鎖並木有被釋放,其他線程無法訪問這個對象(即使睡着也持有對象鎖)。
        在sleep()休眠時間期滿後,該線程不一定會立即執行,這是因爲其它線程可能正在運行而且沒有被調度爲放棄執行,除非此線程具有更高的優先級。
        Thread.Sleep(0)的作用,就是“觸發操作系統立刻重新進行一次CPU競爭”。競爭的結果也許是當前線程仍然獲得CPU控制權,也許會換成別的線程獲得CPU控制權。

    • wait()方法
        wait()方法是Object類裏的方法;當一個線程執行到wait()方法時,它就進入到一個和該對象相關的等待池中,同時失去(釋放)了對象的機鎖(暫時失去機鎖,wait(long timeout)超時時間到後還需要返還對象鎖);其他線程可以訪問;
        wait()使用notify或者notifyAlll或者指定睡眠時間來喚醒當前等待池中的線程。
        wiat()必須放在synchronized block中,否則會在runtime時扔出”java.lang.IllegalMonitorStateException“異常。

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

    1. 如果線程A希望立即結束線程B,則可以對線程B對應的Thread實例調用interrupt方法。如果此刻線程B正在wait/sleep /join,則線程B會立刻拋出InterruptedException,在catch() {} 中直接return即可安全地結束線程。
    2. 需要注意的是,InterruptedException是線程自己從內部拋出的,並不是interrupt()方法拋出的。對某一線程調用 interrupt()時,如果該線程正在執行普通的代碼,那麼該線程根本就不會拋出InterruptedException。但是,一旦該線程進入到 wait()/sleep()/join()後,就會立刻拋出InterruptedException 。
    • 不同點:
      ① Thread類的方法:sleep(),yield()等; Object的方法:wait()和notify()等
      ② 每個對象都有一個鎖來控制同步訪問。Synchronized關鍵字可以和對象的鎖交互,來實現線程的同步。 sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他線程可以使用同步控制塊或者方法。
      ③ wait,notify和notifyAll只能在同步控制方法或者同步控制塊裏面使用,而sleep可以在任何地方使用
      所以sleep()和wait()方法的最大區別是:
          sleep()睡眠時,保持對象鎖,仍然佔有該鎖;
          而wait()睡眠時,釋放對象鎖。
      但是wait()和sleep()都可以通過interrupt()方法打斷線程的暫停狀態,從而使線程立刻拋出InterruptedException(但不建議使用該方法)。

    5.如何正確地停止一個線程?

      停止一個線程意味着在任務處理完任務之前停掉正在做的操作,也就是放棄當前的操作。停止一個線程可以用Thread.stop()方法,但最好不要用它。雖然它確實可以停止一個正在運行的線程,但是這個方法是不安全的,而且是已被廢棄的方法。
    在java中有以下3種方法可以終止正在運行的線程:

    • 使用退出標誌,使線程正常退出,也就是當run方法完成後線程終止。
    • 使用stop方法強行終止,但是不推薦這個方法,因爲stop和suspend及resume一樣都是過期作廢的方法。
      調用stop()方法時會拋出java.lang.ThreadDeath異常
      ①如果強制讓線程停止有可能使一些清理性的工作得不到完成;
      ②對鎖定的對象進行了解鎖,導致數據得不到同步的處理,出現數據不一致的問題;
    • 使用interrupt方法中斷線程。
      調用interrupt方法是在當前線程中打了一個停止標誌並不是真的停止線程

    ① Thread.java類中提供了兩種方法:
    this.interrupted(): 測試當前線程是否已經中斷,並且清除線程的中斷狀態爲false
    this.isInterrupted(): 測試線程是否已經中斷,但是不清除線程的中斷狀態

    異常法
    interrupted()判斷是否中斷,並拋出異常InterruptedException

    public class MyThread extends Thread {
        public void run(){
            super.run();
            try {
                for(int i=0; i<500000; i++){
                    if(this.interrupted()) {
                        System.out.println("線程已經終止, for循環不再執行");
                            throw new InterruptedException();
                    }
                    System.out.println("i="+(i+1));
                }
    
                System.out.println("這是for循環外面的語句,也會被執行");
            } catch (InterruptedException e) {
                System.out.println("進入MyThread.java類中的catch了。。。");
                e.printStackTrace();
            }
        }
    }
    public class Run {
        public static void main(String args[]){
            Thread thread = new MyThread();
            thread.start();
            thread.interrupt();
        }
    }
    

    ③線程在sleep()狀態下停止線程interrupt
    在sleep狀態下停止某一線程,會進入catch語句,並且清除停止狀態值,使之變爲false。

    ④ 使用return停止線程
    將方法isInterrupt()與return結合使用也能實現停止線程的效果:

    public class MyThread extends Thread {
        public void run(){
            while (true){
                if(this.isInterrupted()){
                    System.out.println("線程被停止了!");
                    return;
                }
                System.out.println("Time: " + System.currentTimeMillis());
            }
        }
    }
    
    public class Run {
        public static void main(String args[]) throws InterruptedException {
            Thread thread = new MyThread();
            thread.start();
            Thread.sleep(2000);
            thread.interrupt();
        }
    }
    

    建議使用“拋異常”的方法來實現線程的停止,因爲在catch塊中還可以將異常向上拋,使線程停止事件得以傳播。


    6.如何控制某個方法允許併發訪問線程的大小?

    參考:https://zhuanlan.zhihu.com/p/73151550

      Semaphore兩個重要的方法就是semaphore.acquire() 請求一個信號量,這時候的信號量個數-1(一旦沒有可使用的信號量,也即信號量個數變爲負數時,再次請求的時候就會阻塞,直到其他線程釋放了信號量)semaphore.release()釋放一個信號量,此時信號量個數+1

    public class SemaphoreTest {  
        private Semaphore mSemaphore = new Semaphore(5);  
        public void run(){  
            for(int i=0; i< 100; i++){  
                new Thread(new Runnable() {  
                    @Override  
                    public void run() {  
                        test();  
                    }  
                }).start();  
            }  
        }  
      
        private void test(){  
            try {  
                mSemaphore.acquire();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            System.out.println(Thread.currentThread().getName() + " 進來了");  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            System.out.println(Thread.currentThread().getName() + " 出去了");  
            mSemaphore.release();  
        }  
    }
    

    7.Thread.join的作用和原理

    • Thread.join的作用

    Java中如何讓多線程按照自己指定的順序執行?

    這個問題最簡單的回答是通過Thread.join來實現,久而久之就讓很多人誤以爲Thread.join是用來保證線程的順序性的。
    下面這段代碼演示了Thread.join的作用

    public class JoinDemo extends Thread{
        int i;
        Thread previousThread; //上一個線程
        public JoinDemo(Thread previousThread,int i){
            this.previousThread=previousThread;
            this.i=i;
        }
        @Override
        public void run() {
            try {
              //調用上一個線程的join方法,大家可以自己演示的時候可以把這行代碼註釋掉
                previousThread.join(); 
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("num:"+i);
        }
        public static void main(String[] args) {
            Thread previousThread=Thread.currentThread();
            for(int i=0;i<10;i++){
                JoinDemo joinDemo=new JoinDemo(previousThread,i);
                joinDemo.start();
                previousThread=joinDemo;
            }
        }
    }
    
    • Thread.join的實現原理
    public class Thread implements Runnable {
        ...
        public final void join() throws InterruptedException {
            join(0);
        }
        ...
        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) { //判斷是否攜帶阻塞的超時時間,等於0表示沒有設置超時時間
                while (isAlive()) {//isAlive獲取線程狀態,無線等待直到previousThread線程結束
                    wait(0); //調用Object中的wait方法實現線程的阻塞
                }
            } else { //阻塞直到超時
                while (isAlive()) { 
                    long delay = millis - now;
                    if (delay <= 0) {
                        break;
                    }
                    wait(delay);
                    now = System.currentTimeMillis() - base;
                }
            }
        }...
    

    參考:https://segmentfault.com/a/1190000017255019

    總結,Thread.join其實底層是通過wait/notifyall來實現線程的通信達到線程阻塞的目的;當線程執行結束以後,會觸發兩個事情,第一個是設置native線程對象爲null、第二個是通過notifyall方法,讓等待在previousThread對象鎖上的wait方法被喚醒。

    8.線程池原理

    在這裏插入圖片描述

    • 核心線程(corePool):
      線程池最終執行任務的角色肯定還是線程,同時我們也會限制線程的數量,所以我們可以這樣理解核心線程,有新任務提交時,首先檢查覈心線程數,如果核心線程都在工作,而且數量也已經達到最大核心線程數,則不會繼續新建核心線程,而會將任務放入等待隊列。
    • 等待隊列 (workQueue):
      等待隊列用於存儲當核心線程都在忙時,繼續新增的任務,核心線程在執行完當前任務後,也會去等待隊列拉取任務繼續執行,這個隊列一般是一個線程安全的阻塞隊列,它的容量也可以由開發者根據業務來定製。
    • 非核心線程:
      當等待隊列滿了,如果當前線程數沒有超過最大線程數,則會新建線程執行任務,那麼核心線程和非核心線程到底有什麼區別呢?說出來你可能不信,本質上它們沒有什麼區別,創建出來的線程也根本沒有標識去區分它們是核心還是非核心的,線程池只會去判斷已有的線程數(包括核心和非核心)去跟核心線程數和最大線程數比較,來決定下一步的策略。
    • 線程活動保持時間 (keepAliveTime):
      線程空閒下來之後,保持存活的持續時間,超過這個時間還沒有任務執行,該工作線程結束。
    • 飽和策略 (RejectedExecutionHandler):
      當等待隊列已滿,線程數也達到最大線程數時,線程池會根據飽和策略來執行後續操作,默認的策略是拋棄要加入的任務。

    關於線程池的狀態,有5種,

    1. RUNNING, 運行狀態,值也是最小的,剛創建的線程池就是此狀態。
    2. SHUTDOWN,停工狀態,不再接收新任務,已經接收的會繼續執行
    3. STOP,停止狀態,不再接收新任務,已經接收正在執行的,也會中斷
    4. 清空狀態,所有任務都停止了,工作的線程也全部結束了
    5. TERMINATED,終止狀態,線程池已銷燬

    它們的流轉關係如下:
    在這裏插入圖片描述
    參考:
    https://juejin.im/post/5c8896be5188257ec828072f
    https://www.jianshu.com/p/098819be088c

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