1. 程式人生 > 其它 >6、多執行緒

6、多執行緒

6、多執行緒

6.1 基本概念

程式,程序,執行緒

  • 程式(program)是為完成特定任務、用某種語言編寫的一組指令的集合。即指一段靜態的程式碼,靜態物件。

  • 程序(process)是程式的一次執行過程,或是正在執行的一個程式。是一個動態的過程:有它自身的產生、存在和消亡的過程。——生命週期
     如:執行中的QQ,執行中的MP3播放器
     程式是靜態的,程序是動態
    程序作為資源分配的單位,系統在執行時會為每個程序分配不同的記憶體區域

  • 執行緒(thread),程序可進一步細化為執行緒,是一個程式內部的一條執行路徑。
     若一個程序同一時間 並行執行多個執行緒,就是支援多執行緒的
    執行緒作為排程和執行的基本單位,每個執行緒擁有獨立的執行棧和程式計數器(pc)

    ,執行緒切換的開銷小
     一個程序中的多個執行緒共享相同的記憶體單元/記憶體地址空間
     它們從同一堆中分配物件,可以訪問相同的變數和物件。這就使得執行緒間通訊更簡便、高效。但多個執行緒操作共享的系統資源可能就會帶來 安全的隱患


  • 一個Java應用程式java.exe 至少有三個執行緒main()主執行緒,gc() 垃圾回收執行緒,異常處理執行緒;

  • 並行與併發:

    並行多個CPU同時執行多個任務。比如:多個人同時做不同的事。
    併發一個CPU(採用時間片)同時執行多個任務。比如:秒殺、多個人做同一件事。


對於多執行緒:

  • 以單核CPU為例,只使用單個執行緒先後完成多個任務(呼叫多個方法) 反而比用多個執行緒
    來完成用的時間更短。 ---> 執行緒之間的切換需要時間

多執行緒程式的優點
 提高應用程式的響應。對圖形化介面更有意義,可增強使用者體驗。
 提高計算機系統CPU的利用率
 改善程式結構。將既長又複雜的程序分為多個執行緒,獨立執行,利於理解和修改

何時需要多執行緒
 程式需要同時執行兩個或多個任務
 程式需要實現一些需要等待的任務時,如使用者輸入、檔案讀寫操作、網路操作、搜尋等。
 需要一些後臺執行的程式時

6.2 執行緒的建立和使用*

多執行緒的建立

方式一:繼承Thread類

  1. 建立一個繼承於Thread類的子類

  2. 重寫Thread類的run( ) --> 將此執行緒執行的操作宣告在run( )中

  3. 建立Thread類的子類的物件

  4. 通過此物件呼叫start( )---> a.啟動當前執行緒 b.呼叫當前執行緒的run( )

    //1.建立一個繼承於Thread類的類
    class MyThread extends Thread{
        //2.重寫Thread類的run()
        pbulic void run(){ sout("這個執行緒要執行的操作"); }
    }
    //main--主執行緒
    public void main(){
        //3.建立Thread類的子類的物件
        MyThread t1 = new MyThread();
        //4.通過此物件呼叫start()
        t1.start();
                                                                 
        new Thread(){
            public void run(){ sout("多執行緒--Thread匿名子類"); }
        }.start();
    }
    

注意:

  • 要想啟動多執行緒,必須呼叫start( ),呼叫run()並不會啟動多執行緒
  • 一個執行緒物件只能呼叫一次start()方法啟動

方式二:實現Runnable介面

  1. 建立一個實現 Runnable介面的類 --實現類

  2. 實現類實現介面中的 抽象方法:run( )

  3. 建立實現類的物件

  4. 將此物件作為引數 傳遞到Thread類的構造器中,建立Thread類物件 --代理類

  5. 通過Thread類的物件 呼叫start( )

    //1.建立一個實現了 Runnable介面的類,並實現run()方法
    class MyThread implements Runnable{
        pbulic void run(){ sout("此執行緒要執行的操作") };
    }
                                                             
    public void main(){
        //2.建立實現類物件
        MyThread w = new MyThread(); 
        //3.將實現類物件作為引數 傳遞到Thread類的構造器中,建立Thread類物件
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        //4.通過Thread類物件 掉喲start()
        t1.start();
        t2.start();
    }
    

小結

開發中,優先選擇 實現Runnable介面的方式:
 沒有單繼承的侷限性
多個執行緒可以共享實現類中的資料

聯絡:public class Thread implements Runnable

相同點:兩種方式都需要重寫run( ),將執行緒要執行的邏輯宣告在run( )中。

執行緒方法

Thread中的常用方法

start( ) : 啟動當前執行緒,呼叫當前執行緒的run( )

run( ) : 通常需要重寫Thread類中的此方法,將建立的執行緒要執行的操作宣告在run( )中

currentThread( ):靜態方法,返回當前執行程式碼的執行緒

getName( ):獲取當前執行緒的名字

setName( ):設定當前執行緒的名字 【也可以在 構造器中設定執行緒名字 new MyThread("執行緒1")

yield( ):釋放當前cpu的執行權

join( ):線上程a中呼叫執行緒b的join( ),此時執行緒a進入阻塞,直到b執行完畢。

stop( ):強制結束當前執行緒 (已過時)

sleep(long millitime):讓當前執行緒“睡眠”指定 毫秒,睡眠期間此執行緒 阻塞。

isAlive( ):判斷當前執行緒是否存活。

執行緒的優先順序

MAX_PRIORITY: 10
MIN_PRIORITY: 1
NORM_PRIORITY:5 -->預設優先順序

getPriority( ):獲取當前執行緒優先順序

setPriority( ):設定執行緒的優先順序

--- 優先順序只是提高被cpu執行的概率

執行緒通訊

wait( ),notify( ),notifyAll( ):此三個方法定義在Object類中

執行緒的分類

Java中的執行緒分為兩類:守護執行緒使用者執行緒

虛擬機器JVM 必須確保 使用者執行緒執行完畢才能離開 ,若JVM中都是守護執行緒,當前JVM退出。使用者執行緒完畢--JVM關閉--守護執行緒完畢

守護執行緒是用來服務使用者執行緒的,通過在start( )前呼叫thread.setDaemon(true)可以設定為守護執行緒

守護執行緒: 後臺記錄操作日誌,監控記憶體,垃圾回收等待

6.3 執行緒的生命週期

  • JDK 中用Thread.State 類定義了 執行緒的5種狀態
  • Thread.State (返回列舉型別值) : ( 新建NEW,就緒WAITING,執行RUNNABLE,阻塞BLOCKED,死亡TERMINATED

6.4 執行緒同步*

 執行緒同步的問題:當執行緒a操作某共享資料的過程中(還沒操作完),程序b參與進來也操作該共享資料,就會造成共享資料的錯誤。

 解決: 當一個執行緒a在操作共享資料時,其他執行緒不能參與進來,直到執行緒a操作完畢。

 問題:同步後,該同步片段變為 單執行緒;同步片段過多可能使程式出現錯誤 --被某一執行緒全部佔用。

synchronized

方式一:同步程式碼塊

synchronized(同步監視器){
	//需要被同步的程式碼
}

說明:a.操作共享資料的程式碼,即為需要被同步的程式碼。

​ b.共享資料:多個執行緒共同操作的變數

​ c.同步監視器,俗稱 任何一個類的物件 都可以充當鎖
​ 要求:多個執行緒必須要共用同一把鎖。--Runnable方式中可以用 this
​ --Thread方式中可以用 xx.class【類物件】

方式二:同步方法

若操作共享資料的程式碼完整的宣告在一個方法中,可以考慮使用同步方法

public synchronized void method(){
    //被同步的程式碼(實現Runnabe方式)-- this
}
public static synchronized void method(){
    //繼承Thread方式-- 類.class
}

總結:同步方法仍然涉及到同步監視器,只是不再需要顯示宣告。(預設)
​  非靜態的同步方法,同步監視器是:this
​  靜態的同步方法,同步監視器是:當前類本身

Lock鎖

方式三:Lock鎖

JDK5.0新增,手動上鎖,解鎖

//1.例項化ReentrantLock
privete ReentrantLock lock = new ReentrantLock(); //ReentrantLock(true)-公平鎖
//2.呼叫上鎖方法 lock()
lock.lock();
 ---------------------要同步的程式碼片段
//3.呼叫解鎖方法 unlock()
lock.unlock();

synchronized 與 Lock 的對比

面試題:synchronized 與 Lock的異同

  • 相同:二者都可以解決執行緒安全問題

  • 不同:synchronized機制在執行完相應的程式碼片段後,自動釋放同步監視器

    ​ Lock需要手動的啟動同步lock(),手動的結束同步 unlock()

優先順序:Lock---> 同步程式碼塊 ---> 同步方法

執行緒死鎖

死鎖
 不同的執行緒分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了執行緒的死鎖
 出現死鎖後,不會出現異常,不會出現提示,只是所有的執行緒都處於阻塞狀態,無法繼續

解決方法
專門的演算法、原則
儘量減少同步資源的定義
儘量避免巢狀同步

6.5 執行緒通訊

synchronized(this){
    notify();  //執行緒a 釋放(一個)被阻塞的執行緒b
    /*
    同步程式碼塊
    */
    wait();   //當前執行緒(a)進入阻塞,並釋放同步監視器 --- 交替到執行緒b
}

執行緒通訊涉及到的三個方法:
 wait( ): 當前執行緒進入阻塞狀態,並釋放同步監視器
 notify( ): 喚醒被wait的一個執行緒 (若有多個執行緒被wait 則喚醒優先順序高的那個)
 notifyAll( ):喚醒所有被wait的執行緒

說明(對於wait, notify, notifyAll三個方法):
 必須在 同步程式碼塊或同步方法中使用
呼叫者必須是同步程式碼塊或同步方法中的 同步監視器
 都是 定義在java.lang.Object類中的。

面試題:sleep( ) 和 wait( ) 的異同?

相同點:一旦執行方法,都可以使得當前的執行緒進入阻塞狀態

不同點: 1.兩個方法宣告的位置不同:Thread類中宣告sleep()Object類中宣告wait()
2.呼叫的要求不同:sleep()可以在任何場景下呼叫,wait()必須使用在同步程式碼塊/同步方法中
3.是否釋放鎖:若兩個方法都在同步程式碼塊/同步方法中,sleep()不會釋放鎖,wait()會釋放鎖。

生產者消費者模式:

  • 管程法 (生產者---緩衝區---消費者)

  • 訊號燈法 (標誌位)

  • 生產者消費者 共用同一把鎖

    public synchronized void produce(){
        if(goods < 20){
            goods++; 
            sout("生產產品");
            nofity();   //喚醒消費者,通知消費
        }else{
            wait();    //等待消費
        }
    }
    public synchronized void consum(){
        if(goods > 0){
            goods--;
            sout("消費產品");
            nofity();   //喚醒生產者,通知生產
        }else{
            wait();  //等待生產
        }
    }
    

6.6 新增執行緒建立方式

JDK5.0新增了兩種建立執行緒的方式 ---(共四種建立執行緒的方式)

執行緒建立方式三:實現Callable介面

1.建立一個實現 Callable介面 的實現類
2.實現介面中的call( ) --> 將此執行緒執行的操作宣告在call( )中,可以有返回值
3.建立實現類物件
4.建立FutureTask物件,將實現類物件 傳入 FutureTask構造器中 ---(FutureTask 實現了Runnale介面)
5.建立Thread物件,將FutureTask物件 傳入 Thread構造器中
6.通過Thread物件 呼叫 start( )
---> futureTask.get( )可以獲得 call( )的返回值

//1.建立一個實現 Callable介面 的實現類,實現call()
class myThread implements Callable{
     public Object call() throws Exception{ 
         sout("執行緒要執行的操作");
         return null;  //執行緒的返回值
     }
}

public void main(){
 //2.建立實現類物件
 myThread w = new myThread();
 //3.建立FutureTask物件,將實現類物件 傳入 FutureTask構造器中 
 FutureTask futureTask = new FutureTask(w);
 //4.建立Thread物件,將FutureTask物件 傳入 Thread構造器中,呼叫start()
 new Thread(futureTask).start();

 Object ob = futureTask.get();//5.futureTask.get()可以獲得 call()的返回值
}

實現Callable介面的方式 比 實現Runnable介面方式 建立多執行緒更強大?
 call( )可以有返回值
 call( )可以丟擲異常,被外面的操作捕獲,獲取異常的資訊
 Callable是支援泛型

執行緒建立方式四:執行緒池

提前建立好多個執行緒,放入執行緒池中,使用時直接獲取,用完放回。

  • 提高相應速度,降低資源消耗(避免重複的建立銷燬,實現重複利用)
  • 便於執行緒管理:
     corePoolSize:核心池的大小
     maximumPoolSize:最大執行緒數
     keepAliveTime:執行緒沒有任務時最多保持多長時間後會終止

執行緒池的建立

1.提供指定執行緒數量的執行緒池

2.執行指定的執行緒的操作,需要提供 實現Runnable介面/Callable介面的實現類物件
execute(Runnable實現類物件) / submit(Callable實現類物件)

3.關閉連線池 shutdown()

class MyRunThread implements Runnable{
    public void run(){};
}
class MyCalThread implements Callable{
    public Object cal(){};
}
public void main(){
    //1.提供指定數量的執行緒池
    ExecutorService service = Executors.newFixedThreadPool(8);
    //2.執行指定執行緒的操作--管理執行緒
    service.execute(new MyRunThread() ); //適合 Runnable
    service.submit(new MyCalThread() );  //適合 Callable
    service.setXXX;  //執行緒管理
    //3.關閉連線池
    service.shutdown();
}