首頁 >Java >java教程 >Java執行緒池的幾種實作方法及常見問題解答

Java執行緒池的幾種實作方法及常見問題解答

黄舟
黄舟原創
2017-01-20 11:15:171466瀏覽

下面小編就為大家帶來一篇Java執行緒池的幾種實作方法及常見問題解答。小編覺得蠻不錯的,現在分享給大家,也給大家做個參考。一起跟著小編過來看看吧

工作中,常常會牽扯到線程。例如有些任務,常常會交與線程去非同步執行。抑或服務端程式為每個請求單獨建立一個執行緒處理任務。線程之外的,例如我們用的資料庫連線。這些創建銷毀或開啟關閉的操作,非常影響系統效能。所以,「池」的用處就凸顯出來了。

1. 為什麼要使用執行緒池

在3.6.1節介紹的實作方式中,對每個客戶都分配一個新的工作執行緒。當工作執行緒與客戶通訊結束,這個執行緒就被銷毀。這種實作方式有以下不足之處:

•伺服器建立和銷毀工作的開銷( 包括所花費的時間和系統資源 )很大。這一項不用解釋,可以去查"線程創建過程"。除了機器本身所做的工作,我們還要實例化,啟動,這些都需要佔用堆疊資源。

•除了建立和銷毀執行緒的開銷之外,活動的執行緒也消耗系統資源。 這個應該是對堆疊資源的消耗,猜測資料庫連線數設定一個合理的值,也有這個考慮。

•如果執行緒數目固定,且每個執行緒都有很長的宣告週期,那麼執行緒切換也是相對固定的。不同的作業系統有不同的切換週期,通常20ms左右。這裡說的切換是在jvm以及底層作業系統的調度下,執行緒之間轉讓cpu的使用權。如果頻繁創建和銷毀線程,那麼就將頻繁的切換線程,因為一個線程銷毀後,必然要讓出使用權給已經就緒的線程,使該線程獲得運行機會。在這種情況下,執行緒之間的切換就不在遵循系統的固定切換週期,切換執行緒的開銷甚至比創建和銷毀的開銷還要大。

相對來說,使用線程池,會預先建立一些線程,它們不斷的從工作佇列中取出任務,然後執行該任務。當工作執行緒執行完一個任務後,就會繼續執行工作佇列中的另一個任務。優點如下:

•減少了創建和銷毀的次數,每個工作執行緒都可以一直被重複使用,能執行多個任務。

•可以根據系統的承載能力,方便的調整線程池中線程的數目,防止因為消耗過量的系統資源而導致系統崩潰。

2. 線程池的簡單實現

下面是自己寫的一個簡單的線程池,也是從Java網絡編程這本書上直接照著敲出來的

package thread;
 
import java.util.LinkedList;
 
/**
 * 线程池的实现,根据常规线程池的长度,最大长度,队列长度,我们可以增加数目限制实现
 * @author Han
 */
public class MyThreadPool extends ThreadGroup{
  //cpu 数量 ---Runtime.getRuntime().availableProcessors();
  //是否关闭
  private boolean isClosed = false;
  //队列
  private LinkedList<Runnable> workQueue;
  //线程池id
  private static int threadPoolID;
  private int threadID;
  public MyThreadPool(int poolSize){
    super("MyThreadPool."+threadPoolID);
    threadPoolID++;
    setDaemon(true);
    workQueue = new LinkedList<Runnable>();
    for(int i = 0;i<poolSize;i++){
      new WorkThread().start();
    }
  }
  //这里可以换成ConcurrentLinkedQueue,就可以避免使用synchronized的效率问题
  public synchronized void execute(Runnable task){
    if(isClosed){
      throw new IllegalStateException("连接池已经关闭...");
    }else{
      workQueue.add(task);
      notify();
    }
  }
   
  protected synchronized Runnable getTask() throws InterruptedException {
    while(workQueue.size() == 0){
      if(isClosed){
        return null;
      }
      wait();
    }
    return workQueue.removeFirst();
  }
   
  public synchronized void close(){
    if(!isClosed){
      isClosed = true;
      workQueue.clear();
      interrupt();
    }
  }
   
  public void join(){
    synchronized (this) {
      isClosed = true;
      notifyAll();
    }
    Thread[] threads = new Thread[activeCount()];
    int count = enumerate(threads);
    for(int i = 0;i<count;i++){
      try {
        threads[i].join();
      } catch (Exception e) {
      }
    }
  }
   
  class WorkThread extends Thread{
    public WorkThread(){
      super(MyThreadPool.this,"workThread"+(threadID++));
      System.out.println("create...");
    }
    @Override
    public void run() {
      while(!isInterrupted()){
        System.out.println("run..");
        Runnable task = null;
        try {
          //这是一个阻塞方法
          task = getTask();
           
        } catch (Exception e) {
           
        }
        if(task != null){
          task.run();
        }else{
          break;
        }
      }
    }
  }
}

該線程池主要定義了一個工作隊列和一些預先創建的線程。只要呼叫execute方法,就可以向執行緒提交任務。

後面執行緒在沒有任務的時候,會阻塞在getTask(),直到有新任務進來被喚醒。

join和close都可以用來關閉執行緒池。不同的是,join會把佇列中的任務執行完,而close則立刻清空隊列,中斷所有的工作執行緒。 close()中的interrupt()相當於呼叫了ThreadGroup中包含子執行緒的各自的interrupt(),所以當有執行緒處於wait或sleep時,都會拋出InterruptException

測試類別如下:

public class TestMyThreadPool {
  public static void main(String[] args) throws InterruptedException {
    MyThreadPool pool = new MyThreadPool(3);
    for(int i = 0;i<10;i++){
      pool.execute(new Runnable() {
        @Override
        public void run() {
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
          }
          System.out.println("working...");
        }
      });
    }
    pool.join();
    //pool.close();
  }
}

3. jdk類庫提供的線程池

java提供了很好的線程池實現,比我們自己的實現要更加健壯以及高效,同時功能也更加強大。

類圖如下:

關於這類線程池,前輩們已經有很好的講解。任意百度下java線程池,都有寫的非常詳細的例子和教程,這裡就不再贅述。

4. spring注入線程池

在使用spring框架的時候,如果我們用java提供的方法來創建線程池,在多線程應用中非常不方便管理,而且不符合我們使用spring的思想。 (雖然spring可以透過靜態方法注入)

其實,Spring本身也提供了很好的執行緒池的實作。這個類別叫做ThreadPoolTask​​Executor。

在spring中的配置如下:

<bean id="executorService" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
    <property name="corePoolSize" value="${threadpool.corePoolSize}" />
    <!-- 线程池维护线程的最少数量 -->
    <property name="keepAliveSeconds" value="${threadpool.keepAliveSeconds}" />
    <!-- 线程池维护线程所允许的空闲时间 -->
    <property name="maxPoolSize" value="${threadpool.maxPoolSize}" />
    <!-- 线程池维护线程的最大数量 -->
    <property name="queueCapacity" value="${threadpool.queueCapacity}" />
    <!-- 线程池所使用的缓冲队列 -->
  </bean>

5. 使用執行緒池的注意事項

•死鎖

任何多執行緒程式都有死鎖的風險,最簡單的情形是兩個執行緒AB,A持有鎖1,請求鎖2,B持有鎖2,請求鎖1。 (這種情況在mysql的排他鎖也會出現,不會資料庫會直接報錯提示)。執行緒池中還有另一種死鎖:假設執行緒池中的所有工作執行緒都在執行各自任務時被阻塞,它們在等待某個任務A的執行結果。而任務A卻處於佇列中,由於沒有空閒線程,一直無法得以執行。這樣執行緒池的所有資源將一直阻塞下去,死鎖也就產生了。

•系統資源不足

 如果執行緒池中的執行緒數目非常多,這些執行緒會消耗包括記憶體和其他系統資源在內的大量資源,從而嚴重影響系統效能。

•併發錯誤

執行緒池的工作佇列依靠wait()和notify()方法來讓工作執行緒及時取得任務,但這兩個方法難以使用。如果程式碼錯誤,可能會遺失通知,導致工作執行緒一直保持空閒的狀態,無視於工作佇列中需要處理的任務。因為最好使用一些比較成熟的線程池。

•執行緒洩漏

使用執行緒池的一個嚴重風險是執行緒洩漏。對於工作線程數目固定的線程池,如果工作線程在執行任務時拋出RuntimeException或Error,並且這些異常或錯誤沒有被捕獲,那麼這個工作線程就異常終止,使線程池永久丟失了一個線程。 (這一點太有趣)

另一種情況是,工作線程在執行一個任務時被阻塞,如果等待用戶的輸入數據,但是用戶一直不輸入數據,導致這個線程一直被阻塞。這樣的工作執行緒名存實亡,它實際上不執行任何任務了。如果執行緒池中的所有執行緒都處於這樣的狀態,那麼執行緒池就無法加入新的任務了。

•任務過載

當工作執行緒佇列中有大量排隊等待執行的任務時,這些任務本身可能會消耗太多的系統資源和造成資源缺乏。

綜上所述,使用執行緒池時,要遵循以下原則:

1. 如果任務A在執行過程中需要同步等待任務B的執行結果,那麼任務A不適合加入到執行緒池的工作佇列中。如果把像任務A一樣的需要等待其他任務執行結果的加入到隊列中,可能造成死鎖

2. 如果執行某個任務時可能會阻塞,並且是長時間的阻塞,則應該設定超時時間,避免工作執行緒永久的阻塞下去而導致執行緒洩漏。在伺服器才程式中,當執行緒等待客戶連接,或等待客戶傳送的資料時,都可能造成阻塞,可以透過以下方式設定時間:

呼叫ServerSocket的setSotimeout方法,設定等待客戶連接的逾時時間。

對於每個與客戶連接的socket,呼叫該socket的setSoTImeout方法,設定等待客戶傳送資料的逾時時間。

3. 了解任務的特點,分析任務是執行經常會阻塞io操作,還是執行一直不會阻塞的運算操作。前者時斷時續的佔用cpu,而後者則有較高的利用率。預計完成任務大概需要多長時間,是短時間任務還是長時間任務,然後根據任務的特點,對任務進行分類,然後把不同類型的任務加入到不同的線程池的工作隊列中,這樣就可以根據任務的特點,分配調整每個執行緒池

4. 調整執行緒池的大小。執行緒池的最佳大小主要取決於系統的可用cpu的數目,以及工作佇列中任務的特性。假如一個具有N個cpu的系統上只有一個工作隊列,並且其中全部是運算性質(不會阻塞)的任務,那麼當線程池擁有N或N+1個工作線程時,一般會獲得最大的cpu使用率。

如果工作佇列中包含會執行IO操作並經常阻塞的任務,則要讓執行緒池的大小超過可用 cpu的數量,因為並不是所有的工作執行緒都一直在工作。選擇一個典型的任務,然後估計在執行這個任務的工程中,等待時間與實際佔用cpu進行運算的時間的比例WT/ST。對於一個有N個cpu的系統,需要設定大約N*(1+WT/ST)個執行緒來確保cpu得到充分利用。

當然,cpu利用率不是調整線程池過程中唯一要考慮的事項,隨著線程池工作數目的增長,還會碰到內存或者其他資源的限制,如套接字,打開的文件句柄或數據庫連接數目等。要確保多執行緒消耗的系統資源在系統承受的範圍內。

5. 避免任務過載。伺服器應依系統的承載能力,限制客戶並發連線的數目。當客戶的連接超過了限制值,伺服器可以拒絕連接,並進行友善提示,或限制佇列長度。

以上就是Java執行緒池的幾種實作方法及常見問題解答的內容,更多相關內容請關注PHP中文網(www.php.cn)!


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn