1. 程式人生 > 其它 >多執行緒的一些知識

多執行緒的一些知識

  • 執行緒和程序

    程序:一個在記憶體中獨立執行的程式,每一個程序都有自己獨立的記憶體空間,一個程序可以建立多個執行緒。是作業系統分配資源的基本單位。

    執行緒:由程序建立,用於執行任務,一個程序最少有一個執行緒,可以有多個執行緒,執行緒共享程序中的資料。每個執行緒有自己的程式計數器、虛擬機器棧和本地方法棧,所以系統在生產一條執行緒或者在多條執行緒中切換工作時開銷比程序小的多,所以執行緒也被稱為輕量級程序。是處理器任務排程和執行的基本單位

  • 執行緒的三種建立方式

    • 重寫thread的run方法

      Thrad thread = new Thread(){
      		@Override
      		public void run(){
      		   System.out.println(1)
      		}
      };
      thread.start();
      
    • 實現runnable

      Thread thread = new Thread(new Runnable(){
      		 @Override
      		 public void run(){
      		 		System.out.println(2)
      		 }
      });
      thread.start();
      
    • 實現callback

      Callable<Integer> callable = new Callable<Integer>(){
        	@Override
          public Integer call() throw Exception{
              return 3;
          }
      };
      FutureTask<Integer> task = new FutureTask<>(callable);
      new Thread(task).start();
      while(task.isDone()){
        try{
          System.out.println(task.get());
        }catch(InterruptedException e){
          e.printStackTrace();
        }catch(ExecutionException e){
          e.printStackTrace();
        }
      }
      
  • 執行緒的狀態

    • NEW:建立還未執行,即還沒有呼叫start()方法
    • RUNNABLE:執行狀態,呼叫start()方法之後進入該狀態
    • BLOCKED:阻塞狀態,呼叫start()方法之後拿不鎖時的狀態
    • WAITING:等待狀態,呼叫start()方法拿到鎖之後執行時發現沒有資源,釋放鎖給其他執行緒,進入該狀態,等到有資源之後,繼續執行。
    • TIMED_WAITING:超時等待,呼叫start()方法拿到鎖之後執行發現沒有資源,釋放鎖給其他執行緒,進入超時等待狀態,但是當超過設定的等待時間之後,即使拿不到資源也會繼續執行。
    • TERMINATED:執行緒執行結束狀態
  • wait和sleep的區別

    • wait

      1. 暫停當前執行緒,釋放鎖
      2. 屬於Object物件
      3. wait只能在同步塊中使用
      4. 可以隨時喚醒
    • Sleep

      1. 暫停當前執行緒,不釋放鎖
      2. 屬於執行緒Thread
      3. 可以在任何場景下使用
      4. 只有sleep到設定的時間才會被重新喚醒
  • 為什麼使用多執行緒

    提高程式的處理效率。建立執行緒是一個開銷很大,使用執行緒池可以避免執行緒建立和銷燬帶來的效能開銷,避免大量執行緒因搶佔系統資源而導致阻塞,能夠提供對執行緒的簡單管理,定時執行、間隔執行等。

  • 什麼是執行緒池

    執行緒池的基本思想就是一種物件池,就是在程式啟動的時候開闢一塊空間,然後用於存放執行緒(未死亡),裡面的執行緒執行排程有執行緒池來處理。當需要使用執行緒處理時,就從池中獲取一個物件執行任務,執行完任務之後,再將執行緒池放回到執行緒池中,這樣就避免了新建執行緒帶來的開銷,可以服務已經建立好的執行緒,達到節省資源的目的。

  • 執行緒池的主要元件

    • 執行緒池管理器-ThreadPool

      用於建立執行緒和管理執行緒,包括建立執行緒池、銷燬執行緒池、新增任務

    • 工作執行緒-WorkThread

      執行緒池中的執行緒,用於執行任務,沒有任務時處於等待狀態

    • 任務介面-Task

      每個任務必須實現的介面,以供執行緒排程任務執行,它規定了任務的入口、任務的收尾工作以及任務的執行狀態

    • 任務佇列-taskQueue

      沒有被排程的任務放入到該地方,等待工作執行緒排程執行,起到緩衝的作用。

  • 建立執行緒池的方法

    • newCachedThreadPool

      建立的是一個可快取的執行緒池,如果執行緒池長度超過處理需要,則收回空閒執行緒,若無,則新建執行緒。

      public static void main(String[] args) {
              ExecutorService threadPool = Executors.newCachedThreadPool();
              for (int i = 0; i < 1000; i++) {
                  final int index = i;
                  try {
                      Thread.sleep(index * 1000);
                  } catch (Exception e) {
                      e.printStackTrace();
                  }
                  threadPool.execute(new Runnable() {
                      @Override
                      public void run() {
                          System.out.println(Thread.currentThread().getName()+":"+index);
                      }
                  });
              }
          }
      
    • newFixedThreadPool

      建立的是一個定長的執行緒池,在建立時指定執行緒數。超出的執行緒會在佇列裡面等待。

      public static void main(String[] args) {
              ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
              for (int i = 0; i < 10; i++) {
                  final int index = i;
                  fixedThreadPool.execute(new Runnable() {
                      @Override
                      public void run() {
                          try {
                              System.out.println(Thread.currentThread().getName() + ":" + index);
                              //三個執行緒併發
                              Thread.sleep(2000);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                  });
              }
          }
      
    • newScheduledThreadPool

      建立的是一個定長的執行緒池,支援定時週期性的執行任務

      public static void main(String[] args) {
              ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
              // 表示延遲1秒後每3秒執行一次
              scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
                  @Override
                  public void run() {
                      System.out.println(Thread.currentThread().getName() + ": delay 1 seconds, and excute every 3 seconds");
                  }
              }, 1, 3, TimeUnit.SECONDS);
          }
      
    • newSingleThreadExecutor

      建立只有1個執行緒的執行緒池,只會用唯一的工作執行緒執行任務,保證所有任務按照佇列裡的順序(FIFO,LIFO,優先順序)進行處理。

      public static void main(String[] args) {
              ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
              for (int i = 0; i < 10; i++) {
                  final int index = i;
                  singleThreadPool.execute(new Runnable() {
                      @Override
                      public void run() {
                          try {
                              System.out.println(Thread.currentThread().getName() + ":" + index);
                              Thread.sleep(1000);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                  });
              }
          }
      
  • 建立執行緒池的關鍵類ThreadPoolExecutor

    通過分析上面四種方法的原始碼可以看到,其實他們都是通過ThreadPoolExecutor建立的執行緒池,可見ThreadPoolExecutor是建立執行緒池的一個關鍵類,他在JDK中的UML類關係圖如圖所示:

    我們可以通過ThreadPoolExecutor來建立一個執行緒池

    public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler) 
    

    通過建構函式我們可以看到建立執行緒池所需要的幾個引數

    • corePoolSize

      執行緒池的基本大小,當提交一個任務到執行緒池時,執行緒池會建立一個執行緒來執行任務,即使其他空閒的基本執行緒能夠執行新任務也會建立執行緒,等到需要執行的任務數大於執行緒池基本大小時就不再建立。如果呼叫了執行緒池的prestartAllCoreThreads方法,執行緒池會提前建立並啟動所有基本執行緒。

    • maximumPoolSize

      執行緒池最大大小,執行緒池允許建立的最大執行緒數。如果佇列滿了,並且已建立的執行緒數小於最大執行緒數,則執行緒池會再建立新的執行緒執行任務。值得注意的是如果使用了無界的任務佇列這個引數就沒什麼效果。

    • keepAliveTime

      執行緒活動保持時間,執行緒池的工作執行緒空閒後,保持存活的時間。所以如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高執行緒的利用率。

    • TimeUnit

      執行緒活動保持時間的單位,保持存活的時間的時間單位,可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

    • workQueue

      任務佇列,用於儲存等待執行的任務的阻塞佇列。

      BlockingQueue workQueue = null;
      workQueue = new ArrayBlockingQueue<>(5);//基於陣列的先進先出佇列,有界
      workQueue = new LinkedBlockingQueue<>();//基於連結串列的先進先出佇列,無界
      workQueue = new SynchronousQueue<>();//無緩衝的等待佇列,無界
      

      根據java.util.concurrent.ThreadPoolExecutor#execute原始碼可以看出執行緒池的處理流程

      #獲取執行緒數
      int c = ctl.get();
      #1.執行緒數量未達到corePoolSize,則新建一個執行緒(核心執行緒)執行任務
      if (workerCountOf(c) < corePoolSize) {
      	if (addWorker(command, true))
      		return;
      	c = ctl.get();
      }
      #2.執行緒數量達到了corePools,則將任務移入佇列等待
      if (isRunning(c) && workQueue.offer(command)) {
      	int recheck = ctl.get();
      	if (! isRunning(recheck) && remove(command))
      		reject(command);
      	else if (workerCountOf(recheck) == 0)
      		addWorker(null, false);
      }
      #3.佇列已滿,新建執行緒(非核心執行緒)執行任務
      else if (!addWorker(command, false))
      	#4.佇列已滿,匯流排程數又達到了maximumPoolSize,就會由(RejectedExecutionHandler)丟擲異常
      	reject(command);      
      
    • threadFactory

      執行緒工廠,用於設定建立執行緒的工廠,可以通過執行緒工廠給每個創建出來的執行緒設定更有意義的名字,Debug和定位問題時非常又幫助。

    • RejectedExecutionHandler

      拒絕策略,當佇列和執行緒池都滿了,說明執行緒池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略預設情況下是AbortPolicy,表示無法處理新任務時丟擲異常。

      以下是JDK1.5提供的四種策略:

      ​ AbortPolicy:丟棄任務並丟擲RejectedExecutionException異常。

      ​ DiscardPolicy:丟棄任務,但是不丟擲異常。

      ​ DisCardOldSetPolicy:丟棄佇列最前面的任務,然後提交新來的任務。

      ​ CallerRunPolicy:由呼叫執行緒(提交任務的執行緒,主執行緒)處理該任務。

  • 執行緒池的狀態

    1. 執行緒池的初始化狀態是RUNNING,能夠接收新任務,以及對已新增的任務進行處理。
    2. 執行緒池處在SHUTDOWN狀態時,不接收新任務,但能處理已新增的任務。 呼叫執行緒池的shutdown()介面時,執行緒池由RUNNING -> SHUTDOWN。
    3. 執行緒池處在STOP狀態時,不接收新任務,不處理已新增的任務,並且會中斷正在處理的任務。 呼叫執行緒池的shutdownNow()介面時,執行緒池由(RUNNING or SHUTDOWN ) -> STOP。
    4. 當所有的任務已終止,ctl記錄的”任務數量”為0,執行緒池會變為TIDYING狀態。當執行緒池變為TIDYING狀態時,會執行鉤子函式terminated()。terminated()在ThreadPoolExecutor類中是空的,若使用者想線上程池變為TIDYING時,進行相應的處理;可以通過過載terminated()函式來實現。
    5. 當執行緒池在SHUTDOWN狀態下,阻塞佇列為空並且執行緒池中執行的任務也為空時,就會由 SHUTDOWN -> TIDYING。
    6. 當執行緒池在STOP狀態下,執行緒池中執行的任務為空時,就會由STOP -> TIDYING。 執行緒池徹底終止,就變成TERMINATED狀態。執行緒池處在TIDYING狀態時,執行完terminated()之後,就會由 TIDYING -> TERMINATED。
  • 為什麼阿里java開發手冊不推薦使用Executors建立執行緒池,而是推薦手動建立執行緒池

    newFixedThreadPool和newSingleThreadExecutor主要是因為堆積的請求處理佇列可能會耗費非常大的記憶體,甚至OOM。

    #newSingleThreadExecutor
    public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
        }
     
    #newFixedThreadPool
    public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
    

    由原始碼可以看到這兩個執行緒池都是使用LinkedBlockingQueue作為任務佇列的,這個是無界的佇列,即使你限制了執行緒數,但是任務還是會被放到佇列中進行堆積,最終導致OOM。

    newCachedThreadPool和newScheduledThreadPool主要是因為最大執行緒數是Integer.MAX_VALUE,可能會建立數量非常多的執行緒,甚至OOM。

    #newCachedThreadPool
    public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }
      
    #ScheduledThreadPoolExecutor
     public ScheduledThreadPoolExecutor(int corePoolSize) {
            super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
                  new DelayedWorkQueue());
        }
    

    由原始碼可以看到maximumPoolSize為Integer.MAX_VALUE,這樣如果任務很多的時候會導致床和非常多的執行緒。