Java併發編程(二)-線程池和AQS介紹

什麼是線程池?爲什麼要有線程池?

  線程池是一種多線程處理形式,處理過程中將任務添加到隊列,然後在創建線程後自動啓動這些任務。
  如果併發的線程數量很多,並且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因爲頻繁創建線程和銷燬線程需要時間。
  那麼有沒有一種辦法使得線程可以複用,就是執行完一個任務,並不被銷燬,而是可以繼續執行其他的任務?
  在Java中可以通過線程池來達到這樣的效果。今天我們就來詳細講解一下Java的線程池,首先我們從最核心的ThreadPoolExecutor類中的方法講起

ThreadPoolExecutor類

  JDK1.8中的java.uitl.concurrent.ThreadPoolExecutor類是線程池中最核心的一個類,因此如果要透徹地瞭解Java中的線程池,必須先了解這個類。下面我們來看一下ThreadPoolExecutor類的部分實現源碼。

public class ThreadPoolExecutor extends AbstractExecutorService {
	public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
	public ThreadPoolExecutor(int corePoolSize,
	                          int maximumPoolSize,
	                          long keepAliveTime,
	                          TimeUnit unit,
	                          BlockingQueue<Runnable> workQueue,
	                          RejectedExecutionHandler handler) {
	    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
	         Executors.defaultThreadFactory(), handler);
	}
	public ThreadPoolExecutor(int corePoolSize,
	                          int maximumPoolSize,
	                          long keepAliveTime,
	                          TimeUnit unit,
	                          BlockingQueue<Runnable> workQueue,
	                          ThreadFactory threadFactory) {
	    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
	         threadFactory, defaultHandler);
	}
	public ThreadPoolExecutor(int corePoolSize,
	                          int maximumPoolSize,
	                          long keepAliveTime,
	                          TimeUnit unit,
	                          BlockingQueue<Runnable> workQueue,
	                          ThreadFactory threadFactory,
	                          RejectedExecutionHandler handler) {
	    if (corePoolSize < 0 ||
	        maximumPoolSize <= 0 ||
	        maximumPoolSize < corePoolSize ||
	        keepAliveTime < 0)
	        throw new IllegalArgumentException();
	    if (workQueue == null || threadFactory == null || handler == null)
	        throw new NullPointerException();
	    this.acc = System.getSecurityManager() == null ?
	            null :
	            AccessController.getContext();
	    this.corePoolSize = corePoolSize;
	    this.maximumPoolSize = maximumPoolSize;
	    this.workQueue = workQueue;
	    this.keepAliveTime = unit.toNanos(keepAliveTime);
	    this.threadFactory = threadFactory;
	    this.handler = handler;
	    }
}

  從上面的代碼可以得知,ThreadPoolExecutor繼承了AbstractExecutorService類,並提供了四個構造器,事實上,通過觀察每個構造器的源碼具體實現,發現前面三個構造器都是調用的第四個構造器進行的初始化工作,以下介紹構成方法參數的意思。

    corePoolSize

  核心池的大小,如果池中的實際線程數小於corePoolSize,無論是否其中有空閒的線程,都會給新的任務產生新的線程
  如果池中的線程數>corePoolSize and <maximumPoolSize,而又有空閒線程,就給新任務使用空閒線程,如沒有空閒線程,則產生新線程
    如果池中的線程數=maximumPoolSize,則有空閒線程使用空閒線程,否則新任務放入workQueue。

    maximumPoolSize

  線程池最大線程數,這個參數也是一個非常重要的參數,它表示在線程池中最多能創建多少個線程;

    keepAliveTime

  表示線程沒有任務執行時最多保持多久時間會終止。默認情況下,只有當線程池中的線程數大於corePoolSize時,keepAliveTime纔會起作用,直到線程池中的線程數不大於corePoolSize,即當線程池中的線程數大於corePoolSize時,如果一個線程空閒的時間達到keepAliveTime,則會終止,直到線程池中的線程數不超過corePoolSize。但是如果調用了allowCoreThreadTimeOut(boolean)方法,在線程池中的線程數不大於corePoolSize時,keepAliveTime參數也會起作用,直到線程池中的線程數爲0;

    unit

  參數keepAliveTime的時間單位,有7種取值,在TimeUnit類中有7種靜態屬性:

    TimeUnit.DAYS;               //天
    TimeUnit.HOURS;             //小時
    TimeUnit.MINUTES;           //分鐘
    TimeUnit.SECONDS;           //秒
    TimeUnit.MILLISECONDS;      //毫秒
    TimeUnit.MICROSECONDS;      //微妙
    TimeUnit.NANOSECONDS;       //納秒

    workQueue

  一個阻塞隊列,用來存儲等待執行的任務,這個參數的選擇也很重要,會對線程池的運行過程產生重大影響, BlockingQueue是個接口,有如下實現類:
  ArrayBlockQueue:一個由數組支持的有界阻塞隊列。此隊列按 FIFO(先進先出)原則對元素進行排序。創建其對象必須明確大小,像數組一樣。
  LinkedBlockQueue:一個可改變大小的阻塞隊列。此隊列按 FIFO(先進先出)原則對元素進行排序。創建其對象如果沒有明確大小,默認值是Integer.MAX_VALUE。鏈接隊列的吞吐量通常要高於基於數組的隊列,但是在大多數併發應用程序中,其可預知的性能要低。
  PriorityBlockingQueue:類似於LinkedBlockingQueue,但其所含對象的排序不是FIFO,而是依據對象的自然排序順序或者是構造函數所帶的Comparator決定的順序。
  SynchronousQueue:同步隊列。同步隊列沒有任何容量,每個插入必須等待另一個線程移除,反之亦然。

threadFactory

  線程工廠,主要用來創建線程;

handler

  表示當拒絕處理任務時的策略,有以下四種取值:
  ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
  ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。
  ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
  ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務

ThreadPoolExecutor、AbstractExecutorService、ExecutorService和Executor幾個之間的關係

  Executor是一個頂層接口,在它裏面只聲明瞭一個方法execute(Runnable),返回值爲void,參數爲Runnable類型,從字面意思可以理解,就是用來執行傳進去的任務的;
  然後ExecutorService接口繼承了Executor接口,並聲明瞭一些方法:submit、invokeAll、invokeAny以及shutDown等;
  接着抽象類AbstractExecutorService實現了ExecutorService接口,基本實現了ExecutorService中聲明的所有方法;
  最後ThreadPoolExecutor繼承了類AbstractExecutorService。

ThreadPoolExecutor類中有幾個非常重要的方法

   execute()方法實際上是Executor中聲明的方法,在ThreadPoolExecutor進行了具體的實現,這個方法是ThreadPoolExecutor的核心方法,通過這個方法可以向線程池提交一個任務,交由線程池去執行。
   submit()方法是在ExecutorService中聲明的方法,在AbstractExecutorService就已經有了具體的實現,在ThreadPoolExecutor中並沒有對其進行重寫,這個方法也是用來向線程池提交任務的,但是它和execute()方法不同,它能夠返回任務執行的結果,去看submit()方法的實現,會發現它實際上還是調用的execute()方法,只不過它利用了Future來獲取任務執行結果(Future相關內容將在下一篇講述)。
   shutdown()當線程池調用該方法時,線程池的狀態則立刻變成SHUTDOWN狀態。此時,則不能再往線程池中添加任何任務,否則將會拋出RejectedExecutionException異常。但是,此時線程池不會立刻退出,直到添加到線程池中的任務都已經處理完成,纔會退出。
   shutdownNow()當線程池調用該方法時,線程池的狀態立刻變成STOP狀態,並試圖停止所有正在執行的線程,不再處理還在池隊列中等待的任務,當然,它會返回那些未執行的任務。
它試圖終止線程的方法是通過調用Thread.interrupt()方法來實現的,但是大家知道,這種方法的作用有限,如果線程中沒有sleep 、wait、Condition、定時鎖等應用, interrupt()方法是無法中斷當前的線程的。所以,ShutdownNow()並不代表線程池就一定立即就能退出,它可能必須要等待所有正在執行的任務都執行完成了才能退出。

Executors類的靜態方法

  Java doc中,並不提倡我們直接使用ThreadPoolExecutor,而是使用Executors類中提供的幾個靜態方法來創建線程池(PS:不過阿里巴巴的代碼規範插件推薦使用ThreadPoolExecutor):

   newCachedThreadPool

  創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。這種類型的線程池特點是:
  工作線程的創建數量幾乎沒有限制(其實也有限制的,數目爲Interger. MAX_VALUE), 這樣可靈活的往線程池中添加線程。
  如果長時間沒有往線程池中提交任務,即如果工作線程空閒了指定的時間(默認爲1分鐘),則該工作線程將自動終止。終止後,如果你又提交了新的任務,則線程池重新創建一個工作線程。
  在使用CachedThreadPool時,一定要注意控制任務的數量,否則,由於大量線程同時運行,很有會造成系統癱瘓。

   newFixedThreadPool

  創建一個指定工作線程數量的線程池。每當提交一個任務就創建一個工作線程,如果工作線程數量達到線程池初始的最大數,則將提交的任務存入到池隊列中。
  FixedThreadPool是一個典型且優秀的線程池,它具有線程池提高程序效率和節省創建線程時所耗的開銷的優點。但是,在線程池空閒時,即線程池中沒有可運行任務時,它不會釋放工作線程,還會佔用一定的系統資源。

   newSingleThreadExecutor

  創建一個單線程化的Executor,即只創建唯一的工作者線程來執行任務,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。如果這個線程異常結束,會有另一個取代它,保證順序執行。單工作線程最大的特點是可保證順序地執行各個任務,並且在任意給定的時間不會有多個線程是活動的。

   newScheduleThreadPool

  創建一個定長的線程池,而且支持定時的以及週期性的任務執行,支持定時及週期性任務執行。

   newWorkStealingPool

  創建一個帶並行級別的線程池,並行級別決定了同一時刻最多有多少個線程在執行,如不傳並行級別參數,將默認爲當前系統的CPU個數。

線程池的使用

  通過Executors.newFixedThreadPool(10)靜態方法創建一個10個工作線程數量的線程池。
  通過new ThreadPoolExecutor()創建,具體參數的意思上文有詳細介紹。如果線程池不再使用時,最好使用shutdown()方法關閉,代碼如下。

public class TestThreadPool {
    public static void main(String str[]){
        testExecutors();
        testThreadPool();
    }

    /**
     * 使用Executors靜態方法創建線程池
    */
    public static void testExecutors(){
        //推薦使用newFixedThreadPool線程池
        ExecutorService executorService=Executors.newFixedThreadPool(10);
        //往線程池添加10個線程
        for(int i=1;i<=10;i++){
            executorService.execute(new RunnableDemo("Executors:"+i));
        }
        executorService.shutdown();
    }

    /**
     * 使用ThreadPoolExecutor創建線程池
    */
    public static void testThreadPool(){
        ThreadPoolExecutor threadPoolExecutor= new ThreadPoolExecutor(
                5, 10, 60, TimeUnit.SECONDS, new SynchronousQueue<>(),new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                //如果線程池添加線程失敗,則暫停1s後再同步線程
                try {
                    System.out.println("線程添加失敗");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        for(int i=1;i<=10;i++){
            threadPoolExecutor.execute(new RunnableDemo("ThreadPool:"+i));
        }
        threadPoolExecutor.shutdown();
    }
}

AQS

  AQS是一個用來構建鎖和同步器的框架,使用AQS能簡單且高效地構造出應用廣泛的大量的同步器,比如我們提到的ReentrantLock,Semaphore,其他的諸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基於AQS的。當然,我們自己也能利用AQS非常輕鬆容易地構造出符合我們自己需求的同步器。

AQS 原理概覽

  AQS核心思想是,如果被請求的共享資源空閒,則將當前請求資源的線程設置爲有效的工作線程,並且將共享資源設置爲鎖定狀態。如果被請求的共享資源被佔用,那麼就需要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列鎖實現的,即將暫時獲取不到鎖的線程加入到隊列中。
  AQS使用一個int成員變量state來表示同步狀態,如ReentrantLock的lock方法,實際上就是通過compareAndSetState(0, 1)方法使state值0加到1,維護state值來獲取鎖和釋放鎖。通過內置的FIFO隊列來完成獲取資源線程的排隊工作(公平鎖)。AQS使用CAS對該同步狀態進行原子操作實現對其值的修改。

private volatile int state;//共享變量,使用volatile修飾保證線程可見性

  狀態信息通過protected類型的getState,setState,compareAndSetState進行操作

//返回同步狀態的當前值
protected final int getState() {  
        return state;
}
 // 設置同步狀態的值
protected final void setState(int newState) { 
        state = newState;
}
//原子地(CAS操作)將同步狀態值設置爲給定值update如果當前同步狀態的值等於expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS定義兩種資源共享方式

Exclusive(獨佔)

  只有一個線程能執行,如ReentrantLock。又可分爲公平鎖和非公平鎖:
  公平鎖:按照線程在隊列中的排隊順序,先到者先拿到鎖

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

  非公平鎖:當線程要獲取鎖時,無視隊列順序直接去搶鎖,誰搶到就是誰的

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

  通過上述兩者比較可以發現,公平鎖相對於非公平鎖多了個hasQueuedPredecessors()方法判斷"當前線程"是不是在CLH隊列的隊首,來返回AQS中是不是有比“當前線程”等待更久的線程

Share(共享)

  多個線程可同時執行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 等。通常同步器時需要重寫下面幾個AQS提供的模板方法:

isHeldExclusively()//該線程是否正在獨佔資源。只有用到condition才需要去實現它。
tryAcquire(int)//獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。
tryRelease(int)//獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
tryAcquireShared(int)//共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
tryReleaseShared(int)//共享方式。嘗試釋放資源,成功則返回true,失敗則返回false。

  以CountDownLatch以例,任務分爲N個子線程去執行,state也初始化爲N(注意N要與線程個數一致)。這N個子線程是並行執行的,每個子線程執行完後countDown()一次,state會CAS(Compare and Swap)減1。等到所有子線程都執行完後(即state=0),會unpark()主調用線程,然後主調用線程就會從await()函數返回,繼續後餘動作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章