Java併發之執行緒與執行緒池

語言: CN / TW / HK

我們經常聽到一些大佬說一些概念,比如執行緒不安全,那到底什麼是執行緒不安全呢?

 執行緒不安全指的是,我們在多執行緒的環境下,操作一些共享資料的時候可能會讓我們無法得到期望的結果

執行緒

為什麼會出現執行緒呢?

 執行緒就好比一個人,俗話說的好,眾人拾柴火焰高,而多執行緒也是這個道理!

我們先來看一下執行緒6種狀態的狀態

執行緒狀態

  • New(新生態)

  • Runnable(可執行態)

    在可執行態中又可以分為,Running(執行態)和Ready(就緒態)

    • Running(執行態)

      執行態指的是,該執行緒已經獲取了CPU的時間片,簡單來說,就是該執行緒正在執行 * Ready(就緒態)

      而就緒態指的是,該執行緒萬事俱備只欠“東風”,就差CPU給他分配時間片了 * Blocking(阻塞態) 執行緒在獲取鎖失敗之後,就會進入該狀態,當該執行緒獲取了鎖就會結束該狀態,所有阻塞狀態的執行緒都會放在阻塞佇列中。

  • Waiting(等待態)

當處於執行態的執行緒呼叫wait(),park(),join()方法後,就會進入該狀態,處於等待態的執行緒,會釋放CPU時間片,並且會釋放資源(例如鎖),這個狀態下的執行緒只能等待其他執行緒來喚醒它。 * Timed Waiting(超時等待態)

超時等待態和等待態類似,不過這個狀態的執行緒不需要顯式地去喚醒,這個狀態的執行緒在超過一定時間後,將由系統自動喚醒

  • Terminated(結束態) 執行緒結束後的狀態

執行緒六種狀態的轉換.png

執行緒的三種使用方式

執行緒的使用方式有三種,分別是實現Runnable介面、實現Callable介面、繼承Thread類 * 實現Runnable介面

   建立一個自定義類然後實現該介面,實現該介面的run()方法
   然後在我們需要使用該類的地方直接去例項化即可
  • 實現Callable介面

    建立一個自定義類然後實現該介面,實現該介面的call()方法 然後通過開啟執行緒池服務來建立該類 * 繼承Thread類

    建立一個自定義類然後繼承該類,需要重寫該類的run()方法 這種方式其實和實現Runnable介面的方式類似,因為Thread類也實現了Runnable介面 我們在實現的時候直接去實現即可

    三種方式的對比

其實我們更加推薦實現Runable介面的這種方式,因為相較於其他兩種,實現Callable介面這種方式使用起來更加繁瑣,而繼承Thread類的這種方式是繼承,我們都知道在Java中是不支援多重繼承的,但是支援多重實現,並且用起來實現Runnable介面這種方式也更加簡單

執行緒之間的協作工作

join()方法

我們在很多情況下,會在一個執行緒中呼叫另一個執行緒的方法,這個時候我們就會使用到join方法了

在這裡我們先定義一個執行緒類,為了方便我們直接去繼承Thread類 ```java public class YlOneThread extends Thread {

@Override
public void run() {
    System.out.println("我是亞雷1執行緒");        
}

} 在這裡我們又定義了一個執行緒類,不過在這個執行緒類中需要使用前面那個執行緒java

public class YlTwoThread extends Thread{

private YlOneThread thread;

public YlTwoThread(YlOneThread thread){
    this.thread = thread;
}
@Override
public void run() {
    try {
        thread.join();
    } catch (InterruptedException e) {           
        e.printStackTrace();
    }
    System.out.println("我是亞雷2執行緒");        
}

} ``` 最後我們再建立一個測試類來測試

java public class test { public static void main(String[] args) { YlOneThread Thread1 = new YlOneThread(); YlTwoThread Thread2 = new YlTwoThread(Thread1); Thread2.start(); Thread1.start(); } } 我們可以很明顯的看到再執行緒2中呼叫了執行緒1不出意外我們得到了這樣的結果

我是亞雷1執行緒
我是亞雷2執行緒

在這裡雖然是執行緒2先啟動,不過線上程2中呼叫了執行緒1,執行緒2會先等待執行緒1完成然後繼續執行

wait()、notify()、notifyAll()方法.

在這裡我就簡略的說一下這些方法、因為這些方法並不是來自於JUC包下的而是來自於Object類下面的

  • wait()方法

    這個方法在前面我們也見過了,讓執行緒進入等待態並且也會釋放資源(鎖) * notify()、notifyAll()方法

    這兩個方法用於喚醒執行緒、不同的是一個是喚醒所有執行緒而另一個不是

wait()和sleep()

我們經常拿這兩個方法進行比較、因為它們在我們淺層的認識中都是使執行緒進行等待

其實它們大相徑庭

wait()方法會釋放資源(鎖),這在前面我們已經知道了, 而sleep()方法並不會釋放資源(鎖)

wait()方法是基於Object的方法,而sleep()是基於Thread類的方法

await()、signal()、signalAll()方法

而在JUC中我們使用這些方法來實現執行緒間的排程

```java public class AWaitTest { private ReentrantLock lock = new ReentrantLock();

private Condition condition = lock.newCondition();

public void methodOne(){
    lock.lock();
    try {
        System.out.println("方法一執行......");
        condition.signalAll();
    } catch (Exception e) {

    }finally{
        lock.unlock();
    }        
}
public void methodTwo(){
    lock.lock();
    try {
        condition.await();
        System.out.println("方法二執行.......");
    } catch (Exception e) {
        //TODO: handle exception
    }finally{
        lock.unlock();
    }

}

} ```

```java public static void main(String[] args) {

    ExecutorService ThreadPool = Executors.newCachedThreadPool();
    AWaitTest aWaitTest = new AWaitTest();
    ThreadPool.execute(() -> aWaitTest.methodTwo());
    ThreadPool.execute(() -> aWaitTest.methodOne());
}

```

    方法一執行......
    方法二執行.......

這種方法很明顯wait()那一套更加靈活

執行緒池

說到執行緒池,我們不得不提一下執行緒池的七大引數了。

執行緒池七大引數

java public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 1. corePoolSize 核心執行緒數

   當提交一個任務時,執行緒池建立一個新的執行緒來執行任務,直到執行緒數量達到corePoolSize的時候
   這個時候會將執行緒放入到工作隊列當中阻塞
  1. maximumPoolSize 最大執行緒數

    這個引數只在,工作佇列是有邊界的時候生效,如果工作佇列沒有邊界,這個引數將不生效 因為會將新的執行緒一直新增到工作隊列當中 3. keepAliceTime 執行緒空閒的存活時間

    因為執行緒執行任務執行完畢之後不會立即死亡,會繼續存活下來,而這個引數就是在限制執行緒的存活時間 還有一點需要注意,這個引數預設只在當前執行緒數大於核心執行緒數的情況下生效 4. unit 執行緒存活時間單位

    見名知意,這個引數作為keepAliceTime的單位 5. workQueue 工作佇列

    用於儲存等待需要執行任務的新執行緒 6. threadFactory 執行緒工廠

    用於建立執行緒的執行緒工廠

  2. handler 飽和策略

    當阻塞佇列滿了,並且沒有空閒的的工作執行緒,如果此時還不斷提交任務,執行緒池必須進行處理 執行緒池提供了四種飽和策略 1. AbortPolicy: 直接丟擲異常,預設策略 2. CallerRunsPolicy: 用呼叫者所在的執行緒來執行任務 3. DiscardOldestPolicy: 丟棄阻塞佇列中靠最前的任務,並執行當前任務 4. DiscardPolicy: 直接丟棄任務

    執行緒池的執行流程

執行緒池到底是怎麼來建立執行緒的呢?

我們在提交任務後,
執行緒池會先判斷當前執行緒數是否大於核心執行緒數,
如果不大於則直接建立工作執行緒,
否則則建立執行緒新增到阻塞隊列當中,
然後,如果執行緒池會再次進行判斷阻塞佇列是否滿
如果不滿,則直接新增到阻塞隊列當中
否則,會再次判斷當前執行緒數是否大於最大執行緒數
如果大於則執行拒絕策略,否則就建立執行緒

執行緒池的執行流程.png