1. 程式人生 > 其它 >Java多執行緒與執行緒池

Java多執行緒與執行緒池

多執行緒及其問題,執行緒池及使用。搞懂執行緒生命週期、建立等只是第一步,AQS的設計精髓還需要取理解。

關於併發,不要忘了Java中有個十分強大的AQS框架

1.概念

1.1 併發 & 並行 & 序列

併發:巨集觀上多個任務同時執行,實際上同一時刻只有一個執行緒在執行。
並行:微觀上多個任務同時執行,即同一時刻多個任務同時執行。
序列:單執行緒

1.2 併發的作用

併發通常是用於提高執行在單處理器上的程式的效能。乍看不對,多執行緒涉及輪轉使用CPU增加了上下文切換時間,應該更慢。但是我們還需要考慮程式中執行緒阻塞。單執行緒程式執行緒阻塞會導致整個程式阻塞,但是多執行緒可以應對這種情況。也就是說,如果程式中沒有任務會阻塞,多執行緒將毫無意義。

1.3 併發的多面性

優:
讓程式執行更快(總體來看),更充分地利用CPU,提高系統性能。
劣:


帶來執行緒安全問題,增加了系統風險,程式設計難度也提高了。所以在使用併發時,預設執行緒不安全,時刻提醒自己。

1.4 執行緒基本實現機制

底層原理是切分CPU時間,為執行緒分配CPU時間切片,獲得CPU時間切片才可使用CPU。

2.建立執行緒

2.1 定義執行緒任務Runnable

實現Runnable介面是自定義功能簡單的執行緒的常用方式。

public class RunnableThread implements Runnable {
    @Override
    public void run() {
    // ...
    }
}

光是定義一個Runnable介面的實現類是無法實現執行緒行為的,它就相當於一個普通類,必須要將這個任務顯示地附著到執行緒上

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new RunnableThread());
        t.start();
    }
}

2.2 Thread類

如果想用Thread的方法,可以繼承Thread建立任務。

public class MyThread extends Thread {
    @Override
    public void run() {
    // ...
    }
}

另外還有之中匿名方式,也是lambda。

public class Main {
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("是隻豬呀");
        }).start();
    }
}

2.3 Callable

還可以實現Callable介面,用得比較少,知道有就行。

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

2.4 執行緒池建立執行緒

關於執行緒池,必須要先理清幾個介面和類的關係,文章後面會介紹使用。

  • Executor介面:執行提交的Runnable任務的物件
  • ExecutorService介面:提供了更多管理方法API。ExecutorService extends Executor
  • Executors類:定義了ExecutorServiceExecutorScheduledExecutorServiceThreadFactoryCallable類的工廠和方法。裡面包含了大量的public static ExecutorService方法,用於建立執行緒池,而這些方法中部分又使用new ThreadPoolExecutor()來建立執行緒池。
  • ThreadPoolExecutor:阿里Java開發手冊推薦使用的建立執行緒池的工具。

3.執行緒的生命週期

  • NEW:新建
  • RUNNABLE:執行
  • BLOCKED:阻塞
  • WAITING:等待
  • TIMED_WAITING:計時等待
  • TERMINATED:已終止
// 新建狀態
Thread t = new MyThread();

// 執行狀態
t.start();

// 阻塞狀態
執行緒 t 需要 synchronized(obj),但是已經被別的執行緒持有,一直去嘗試給這個物件加鎖

// 等待狀態
t.wait();
t.join();  // 等待別的執行緒結束
LockSupport.park();

// 計時等待 給定了時長的
t.sleep(1000);
t.wait(1000);

// 終止
執行緒任務執行完成

如果執行緒需要執行一個長時間任務,可能需要能中斷執行緒

// 中斷執行緒
t.interrupt();

// 檢查執行緒是否中斷 都呼叫native方法
t.isInterrupted();

4.守護執行緒

說到守護執行緒首先想到JVM的垃圾收集器。守護執行緒是為使用者執行緒服務的,所有使用者執行緒結束後,整個程式就結束了,而不會關心守護執行緒狀態。守護執行緒是低優先順序執行緒,任何守護執行緒都是為所有的使用者執行緒服務,而不是某些使用者執行緒。可以t.setDaemon(true);t.start();這樣設定,要在start()之前。

5.死鎖

死鎖表現為:多個執行緒都持有部分資源,而又在相互嘗試獲取其它執行緒持有的資源的狀態。

形成鎖的四個必要條件:

  • 互斥:兩個執行緒執行都需要相同的資源
  • 不可剝奪:執行緒持有的資源不能被其執行緒剝奪,只能自己釋放
  • 請求與保持:持有部分資源,同時請求另一部分必要資源
  • 迴圈等待:如兩個執行緒都在等待對方釋放資源

死鎖的解決辦法,破壞四個條件中的任意一個

  • 互斥:是必然的,否則沒有加鎖的必要。
  • 不可剝奪:將資源定義成原子的,一次性獲取所有需要的資源
  • 請求與保持:申請不到資源可以主動釋放已佔有的資源
  • 迴圈等待:按順序申請資源,逆序釋放

6.執行緒池的使用

6.1 Executors.newFixedThreadPool

建立固定大小執行緒池

// 5個執行緒的執行緒池
ExecutorService executor = Executors.newFixedThreadPool(5);

// 提交任務
executor.submit(new RunnableTask());

// 關閉執行緒池
executor.shutdown(); 

// 立即關閉 不會等待執行緒執行結束
executor.shutdownNow();

// 指定延時後關閉 需要捕獲InterruptedException
executor.awaitTermination(60, TimeUnit.SECONDS);

6.2 Executors.newCachedThreadPool

根據任務數量動態調整執行緒池的大小

6.3 ScheduledThreadPool

定時任務

ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);

// 10秒後執行任務 且只執行一次
ses.schedule(new RunnableTask(), 10, TimeUnit.SECONDS);

// 延遲10秒後開始執行任務 任務每5秒內執行一次
ses.scheduleAtFixedRate(new RunnableTask(), 10, 5, TimeUnit.SECONDS);

// 延遲10秒後開始執行任務 任務每隔5秒執行一次(這個是不包含執行任務的時間的,如任務執行需要2秒,那就是每7秒執行一次)
ses.scheduleWithFixedDelay(new RunnableTask(), 10, 5, TimeUnit.SECONDS);

6.4 ThreadPoolExecutor

Executors的方法也使用了ThreadPoolExecutor來建立執行緒池。使用ThreadPoolExecutor能夠讓我們更方便控制其核心引數。關於ThreadPoolExecutor可以參考另一篇部落格