Java執行緒總結

三省l發表於2020-10-30

在這裡插入圖片描述

1. 實現執行緒

  1. 繼承Thread類:

    1. 定義一個類MyThread繼承Thread類
    2. 在MyThread類中重寫run()方法
    3. 建立MyThread類的物件
    4. 呼叫MyThread的start方法啟動執行緒
  2. 實現Runnable介面:無返回值

    1. 定義一個類MyRunnable實現Runnable介面
    2. 在MyRunnable類中重寫run()方法
    3. 建立MyRunnable類的物件
    4. 建立Thread類的物件,把MyRunnable物件作為構造方法的引數
    5. 呼叫Thread的start方法啟動執行緒
  3. 實現Callable介面:有返回值

    1. 定義一個類實現Callable並重寫call()方法
    2. 建立Callable實現類物件
    3. 建立FutureTask(Callable callable)物件,並把Callable實現類物件作為引數
    4. 建立Thread類的物件,把FutureTask物件作為構造方法的引數
    5. 呼叫Thread的start方法啟動執行緒
  4. 建立執行緒池:

    1. 呼叫工廠類Executors靜態方法newFixedThreadPool建立執行緒池物件ExecutorService
    2. 呼叫執行緒池物件ExecutorService的submit方法提交一個執行緒
      1. 傳遞Runnable介面
      2. 傳遞Callable介面
    3. 呼叫執行緒池物件ExecutorService的shutdown方法關閉執行緒池
  5. 相比繼承Thread類,實現Runnable介面的好處

    1. 介面可以多實現,避免了Java單繼承的侷限性
    2. 適合多個相同程式的執行緒去共享同一個資源。把執行緒(new Thread)和程式任務(run方法)有效分離,較好的體現了物件導向的設計思想
  6. Java是搶佔式排程,優先讓優先順序高的執行緒使用CPU,優先順序高的執行緒獲取的CPU時間片相對多一些;如果執行緒的優先順序相同,那麼會隨機選擇一個。

  7. 開啟一個新執行緒時會在記憶體中為該執行緒分配一個棧記憶體,當執行緒執行完後會釋放該棧記憶體。

  8. Thread類常用方法:

    Runnable介面方法:
    void run()
    
    Thread類常用構造:
    // 分配一個新的Thread物件
    Thread()
    Thread(String name)
    Thread(Runnable target)
    Thread(Runnable target, String name)
    
    Thread類常用方法:
    // 導致此執行緒開始執行;Java虛擬機器呼叫此執行緒的run方法
    void start()
    // 返回此執行緒的名稱
    String getName()
    // 將此執行緒的名稱更改為等於引數name
    void setName(String name)
    // 返回對當前正在執行的執行緒物件的引用
    static Thread currentThread()
    // 返回此執行緒的優先順序
    // 執行緒優先順序的範圍是:1-10;執行緒預設優先順序是5
    int getPriority()
    // 更改此執行緒的優先順序
    void setPriority(int newPriority)
    // 使當前正在執行的執行緒暫停執行指定的毫秒數
    static void sleep(long millis)
    // 將此執行緒標記為守護執行緒,當執行的執行緒都是守護執行緒時,Java虛擬機器將退出
    void setDaemon(boolean on)
    // 等待這個執行緒死亡再執行其他執行緒
    void join()
    
    Object的wait()方法會失去鎖物件;需要被鎖物件呼叫,而且只能出現在同步中
    Thread的靜態方法sleep()不會失去鎖物件;不需要物件鎖
    

2. 執行緒狀態

  1. 在給定時間點上,一個執行緒只能處於一種狀態。這些狀態是虛擬機器狀態,它們並沒有反映所有作業系統執行緒狀態。執行緒可以處於下列狀態之一:

    1. NEW(新建):執行緒被建立,但還沒呼叫start方法啟動
    2. RUNNABLE(可執行):正在Java虛擬機器中執行的執行緒處於這種狀態
    3. BLOCKED(鎖阻塞):受阻塞並等待某個監視器鎖的執行緒處於這種狀態
    4. WAITING(無限等待):無限期地等待另一個執行緒來執行某一特定操作(如喚醒)的執行緒處於這種狀態
    5. TIMED_WAITING(計時等待):等待另一個執行緒來執行取決於指定等待時間的操作的執行緒處於這種狀態,這一狀態將一直保持到超時期滿或者接收到喚醒通知
    6. TERMINATED(被終止):已退出的執行緒處於這種狀態
  2. 執行緒生命週期:

    NEW-->start()並等待鎖-->BLOCKED
    NEW-->start()並獲得鎖-->RUNNABLE
    RUNNABLE-->run()結束 或 stop()-->TERMINATED
    
    RUNNABLE-->沒有爭取到鎖物件-->BLOCKED
    BLOCKED-->獲取到了鎖物件-->RUNNABLE
    
    RUNNABLE-->wait()-->WAITING
    WAITING-->其他執行緒呼叫notify()並獲得鎖-->RUNNABLE
    
    RUNNABLE-->sleep(引數)wait(引數)-->TIMED_WAITING
    TIMED_WAITING-->sleep(引數)時間到 或 (wait(引數)時間到並獲得鎖 或 其他執行緒呼叫notify()並獲得鎖)-->RUNNABLE
    
    TIMED_WAITING-->wait(引數)時間到但沒有獲得鎖 或 其他執行緒呼叫notify()但沒有獲得鎖-->BLOCKED
    

3. 執行緒同步

  1. 執行緒安全問題大都是由全域性變數及靜態變數引起的。若每個執行緒中對全域性變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全域性變數是執行緒安全的;若有多個執行緒同時執行寫操作,一般都需要考慮執行緒同步,否則的話就可能影響執行緒的安全性。
  2. 判斷多執行緒程式是否會有資料安全問題:
    1. 是否是多執行緒環境
    2. 是否有共享資料
    3. 是否有多條語句操作共享資料
  3. 同步的好處:解決了多執行緒的資料安全問題
  4. 同步的弊端:當執行緒很多時,因為每個執行緒都會去判斷同步上的鎖,這是很耗費資源的,無形中會降低程式的執行效率

3.1 synchronized關鍵字

  1. 同步程式碼塊:鎖住多條操作共享資料語句。在任何時候,最多允許一個執行緒擁有同步鎖,誰拿到鎖就進入程式碼塊,其他的執行緒只能在外等著(BLOCKED)

    格式:
    synchronized(同步鎖) {
    	多條語句操作共享資料的程式碼
    }
    
    同步鎖又稱為物件監視器,可以是任意物件,需要同步的執行緒要使用同一把鎖。
    
    synchronized(同步鎖):就相當於給程式碼加鎖了,任意物件就可以看成是一把鎖
    
  2. 同步方法:把synchronized關鍵字加到方法上。當一個方法中的所有程式碼,全部是執行緒操作共享資料時,可以將整個方法進行同步。

    格式:修飾符 synchronized 返回值型別 方法名(方法引數) {}
    非靜態同步方法的鎖物件是:this
    
    同步靜態方法:把synchronized關鍵字加到靜態方法上
    格式:修飾符 static synchronized 返回值型別 方法名(方法引數) {}
    靜態同步方法的鎖物件是:類名.class
    

3.2 Lock介面

  1. JDK5以後提供了Lock介面,目的是取代synchronized。Lock實現提供了比使用synchronized方法和語句可獲得的更廣泛的鎖定操作,同步程式碼塊/同步方法具有的功能Lock都有,除此之外更強大、更體現物件導向。

  2. Lock常用方法:

    // 獲取鎖
    void lock()
    
    // 釋放鎖
    void unlock()
    
    // 返回繫結到此Lock例項的新Condition例項
    // 即在記憶體中建立一個阻塞佇列
    Condition newCondition()
    
  3. Lock是介面不能直接例項化,可以採用它的實現類來例項化(如ReentrantLock)

    使用方式:
    class X {
        private final ReentrantLock lock = new ReentrantLock();
        // ...
    
        public void m() {
            lock.lock(); // block until condition holds
            try {
                // ... method body
            } finally {
                lock.unlock();
            }
        }
    }
    

4. Condition介面

  1. Condition即一個阻塞佇列

  2. Condition介面將Object監視器方法(wait、notify和notifyAll)分解成截然不同的物件,以便通過將這些物件與任意Lock實現組合使用。即Condition介面實現等待和喚醒,必須依賴Lock介面鎖。

  3. Condition常用方法:

    // 接到訊號或被中斷之前一直處於等待狀態,釋放鎖,進阻塞佇列
    // 相當於P操作
    void await()
    
    // 喚醒一個等待執行緒,獲取鎖,出阻塞佇列
    // 相當於V操作
    void signal()
    
    // 喚醒所有等待執行緒
    void signalAll()
    
  4. Condition介面方法和Object類方法比較:

    1. Condition可以和任意的Lock組合,實現管理執行緒的阻塞佇列。
      1. 一個執行緒中,可以使用多個Lock鎖,每個Lock鎖上可以結合Condition物件。
      2. synchronized同步中做不到將執行緒劃分到不同的佇列中。
    2. Object類wait()和notify()都要和作業系統互動,並通知CPU掛起執行緒,喚醒執行緒,效率低。
    3. Condition介面方法await()不和作業系統互動,而是讓釋放鎖,並存放到執行緒佇列容器中,當被signal()喚醒後,從佇列中出來,從新獲取鎖後在執行。
  5. 生產者消費者示例:當沒有時商品時,消費執行緒等待;生產執行緒生產商品,並通知消費執行緒(解除消費執行緒的等待狀態);保證執行緒安全,必須生產一個消費一個,不能同時生產或者消費多個。

    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    // 商品類
    public class Goods {
        private int count;
        private boolean hasGoods = false;
        private Lock lock = new ReentrantLock();
        // 生產者阻塞佇列
        private Condition production = lock.newCondition();
        // 消費者阻塞佇列
        private Condition consumption = lock.newCondition();
    
        // 消費者消費
        public void get() {
            // 獲取鎖物件
            lock.lock();
            // 用while迴圈判斷,因為執行緒被喚醒後,也需要判斷當前商品狀態,不然多執行緒的話會出問題
            while (hasGoods == false) {
                // 等待,釋放鎖,進入消費阻塞佇列
                try {
                    consumption.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 消費商品
            System.out.println(Thread.currentThread().getName() + "消費第" + count + "個商品");
            // 有了商品就修改標誌位
            hasGoods = false;
            // 喚醒生產者執行緒
            production.signal();
            // 消費完畢,釋放鎖
            lock.unlock();
        }
    
        // 生產者生產
        public void set() {
            // 獲取鎖物件
            lock.lock();
            // 用while迴圈判斷,因為執行緒被喚醒後,也需要判斷當前商品狀態,不然多執行緒的話會出問題
            while (hasGoods == true) {
                // 等待,釋放鎖,進入生產阻塞佇列
                try {
                    production.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 生產商品
            count++;
            System.out.println(Thread.currentThread().getName() + "生產第" + count + "個商品");
            // 有了商品就修改標誌位
            hasGoods = true;
            // 喚醒消費者執行緒
            consumption.signal();
            // 生產完畢,釋放鎖
            lock.unlock();
        }
    }
    
    // 消費類
    public class Consumers implements Runnable {
        private Goods goods;
    
        public Consumers() {
        }
    
        public Consumers(Goods goods) {
            this.goods = goods;
        }
    
        @Override
        public void run() {
            while (true) {
                goods.get();
            }
        }
    }
    
    // 生產類
    public class Producers implements Runnable {
        private Goods goods;
    
        public Producers() {
        }
    
        public Producers(Goods goods) {
            this.goods = goods;
        }
    
        @Override
        public void run() {
            while (true) {
                goods.set();
            }
        }
    }
    
    // 測試類
    public class Test {
        public static void main(String[] args) {
            Goods goods = new Goods();
            new Thread(new Producers(goods)).start();
            new Thread(new Consumers(goods)).start();
    
            new Thread(new Producers(goods)).start();
            new Thread(new Consumers(goods)).start();
    
            new Thread(new Producers(goods)).start();
            new Thread(new Consumers(goods)).start();
        }
    }
    

5. 執行緒池

  1. Executors類常用方法:

    // 建立一個可重用固定執行緒數的執行緒池,以共享的無界佇列方式來執行這些執行緒
    static ExecutorService newFixedThreadPool(int nThreads)
    
    // 建立一個根據需要建立新執行緒的執行緒池,但在可用時將重新使用以前構造的執行緒
    static ExecutorService newCachedThreadPool()
    
    // 建立一個執行緒池,可以排程命令在給定的延遲之後執行,或定期執行
    static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
    
    // 建立一個使用從無界佇列執行的單個工作執行緒的執行程式
    static ExecutorService newSingleThreadExecutor()
    
  2. ExecutorService介面常用方法:

    // 提交一個 Runnable 任務用於執行,並返回一個表示該任務的 Future
    Future<?> submit(Runnable task)
    
    // 提交一個返回值的任務用於執行,返回一個表示任務的未決結果的 Future
    <T> Future<T> submit(Callable<T> task)
    
    // 關閉執行緒池,要等所有執行緒都完成任務後再關閉,但是不接收新任務
    void shutdown()
    
  3. Callable介面常用方法:

    執行緒執行的任務介面,類似於Runnable介面。
    // 計算結果,如果無法計算結果,則丟擲一個異常
    // 執行緒要執行的任務方法,只能線上程池使用
    // 比起run()方法,call()方法具有返回值,可以獲取到執行緒執行的結果
    V call()
    
  4. Future介面常用方法:

    // 如有必要,等待計算完成,然後獲取其結果
    // 獲取執行緒執行的結果,就是獲取call()方法返回值
    V get()
    
  5. 執行緒池示例:

    import java.util.Arrays;
    import java.util.concurrent.*;
    
    /**
     * 執行緒池示例:
     * 1. 開啟一個執行緒,計算1+2+...+100
     * 2. 開啟一個執行緒,切割字串
     */
    class Calc implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            int sum = 0;
            for (int i = 1; i <= 100; i++) {
                sum += i;
            }
            return sum;
        }
    }
    
    class Split implements Callable<String[]> {
        @Override
        public String[] call() throws Exception {
            return "192.168.100.226".split("\\.");
        }
    }
    
    public class Main {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            ExecutorService es = Executors.newFixedThreadPool(2);
            // 提交計算任務
            Future<Integer> sum = es.submit(new Calc());
            System.out.println(sum.get());
            // 提交分割任務
            Future<String[]> split = es.submit(new Split());
            System.out.println(Arrays.toString(split.get()));
            // 關閉執行緒池
            es.shutdown();
        }
    }
    

6. Timer類

java.util.Timer常用方法:
// 安排指定的任務在指定的時間開始進行重複的固定延遲執行
// TimerTask是定時器要執行的任務,一個抽象類,我們需要繼承並重寫方法run()
// firstTime定時器開始執行的時間
// period時間間隔,毫秒值
void schedule(TimerTask task, Date firstTime, long period)

public static void main(String[] args) {
    Timer timer = new Timer();
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            System.out.println(System.currentTimeMillis());
        }
    }, new Date(), 1000);
}

若有錯誤或補充,歡迎私信

相關文章