Java學習筆記(五):線程

foochanehttps://foochane.cn/article/2019123002.html

1 多線程

我們在之前,學習的程序在沒有跳轉語句的前提下,都是由上至下依次執行,那現在想要設計一個程序,邊打游戲邊聽歌,怎么設計?

要解決上述問題,咱們得使用多進程或者多線程來解決.

1.1 并發與并行

  • 并發:指兩個或多個事件在同一個時間段內發生。
  • 并行:指兩個或多個事件在同一時刻發生(同時發生)。

[圖片上傳失敗...(image-4afbb6-1580720568911)]

在操作系統中,安裝了多個程序,并發指的是在一段時間內宏觀上有多個程序同時運行,這在單 CPU 系統中,每一時刻只能有一道程序執行,即微觀上這些程序是分時的交替運行,只不過是給人的感覺是同時運行,那是因為分時交替運行的時間是非常短的。

而在多個 CPU 系統中,則這些可以并發執行的程序便可以分配到多個處理器上(CPU),實現多任務并行執行,即利用每個處理器來處理一個可以并發執行的程序,這樣多個程序便可以同時執行。目前電腦市場上說的多核 CPU,便是多核處理器,核越多,并行處理的程序越多,能大大的提高電腦運行的效率。

注意:單核處理器的計算機肯定是不能并行的處理多個任務的,只能是多個任務在單個CPU上并發運行。同理,線程也是一樣的,從宏觀角度上理解線程是并行運行的,但是從微觀角度上分析卻是串行運行的,即一個線程一個線程的去運行,當系統只有一個CPU時,線程會以某種順序執行多個線程,我們把這種情況稱之為線程調度。

1.2 線程與進程

  • 進程:是指一個內存中運行的應用程序,每個進程都有一個獨立的內存空間,一個應用程序可以同時運行多個進程;進程也是程序的一次執行過程,是系統運行程序的基本單位;系統運行一個程序即是一個進程從創建、運行到消亡的過程。

  • 線程:線程是進程中的一個執行單元,負責當前進程中程序的執行,一個進程中至少有一個線程。一個進程中是可以有多個線程的,這個應用程序也可以稱之為多線程程序。

    簡而言之:一個程序運行后至少有一個進程,一個進程中可以包含多個線程

我們可以再電腦底部任務欄,右鍵----->打開任務管理器,可以查看當前任務的進程:

進程

[圖片上傳失敗...(image-ff8a13-1580720568911)]

線程

[圖片上傳失敗...(image-b72516-1580720568911)]

線程調度:

  • 分時調度

    所有線程輪流使用 CPU 的使用權,平均分配每個線程占用 CPU 的時間。

  • 搶占式調度

    優先讓優先級高的線程使用 CPU,如果線程的優先級相同,那么會隨機選擇一個(線程隨機性),Java使用的為搶占式調度。

    • 設置線程的優先級

    [圖片上傳失敗...(image-77c44e-1580720568911)]

    • 搶占式調度詳解

      大部分操作系統都支持多進程并發運行,現在的操作系統幾乎都支持同時運行多個程序。比如:現在我們上課一邊使用編輯器,一邊使用錄屏軟件,同時還開著畫圖板,dos窗口等軟件。此時,這些程序是在同時運行,”感覺這些軟件好像在同一時刻運行著“。

      實際上,CPU(中央處理器)使用搶占式調度模式在多個線程間進行著高速的切換。對于CPU的一個核而言,某個時刻,只能執行一個線程,而 CPU的在多個線程間切換速度相對我們的感覺要快,看上去就是在同一時刻運行。
      其實,多線程程序并不能提高程序的運行速度,但能夠提高程序運行效率,讓CPU的使用率更高。

      [圖片上傳失敗...(image-e3f872-1580720568911)]

1.3 使用Thread類創建線程

翻閱API后得知創建線程的方式總共有兩種,一種是繼承Thread類方式,一種是實現Runnable接口方式。

Java使用java.lang.Thread類代表線程,所有的線程對象都必須是Thread類或其子類的實例。每個線程的作用是完成一定的任務,實際上就是執行一段程序流即一段順序執行的代碼。Java使用線程執行體來代表這段程序流。Java中通過繼承Thread類來創建啟動多線程的步驟如下:

  1. 定義Thread類的子類,并重寫該類的run()方法,該run()方法的方法體就代表了線程需要完成的任務,因此把run()方法稱為線程執行體。
  2. 創建Thread子類的實例,即創建了線程對象
  3. 調用線程對象的start()方法來啟動該線程

代碼如下:

自定義線程類:

public class MyThread extends Thread {
    //定義指定線程名稱的構造方法
    public MyThread(String name) {
        //調用父類的String參數的構造方法,指定線程的名稱
        super(name);
    }
    /**
     * 重寫run方法,完成該線程執行的邏輯
     */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName()+":正在執行!"+i);
        }
    }
}

測試類:

public class Demo01 {
    public static void main(String[] args) {
        //創建自定義線程對象
        MyThread mt = new MyThread("新的線程!");
        //開啟新線程
        mt.start();
        //在主方法中執行for循環
        for (int i = 0; i < 10; i++) {
            System.out.println("main線程!"+i);
        }
    }
}

Thread 類的常用方法:

java.lang.Thread 類的API中定義了有關線程的一些方法,具體如下:

構造方法:

  • public Thread() :分配一個新的線程對象。
  • public Thread(String name):分配一個指定名字的新的線程對象。
  • public Thread(Runnable target) :分配一個帶有指定目標新的線程對象。
  • public Thread(Runnable target,String name) :分配一個帶有指定目標新的線程對象并指定名字。

常用方法:

  • public String getName() :獲取當前線程名稱。
  • public void start() :導致此線程開始執行; Java虛擬機調用此線程的run方法
  • public void run():此線程要執行的任務在此處定義代碼。
  • public static void sleep(long millis) :使當前正在執行的線程以指定的毫秒數暫停(暫時停止執行)。
  • public static Thread currentThread() :返回對當前正在執行的線程對象的引用。

1.4 使用Runnable接口創建線程

采用 java.lang.Runnable 也是非常常見的一種,我們只需要重寫run方法即可。
步驟如下:

  1. 定義Runnable接口的實現類,并重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。
  2. 創建Runnable實現類的實例,并以此實例作為Thread的target來創建Thread對象,該Thread對象才是真正
    的線程對象。
  3. 調用線程對象的start()方法來啟動線程。

代碼如下:

定義Runnable接口的實現類

public class MyRunnable implements Runnable{
    @Override    
    public void run() {    
        for (int i = 0; i < 20; i++) {        
            System.out.println(Thread.currentThread().getName()+" "+i);            
        }        
    }    
}

測試類

public class Demo {
    public static void main(String[] args) {
        //創建自定義類對象  線程任務對象
        MyRunnable mr = new MyRunnable();
        //創建線程對象
        Thread t = new Thread(mr, "小強");
        t.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("旺財 " + i);
        }
    }
}

通過實現 Runnable接口,使得該類有了多線程類的特征。run()方法是多線程程序的一個執行目標。所有的多線程代碼都在run方法里面。Thread類實際上也是實現了Runnable接口的類。

在啟動的多線程的時候,需要先通過Thread類的構造方法Thread(Runnable target) 構造出對象,然后調用Thread對象的start()方法來運行多線程代碼。

實際上所有的多線程代碼都是通過運行Thread的start()方法來運行的。因此,不管是繼承Thread類還是實現Runnable接口來實現多線程,最終還是通過Thread的對象的API來控制線程的,熟悉Thread類的API是進行多線程編程的基礎。

tips:

Runnable對象僅僅作為Thread對象的target,Runnable實現類里包含的run()方法僅作為線程執行體。而實際的線程對象依然是Thread實例,只是該Thread線程負責執行其target的run()方法。

1.5 Thread 和Runnable的區別

如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable接口的話,則很容易的實現資源共享。

總結:

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

  1. 適合多個相同的程序代碼的線程去共享同一個資源。
  2. 可以避免java中的單繼承的局限性。
  3. 增加程序的健壯性,實現解耦操作,代碼可以被多個線程共享,代碼和線程獨立。線程池只能放入實現Runable或Callable類線程,不能直接放入繼承Thread的類。

擴充:在java中,每次程序運行至少啟動2個線程。一個是main線程,一個是垃圾收集線程。因為每當使用java命令執行一個類的時候,實際上都會啟動一個JVM,每一個JVM其實在就是在操作系統中啟動了一個進程。

1.6 匿名內部類方式實現線程的創建

使用線程的內匿名內部類方式,可以方便的實現每個線程執行不同的線程任務操作。使用匿名內部類的方式實現Thread類和Runnable接口,重寫的run方法:

  public class NoNameInnerClassThread {
    public static void main(String[] args) {
        //1 線程的父類是Thread
        // new MyThread().start();
        new Thread(){
            //重寫run方法,設置線程任務
            @Override
            public void run() {
                for (int i = 0; i <20 ; i++) {
                    System.out.println(Thread.currentThread().getName()+"-->"+"aaa");
                }
            }
        }.start();

        //2 線程的接口Runnable
        //Runnable r = new RunnableImpl();//多態
        Runnable r = new Runnable(){
            //重寫run方法,設置線程任務
            @Override
            public void run() {
                for (int i = 0; i <20 ; i++) {
                    System.out.println(Thread.currentThread().getName()+"-->"+"bbb");
                }
            }
        };
        new Thread(r).start();

        //簡化接口的方式
        new Thread(new Runnable(){
            //重寫run方法,設置線程任務
            @Override
            public void run() {
                for (int i = 0; i <20 ; i++) {
                    System.out.println(Thread.currentThread().getName()+"-->"+"ccc");
                }
            }
        }).start();
    }
}

1.7 多線程原理

分析如下代碼:

自定義線程類:

 public class MyThread extends Thread{
/*    
 * 利用繼承中的特點     
 *   將線程名稱傳遞  進行設置    
 */    
public MyThread(String name){    
    super(name);        
}    
/*    
 * 重寫run方法    
 *  定義線程要執行的代碼    
 */    
public void run(){           
        for (int i = 0; i < 20; i++) { 
            //getName()方法 來自父親            
            System.out.println(getName()+i);            
        }        
    }    
}

測試類:

public class Demo {
    public static void main(String[] args) {
        System.out.println("這里是main線程");  
        MyThread mt = new MyThread("小強");            
        mt.start();//開啟了一個新的線程    
         for (int i = 0; i < 20; i++) {    
            System.out.println("旺財:"+i);            
         }        
    }    
} 

下面畫個多線程執行時序圖來體現一下多線程程序的執行流程。

流程圖:

[圖片上傳失敗...(image-9dfe1a-1580720568911)]

程序啟動運行 main時候,java虛擬機啟動一個進程,主線程main在main()調用時候被創建。隨著調用mt的對象的start方法,另外一個新的線程也啟動了,這樣,整個應用就在多線程下運行。通過這張圖我們可以很清晰的看到多線程的執行流程,那么為什么可以完成并發執行呢?我們再來講一講原理。

多線程執行時,到底在內存中是如何運行的呢?以上個程序為例,進行圖解說明:多線程執行時,在棧內存中,其實每一個執行線程都有一片自己所屬的棧內存空間。進行方法的壓棧和彈棧。

[圖片上傳失敗...(image-75c3ee-1580720568911)]

當執行線程的任務結束了,線程自動在棧內存中釋放了。但是當所有的執行線程都結束了,那么進程就結束了。

2 線程安全

2.1 線程安全

如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。

我們通過一個案例,演示線程的安全問題:

電影院要賣票,我們模擬電影院的賣票過程。假設要播放的電影是 “葫蘆娃大戰奧特曼”,本次電影的座位共100個(本場電影只能賣100張票)。

我們來模擬電影院的售票窗口,實現多個窗口同時賣 “葫蘆娃大戰奧特曼”這場電影票(多個窗口一起賣這100張票)

需要窗口,采用線程對象來模擬;需要票,Runnable接口子類來模擬

模擬票:

public class Ticket implements Runnable {
    private int ticket = 100;
    /*
     * 執行賣票操作
     */
    @Override
    public void run() {
        //每個窗口賣票的操作
        //窗口 永遠開啟
        while (true) {
            if (ticket > 0) {//有票 可以賣
                //出票操作
                //使用sleep模擬一下出票時間
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    // TODO Auto‐generated catch block
                    e.printStackTrace();
                }
                //獲取當前線程對象的名字
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在賣:" + ticket--);
            }
        }
    }
}

測試類:

public class Demo {
    public static void main(String[] args) {    
        //創建線程任務對象        
        Ticket ticket = new Ticket();        
        //創建三個窗口對象        
        Thread t1 = new Thread(ticket, "窗口1");        
        Thread t2 = new Thread(ticket, "窗口2");        
        Thread t3 = new Thread(ticket, "窗口3");        

        //同時賣票        
        t1.start();        
        t2.start();        
        t3.start();        
    }    
}

運行的異常結果:

窗口3正在賣:100
窗口2正在賣:-1
窗口1正在賣:0
窗口2正在賣:100

發現程序出現了兩個問題:

  1. 相同的票數,比如100這張票被賣了兩回。
  2. 不存在的票,比如0票與-1票,是不存在的。

這種問題,幾個窗口(線程)票數不同步了,這種問題稱為線程不安全。

線程安全問題都是由全局變量及靜態變量引起的。若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。

當我們使用多個線程訪問同一資源的時候,且多個線程中對資源有寫的操作,就容易出現線程安全問題。

要解決上述多線程并發訪問一個資源的安全性問題:也就是解決重復票與不存在票問題,Java中提供了同步機制(synchronized)來解決。

為了保證每個線程都能正常執行原子操作,Java引入了線程同步機制。
那么怎么去使用呢?有三種方式完成同步操作:

  1. 同步代碼塊。

  2. 同步方法。

  3. 鎖機制。

3.2 同步代碼塊

同步代碼塊 : synchronized 關鍵字可以用于方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。

格式:

synchronized(同步鎖){
     需要同步操作的代碼
}

同步鎖:

對象的同步鎖只是一個概念,可以想象為在對象上標記了一個鎖.

  1. 鎖對象 可以是任意類型。
  2. 多個線程對象 要使用同一把鎖。

注意:在任何時候,最多允許一個線程擁有同步鎖,誰拿到鎖就進入代碼塊,其他的線程只能在外等著(BLOCKED)。

使用同步代碼塊解決代碼:

public class Ticket implements Runnable {
    private int ticket = 100;

    //創建一個鎖對象
    Object lock = new Object();

    /*
     * 執行賣票操作
     */
    @Override
    public void run() {
        //每個窗口賣票的操作
        //窗口 永遠開啟
        while (true) {
            synchronized (lock) {
                if (ticket > 0) {//有票 可以賣
                    //出票操作
                    //使用sleep模擬一下出票時間
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto‐generated catch block
                        e.printStackTrace();
                    }
                    //獲取當前線程對象的名字
                    String name = Thread.currentThread().getName();
                    System.out.println(name + "正在賣:" + ticket--);
                }
            }
        }
    }
}

當使用了同步代碼塊后,上述的線程的安全問題,解決了。

3.3 同步方法

同步方法 :使用synchronized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程只能在方法外等著。

格式:

public synchronized void method(){
   可能會產生線程安全問題的代碼 
}

同步鎖是誰?
對于非static方法,同步鎖就是this。
對于static方法,我們使用當前方法所在類的字節碼對象(類名.class)。

使用同步方法代碼如下:

public class Ticket implements Runnable{
    private int ticket = 100;
    /*
     * 執行賣票操作
     */
    @Override
    public void run() {
        //每個窗口賣票的操作
        //窗口 永遠開啟
        while(true){
            sellTicket();
        }
    }

    /*
     * 鎖對象是誰調用這個方法就是誰
     *   隱含鎖對象就是this
     *   相當于synchronized (this){}
     */
    public synchronized void sellTicket(){
        if(ticket>0){//有票 可以賣
            //出票操作
            //使用sleep模擬一下出票時間
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto‐generated catch block
                e.printStackTrace();
            }
            //獲取當前線程對象的名字
            String name = Thread.currentThread().getName();
            System.out.println(name+"正在賣:"+ticket--);
        }
    }
}

3.4 Lock 鎖

java.util.concurrent.locks.Lock機制提供了比synchronized代碼塊和synchronized方法更廣泛的鎖定操作,同步代碼塊/同步方法具有的功能Lock都有,除此之外更強大,更體現面向對象。

Lock鎖也稱同步鎖,加鎖與釋放鎖方法化了,如下:

  • public void lock() :加同步鎖。

  • public void unlock() :釋放同步鎖。

使用如下

public class Ticket implements Runnable{
    private int ticket = 100;

    Lock lock = new ReentrantLock();
    /*
     * 執行賣票操作
     */
    @Override
    public void run() {
        //每個窗口賣票的操作
        //窗口 永遠開啟
        while(true){
            lock.lock();
            if(ticket>0){//有票 可以賣
                //出票操作
                //使用sleep模擬一下出票時間
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    // TODO Auto‐generated catch block
                    e.printStackTrace();
                }
                //獲取當前線程對象的名字
                String name = Thread.currentThread().getName();
                System.out.println(name+"正在賣:"+ticket--);
            }
            lock.unlock();
            
//            //另一種寫法,將unlock放在finally里面
//            lock.lock();
//            if(ticket>0){//有票 可以賣
//                //出票操作
//                //使用sleep模擬一下出票時間
//                try {
//                    Thread.sleep(10);
//                    //獲取當前線程對象的名字
//                    String name = Thread.currentThread().getName();
//                    System.out.println(name+"正在賣:"+ticket--);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                } finally {
//                    lock.unlock();
//                }
//            }
//        }
        }
    }
}

3 線程狀態

3.1 線程狀態概述

當線程被創建并啟動以后,它既不是一啟動就進入了執行狀態,也不是一直處于執行狀態。在線程的生命周期中,有幾種狀態呢?在API中 java.lang.Thread.State 這個枚舉中給出了六種線程狀態:

這里先列出各個線程狀態發生的條件,下面將會對每種狀態進行詳細解析

線程狀態 導致狀態發生條件
NEW(新建) 線程剛被創建,但是并未啟動。還沒調用start方法。
Runnable(可運行) 線程可以在java虛擬機中運行的狀態,可能正在運行自己代碼,也可能沒有,這取決于操作系統處理器。
Blocked(鎖阻塞) 當一個線程試圖獲取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked狀態;當該線程持有鎖時,該線程將變成Runnable狀態。
Waiting(無限等待) 一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀態。進入這個狀態后是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒。
Timed Waiting(計時等待) 同waiting狀態,有幾個方法有超時參數,調用他們將進入Timed Waiting狀態。這一狀態將一直保持到超時期滿或者接收到喚醒通知。帶有超時參數的常用方法有Thread.sleep 、Object.wait。
Teminated(被終止) 因為run方法正常退出而死亡,或者因為沒有捕獲的異常終止了run方法而死亡。

我們不需要去研究這幾種狀態的實現原理,我們只需知道在做線程操作中存在這樣的狀態。那我們怎么去理解這幾個狀態呢,新建與被終止還是很容易理解的,我們就研究一下線程從Runnable(可運行)狀態與非運行狀態之間的轉換問題。

3.2 Timed Waiting (計時等待)

Timed Waiting在API中的描述為:一個正在限時等待另一個線程執行一個(喚醒)動作的線程處于這一狀態。單獨的去理解這句話,真是玄之又玄,其實我們在之前的操作中已經接觸過這個狀態了,在哪里呢?

在我們寫賣票的案例中,為了減少線程執行太快,現象不明顯等問題,我們在run方法中添加了sleep語句,這樣就強制當前正在執行的線程休眠(暫停執行),以“減慢線程”。

其實當我們調用了sleep方法之后,當前執行的線程就進入到“休眠狀態”,其實就是所謂的Timed Waiting(計時等待),那么我們通過一個案例加深對該狀態的一個理解。

實現一個計數器,計數到100,在每個數字之間暫停1秒,每隔10個數字出一個字符串

代碼:

 public class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i) % 10 == 0) {
                System.out.println("‐‐‐‐‐‐‐" + i);
                }
            System.out.print(i);
            try {
                Thread.sleep(1000);
               System.out.print("    線程睡眠1秒!\n");  
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        new MyThread().start();
    }
}

通過案例可以發現, sleep方法的使用還是很簡單的。我們需要記住下面幾點:

  1. 進入 TIMED_WAITING 狀態的一種常見情形是調用的 sleep 方法,單獨的線程也可以調用,不一定非要有協
    作關系。
  2. 為了讓其他線程有機會執行,可以將Thread.sleep()的調用放線程run()之內。這樣才能保證該線程執行過程
    中會睡眠
  3. sleep與鎖無關,線程睡眠到期自動蘇醒,并返回到Runnable(可運行)狀態。

小提示:sleep()中指定的時間是線程不會運行的最短時間。因此,sleep()方法不能保證該線程睡眠到期后就
開始立刻執行。

Timed Waiting 線程狀態圖:

[圖片上傳失敗...(image-4896a9-1580720568911)]

3.3 BLOCKED (鎖阻塞)

Blocked 狀態在API中的介紹為:一個正在阻塞等待一個監視器鎖(鎖對象)的線程處于這一狀態。

我們已經學完同步機制,那么這個狀態是非常好理解的了。比如,線程A與線程B代碼中使用同一鎖,如果線程A獲取到鎖,線程A進入到Runnable狀態,那么線程B就進入到Blocked鎖阻塞狀態。

這是由Runnable狀態進入Blocked狀態。除此Waiting以及Time Waiting狀態也會在某種情況下進入阻塞狀態,而這部分內容作為擴充知識點帶領大家了解一下。

Blocked 線程狀態圖

[圖片上傳失敗...(image-84a5a7-1580720568911)]

3.4 Waiting (無限等待)

Wating狀態在API中介紹為:一個正在無限期等待另一個線程執行一個特別的(喚醒)動作的線程處于這一狀態。

那么我們之前遇到過這種狀態嗎?答案是并沒有,但并不妨礙我們進行一個簡單深入的了解。我們通過一段代碼來學習一下:

  public class WaitingTest {
    public static Object obj = new Object();
    public static void main(String[] args) {
        // 演示waiting
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    synchronized (obj){
                        try {
                            System.out.println( Thread.currentThread().getName() +"=== 獲取到鎖對象,調用wait方法,進入waiting狀態,釋放鎖對象");
                            obj.wait();  //無限等待
                            //obj.wait(5000); //計時等待, 5秒 時間到,自動醒來
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println( Thread.currentThread().getName() + "=== 從waiting狀態醒來,獲取到鎖對象,繼續執行了");
                    }
                }
            }
        },"等待線程").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
//                while (true){   //每隔3秒 喚醒一次
                    try {
                        System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 等待3秒鐘");
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj){
                        System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 獲取到鎖對象,調用notify方法,釋放鎖對象");
                        obj.notify();
                    }
                }
//            }
        },"喚醒線程").start();
    }
}
                                               

通過上述案例我們會發現,一個調用了某個對象的 Object.wait 方法的線程會等待另一個線程調用此對象的Object.notify()方法 或 Object.notifyAll()方法。

其實waiting狀態并不是一個線程的操作,它體現的是多個線程間的通信,可以理解為多個線程之間的協作關系,多個線程會爭取鎖,同時相互之間又存在協作關系。就好比在公司里你和你的同事們,你們可能存在晉升時的競爭,但更多時候你們更多是一起合作以完成某些任務。

當多個線程協作時,比如A,B線程,如果A線程在Runnable(可運行)狀態中調用了wait()方法那么A線程就進入了Waiting(無限等待)狀態,同時失去了同步鎖。假如這個時候B線程獲取到了同步鎖,在運行狀態中調用了notify()方法,那么就會將無限等待的A線程喚醒。注意是喚醒,如果獲取到鎖對象,那么A線程喚醒后就進入Runnable(可運行)狀態;如果沒有獲取鎖對象,那么就進入到Blocked(鎖阻塞狀態)。

Waiting 線程狀態圖

[圖片上傳失敗...(image-16ee09-1580720568911)]

3.5 補充知識點

到此為止我們已經對線程狀態有了基本的認識,想要有更多的了解,詳情可以見下圖:

[圖片上傳失敗...(image-a962c0-1580720568912)]

tips:
我們在翻閱API的時候會發現Timed Waiting(計時等待) 與 Waiting(無限等待) 狀態聯系還是很緊密的,比如Waiting(無限等待) 狀態中wait方法是空參的,而timed waiting(計時等待) 中wait方法是帶參的。這種帶參的方法,其實是一種倒計時操作,相當于我們生活中的小鬧鐘,我們設定好時間,到時通知,可是如果提前得到(喚醒)通知,那么設定好時間在通知也就顯得多此一舉了,那么這種設計方案其實是一舉兩得。如果沒有得到(喚醒)通知,那么線程就處于Timed Waiting狀態,直到倒計時完畢自動醒來;如果在倒計時期間得到(喚醒)通知,那么線程從Timed Waiting狀態立刻喚醒。

4 等待喚醒機制

4.1 線程間通信

概念:多個線程在處理同一個資源,但是處理的動作(線程的任務)卻不相同。

比如:線程A用來生成包子的,線程B用來吃包子的,包子可以理解為同一資源,線程A與線程B處理的動作,一個是生產,一個是消費,那么線程A與線程B之間就存在線程通信問題。

[圖片上傳失敗...(image-4799c3-1580720568912)]

為什么要處理線程間通信:

多個線程并發執行時, 在默認情況下CPU是隨機切換線程的,當我們需要多個線程來共同完成一件任務,并且我們希望他們有規律的執行, 那么多線程之間需要一些協調通信,以此來幫我們達到多線程共同操作一份數據。

如何保證線程間通信有效利用資源:

多個線程在處理同一個資源,并且任務不同時,需要線程通信來幫助解決線程之間對同一個變量的使用或操作。 就是多個線程在操作同一份數據時, 避免對同一共享變量的爭奪。也就是我們需要通過一定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制。

4.2 等待喚醒機制

什么是等待喚醒機制

這是多個線程間的一種協作機制。談到線程我們經常想到的是線程間的競爭(race),比如去爭奪鎖,但這并不是故事的全部,線程間也會有協作機制。就好比在公司里你和你的同事們,你們可能存在在晉升時的競爭,但更多時候你們更多是一起合作以完成某些任務。

就是在一個線程進行了規定操作后,就進入等待狀態(wait()), 等待其他線程執行完他們的指定代碼過后 再將其喚醒(notify());在有多個線程進行等待時, 如果需要,可以使用 notifyAll()來喚醒所有的等待線程。

wait/notify 就是線程間的一種協作機制。

等待喚醒中的方法

等待喚醒機制就是用于解決線程間通信的問題的,使用到的3個方法的含義如下:

  1. wait:線程不再活動,不再參與調度,進入 wait set 中,因此不會浪費 CPU 資源,也不會去競爭鎖了,這時的線程狀態即是 WAITING。它還要等著別的線程執行一個特別的動作,也即是“通知(notify)”在這個對象上等待的線程從wait set 中釋放出來,重新進入到調度隊列(ready queue)中
  2. notify:則選取所通知對象的 wait set 中的一個線程釋放;例如,餐館有空位置后,等候就餐最久的顧客最先入座。
  3. notifyAll:則釋放所通知對象的 wait set 上的全部線程。

注意:

哪怕只通知了一個等待的線程,被通知線程也不能立即恢復執行,因為它當初中斷的地方是在同步塊內,而此刻它已經不持有鎖,所以她需要再次嘗試去獲取鎖(很可能面臨其它線程的競爭),成功后才能在當初調用 wait 方法之后的地方恢復執行。

總結如下:

  • 如果能獲取鎖,線程就從 WAITING 狀態變成 RUNNABLE 狀態;
  • 否則,從 wait set 出來,又進入 entry set,線程就從 WAITING 狀態又變成 BLOCKED 狀態

調用wait和notify方法需要注意的細節

  1. wait方法與notify方法必須要由同一個鎖對象調用。因為:對應的鎖對象可以通過notify喚醒使用同一個鎖對象調用的wait方法后的線程。
  2. wait方法與notify方法是屬于Object類的方法的。因為:鎖對象可以是任意對象,而任意對象的所屬類都是繼承了Object類的。
  3. wait方法與notify方法必須要在同步代碼塊或者是同步函數中使用。因為:必須要通過鎖對象調用這2個方法。

4.3 生產者與消費者問題

等待喚醒機制其實就是經典的“生產者與消費者”的問題。

就拿生產包子消費包子來說等待喚醒機制如何有效利用資源:

包子鋪線程生產包子,吃貨線程消費包子。當包子沒有時(包子狀態為false),吃貨線程等待,包子鋪線程生產包子(即包子狀態為true),并通知吃貨線程(解除吃貨的等待狀態),因為已經有包子了,那么包子鋪線程進入等待狀態。接下來,吃貨線程能否進一步執行則取決于鎖的獲取情況。如果吃貨獲取到鎖,那么就執行吃包子動作,包子吃完(包子狀態為false),并通知包子鋪線程(解除包子鋪的等待狀態),吃貨線程進入等待。包子鋪線程能否進一步執行則取決于鎖的獲取情況。

代碼演示:

包子資源類:

public class BaoZi {
     String  pier ;
     String  xianer ;
     boolean  flag = false ;//包子資源 是否存在  包子資源狀態
}

吃貨線程類:

public class ChiHuo extends Thread{
    private BaoZi bz;

    public ChiHuo(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }
    @Override
    public void run() {
        while(true){
            synchronized (bz){
                if(bz.flag == false){//沒包子
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("吃貨正在吃"+bz.pier+bz.xianer+"包子");
                bz.flag = false;
                bz.notify();
            }
        }
    }
}

包子鋪線程類:

public class BaoZiPu extends Thread {

    private BaoZi bz;

    public BaoZiPu(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }

    @Override
    public void run() {
        int count = 0;
        //造包子
        while(true){
            //同步
            synchronized (bz){
                if(bz.flag == true){//包子資源  存在
                    try {

                        bz.wait();

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 沒有包子  造包子
                System.out.println("包子鋪開始做包子");
                if(count%2 == 0){
                    // 冰皮  五仁
                    bz.pier = "冰皮";
                    bz.xianer = "五仁";
                }else{
                    // 薄皮  牛肉大蔥
                    bz.pier = "薄皮";
                    bz.xianer = "牛肉大蔥";
                }
                count++;

                bz.flag=true;
                System.out.println("包子造好了:"+bz.pier+bz.xianer);
                System.out.println("吃貨來吃吧");
                //喚醒等待線程 (吃貨)
                bz.notify();
            }
        }
    }
}

測試類:

public class Demo {
    public static void main(String[] args) {
        //等待喚醒案例
        BaoZi bz = new BaoZi();

        ChiHuo ch = new ChiHuo("吃貨",bz);
        BaoZiPu bzp = new BaoZiPu("包子鋪",bz);

        ch.start();
        bzp.start();
    }
}

執行效果:

包子鋪開始做包子
包子造好了:冰皮五仁
吃貨來吃吧
吃貨正在吃冰皮五仁包子
包子鋪開始做包子
包子造好了:薄皮牛肉大蔥
吃貨來吃吧
吃貨正在吃薄皮牛肉大蔥包子
包子鋪開始做包子
包子造好了:冰皮五仁
吃貨來吃吧
吃貨正在吃冰皮五仁包子

5 線程池

5.1 線程池思想概述

[圖片上傳失敗...(image-c7bfc4-1580720568912)]

我們使用線程的時候就去創建一個線程,這樣實現起來非常簡便,但是就會有一個問題:

如果并發的線程數量很多,并且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因為頻繁創建線程和銷毀線程需要時間。

那么有沒有一種辦法使得線程可以復用,就是執行完一個任務,并不被銷毀,而是可以繼續執行其他的任務?

在Java中可以通過線程池來達到這樣的效果。今天我們就來詳細講解一下Java的線程池。

5.2 線程池概念

  • 線程池:其實就是一個容納多個線程的容器,其中的線程可以反復使用,省去了頻繁創建線程對象的操作,無需反復創建線程而消耗過多資源。

由于線程池中有很多操作都是與優化資源相關的,我們在這里就不多贅述。我們通過一張圖來了解線程池的工作原理:

[圖片上傳失敗...(image-bb787a-1580720568912)]

合理利用線程池能夠帶來三個好處:

  1. 降低資源消耗。減少了創建和銷毀線程的次數,每個工作線程都可以被重復利用,可執行多個任務。
  2. 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  3. 提高線程的可管理性。可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因為消耗過多的內存,而把服務器累趴下(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,最后死機)。

5.3 線程池的使用

Java里面線程池的頂級接口是java.util.concurrent.Executor,但是嚴格意義上講Executor并不是一個線程池,而只是一個執行線程的工具。真正的線程池接口是java.util.concurrent.ExecutorService

要配置一個線程池是比較復雜的,尤其是對于線程池的原理不是很清楚的情況下,很有可能配置的線程池不是較優的,因此在java.util.concurrent.Executors線程工廠類里面提供了一些靜態工廠,生成一些常用的線程池。官方建議使用Executors工程類來創建線程池對象。

Executors類中有個創建線程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回線程池對象。(創建的是有界線程池,也就是池中的線程個數可以指定最大數量)

獲取到了一個線程池ExecutorService 對象,那么怎么使用呢,在這里定義了一個使用線程池對象的方法如下:

  • public Future<?> submit(Runnable task):獲取線程池中的某一個線程對象,并執行

    Future接口:用來記錄線程任務執行完畢后產生的結果。線程池創建與使用。

使用線程池中線程對象的步驟:

  1. 創建線程池對象。
  2. 創建Runnable接口子類對象。(task)
  3. 提交Runnable接口子類對象。(take task)
  4. 關閉線程池(一般不做)。

Runnable實現類代碼:

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("我要一個教練");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("教練來了: " + Thread.currentThread().getName());
        System.out.println("教我游泳,交完后,教練回到了游泳池");
    }
}

線程池測試類:

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 創建線程池對象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象
        // 創建Runnable實例對象
        MyRunnable r = new MyRunnable();

        //自己創建線程對象的方式
        // Thread t = new Thread(r);
        // t.start(); ---> 調用MyRunnable中的run()

        // 從線程池中獲取線程對象,然后調用MyRunnable中的run()
        service.submit(r);
        // 再獲取個線程對象,調用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
        // 注意:submit方法調用結束后,程序并不終止,是因為線程池控制了線程的關閉。
        // 將使用完的線程又歸還到了線程池中
        // 關閉線程池
        //service.shutdown();
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370