Java併發技術研究

1.JAVA併發編程基礎

Java從誕生開始就選擇了內置對多線程的支持。但是過多地創建線程和對線程的不當管理也容易造成問題。因而對於開發人員如何編寫優秀的併發程序是一個不小的挑戰。

1.1 多線程

1.1.1 多線程簡單示例

一個Java程序從main()方法開始執行,然後按照既定的代碼邏輯執行,看似沒有其他線程參與,但實際上Java程序天生就是多線程程序,因爲執行main()方法的是一個名稱爲main的線程

package ThreadDemo;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class MultiThread{

    public static void main(String[] args) {
        // 獲取Java線程管理MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要獲取同步的monitor和synchronizer信息,僅獲取線程和線程堆棧信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        // 遍歷線程信息,僅打印線程ID和線程名稱信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.
                    getThreadName());
        }
    }
}

控制檯輸入結果:
[5] Monitor Ctrl-Break   // console控制線程
[4] Signal Dispatcher    // 分發處理髮送給JVM信號的線程
[3] Finalizer            // 調用對象finalize方法的線程
[2] Reference Handler    // 清除reference引用線程
[1] main                 // 主線程

1.1.2 爲什麼使用多線程

  • 更多的處理器核心
  • 更快的響應時間
  • 更好的編程模型

1.1.3 線程的狀態

  • Java線程的狀態
    這裏寫圖片描述

  • Java線程狀態變遷
    這裏寫圖片描述
    線程創建之後,調用start()方法開始運行。當線程執行wait()方法之後,線程進入等待狀態。

    進入等待狀態的線程需要依靠其他線程的通知才能夠返回到運行狀態,而超時等待狀態相當於在等待狀態的基礎上增加了超時限制,也就是超時時間到達時將會返回到運行狀態。

    當線程調用同步方法時,在沒有獲取到鎖的情況下,線程將會進入到阻塞狀態。線程在執行Runnable的run()方法之後將會進入到終止狀態。

1.1.4 Daemon線程

Daemon線程是一種後臺支持型線程。當一個Java虛擬機中不存在非Daemon線程的時候,Java虛擬機將會退出。

通過調用Thread.setDaemon(true)將線程設置爲Daemon線程。注意:設置線程的Demon屬性需要在啓動線程之前設置。

1.1.5 ThreadLocal(易混淆)

ThreadLocal的作用是提供線程內的局部變量,這種變量在線程的生命週期內起作用,減少同一個線程內多個函數或者組件之間一些公共變量的傳遞複雜度。

    public static class ThreadShareList {
        private static final ThreadLocal<ArrayList<String>> threadLocal = new ThreadLocal<ArrayList<String>>();  

        public static ArrayList<String> getCurrList() {  
            // 獲取當前線程內共享的arrayList  
            ArrayList<String> arrayList = threadLocal.get();  
            if(null==arrayList) {  
                arrayList = new ArrayList<String>();
                threadLocal.set(arrayList);  
            }       
            return arrayList;  
        }  
    }

Jdk7提供的類ThreadLocalRandom使用Threadlocal機制實現創建單線程相關的random。

1.1.6 線程安全

在多線程編程中,可能會出現多個線程併發訪問同一個資源(內存區(變量,數組,或對象)、系統(數據庫,web services等)、文件)的情況。如果不對這樣的訪問做控制,就可能出現不可預知的結果。這就是線程安全問題。

在Java多線程編程當中,提供了以下幾種方式來實現線程安全:

  • 隱形鎖(Synchronized)和顯式鎖(Lock):屬於互斥同步方法,是重量級的多線程同步機制,可能會引起上下文切換和線程調度,它同時提供內存可見性、有序性和原子性。

  • volatile:輕量級多線程同步機制,不會引起上下文切換和線程調度。僅提供內存可見性、有序性保證,不提供原子性。

  • CAS原子指令:屬於非阻塞同步方法,輕量級多線程同步機制,不會引起上下文切換和線程調度。它同時提供內存可見性、有序性和原子化更新保證。比如後面分享的Atomic機制

1.2 volatile關鍵字

Java支持多個線程同時訪問一個對象或者對象的成員變量,但是程序在執行過程中,一個線程看到的變量並不一定是最新的。
這裏寫圖片描述

關鍵字volatile可以用來修飾成員變量,就是告知程序任何對該變量的訪問均需要從共享內存中獲取,而對它的改變必須同步刷新回共享內存,它能保證所有線程對變量訪問的可見性。

過多地使用volatile是不必要的,因爲它會降低程序執行的效率。

注意:volatile關鍵字無法保證原子操作的原子性,比如多個線程同時操作count++,如果需要保證原子性的話需要使用synchronized,Atomic,Lock。

1.3 synchronized關鍵字

關鍵字synchronized(隱形鎖)可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻,只能有一個線程處於方法或者同步塊中,它保證了線程對變量訪問的可見性和排他性。

用synchronized實現同步的基礎:Java中的每一個對象都可以作爲鎖。具體表現
爲以下3種形式:

  • 對於普通同步方法,鎖是當前實例對象。
  • 對於靜態同步方法,鎖是當前類的Class對象。
  • 對於同步方法塊,鎖是Synchonized括號裏配置的對象。

當一個線程試圖訪問同步代碼塊時,它首先必須得到鎖,退出或拋出異常時必須釋放鎖(synchronized自身控制)。

典型的雙重鎖示例:

/**
 * 延遲初始化,效率優化(雙重鎖機制)
 */
public class SingleInstance04 {

    private static volatile SingleInstance04 singleInstance04;

    private SingleInstance04() { }

    public static SingleInstance04 getSingleInstance04() {

        if (null == singleInstance04) {
            synchronized (singleInstance04) {
                if (null == singleInstance04) {
                    singleInstance04 = new SingleInstance04();
                }
            }
        }
        return singleInstance04;
    }
}

2.Java中的鎖

在Java中,使用synchronized關鍵字將會隱式地獲取鎖,但是它將鎖的獲取和釋放固化了,擴展性不好。比如synchronized無法解決生產者消費者模式場景。

在Jdk5以後,併發包新增了Lock接口(以及相關實現類)用來實現鎖功能,和synchronized具有類似的功能,Lock可以顯示的獲取和釋放鎖,更加靈活。如下圖所示:
這裏寫圖片描述

2.1 Lock接口

Lock正常使用的方式如下:

        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            System.out.println("Lock接口使用模式");
        } finally {
            lock.unlock();
        }

Lock接口提供的synchronized關鍵字所不具備的主要特性:

  • 嘗試非阻塞的獲取鎖
  • 能夠響應中斷地獲取鎖
  • 超時獲取鎖

嘗試非阻塞的獲取鎖示例:

    public static void main(String[] argv) {
        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            new Thread() {
                @Override
                public void run() {
                    System.out.println("Lock接口非阻塞測試1");
                    lock.tryLock();
                    System.out.println("Lock接口非阻塞測試2");
                }
            }.start();

            Thread.sleep(4000);
            System.out.println("Lock接口使用模式");
        } catch (Exception e) {
            System.out.println(e);
        } finally {
            lock.unlock();
        }
    }   

    測試結果:
    Lock接口非阻塞測試1
    Lock接口非阻塞測試2
    Lock接口使用模式 

Lock相關接口,可參考jdk中接口Lock定義。

2.2 隊列同步器(AbstractQueuedSynchronizer)

AbstractQueuedSynchronizer,是用來構建鎖或者其他同步組件的基礎框架,它使用了一個int成員變量表示同步狀態,通過內置的FIFO隊列來完成資源獲
取線程的排隊工作。

同步器面向的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等底層操作。

  • getState():獲取當前同步狀態
  • setState(int newState):設置當前同步狀態
  • compareAndSetState(int expect,int update):使用CAS設置當前狀態,該方法能夠保證狀態設置的原子性

同步器提供的模板方法基本上分爲3類:

- 獨佔式獲取與釋放同步狀態
- 共享式獲取與釋放同步狀態
- 查詢同步隊列中的等待線程情況。

自定義同步組件將使用同步器提供的模板方法來實現自己的同步語義。下面給出一個獨佔鎖的實例:

class Mutex implements Lock {

    // 靜態內部類,自定義同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 是否處於佔用狀態
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
        // 當狀態爲0的時候獲取鎖
        public boolean tryAcquire(int acquires) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        // 釋放鎖,將狀態設置爲0
        protected boolean tryRelease(int releases) {
            if (getState() == 0) throw new
                    IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        // 返回一個Condition,每個condition都包含了一個condition隊列
        Condition newCondition() { return new ConditionObject(); }
    }

    // 僅需要將操作代理到Sync上即可
    private final Sync sync = new Sync();

    public void lock() {
        sync.acquire(1);
    }

    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    public void unlock() {
        sync.release(1);
    }

    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

運行測試結果:
Thread-0acquire lock
Thread-0release lock
Thread-1acquire lock
Thread-1release lock

2.3 重入鎖(ReentrantLock)

ReentrantLock是支持重進入的鎖,該鎖能夠支持一個線程對資源的重複加鎖。換句話說重進入是指任意線程在獲取到鎖之後能夠再次獲取該鎖而不會被鎖所阻塞。

ReentrantLock中的鎖存在兩種形式

  • 公平加鎖
  • 非公平加鎖

測試重進入特性示例:

public static void main(String[] argv) {
        Lock lock = new ReentrantLock();
        lock.lock();
        lock.lock();
        try {
            new Thread() {
                @Override
                public void run() {
                    System.out.println("Lock接口非阻塞測試1");
                    lock.lock();
                    System.out.println("Lock接口非阻塞測試2");
                }
            }.start();

            Thread.sleep(4000);
            System.out.println("Lock接口使用模式");
        } catch (Exception e) {
            System.out.println(e);
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }

2.4 讀寫鎖(ReentrantReadWriteLock)

之前提到的ReentrantLock或者Mutex都是互斥排他鎖,某一時刻只允許一個線程獲取到鎖(即使是共享鎖也是存在限制的,只能最多其允許的線程併發訪問),對於讀寫模式顯然都不適應。

jdk提供了讀寫鎖,能夠實現在同一時刻可以允許多個讀線程訪問,但是在寫線程訪問時,所有的讀線程和其他寫線程均被阻塞。讀寫鎖維護了一個讀鎖,一個寫鎖。

ReentrantReadWriteLock的特性:

  • 公平性選擇
  • 重進入
  • 鎖降級

ReentrantReadWriteLock示例:

public class ReentrantReadWriteLockTest {

    static Map<String, Object> map = new HashMap<>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    // 構建一個讀鎖,一個寫鎖
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();

    // 獲取一個key對應的value
    public static Object get(String key) {
        r.lock();
        try {
            System.out.println("get: key="+key+", value="+map.get(key));
            return map.get(key);
        } finally {
            r.unlock();
        }
    }
    // 設置key對應的value,並返回舊的value
    public static Object put(String key, Object value) {
        w.lock();
        try {
            System.out.println("put: key="+key+", value="+value);
            return map.put(key, value);
        } finally {
            w.unlock();
        }
    }

    // 清空所有的內容
    public static void clear() {
        w.lock();
        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }

}

2.5 StampedLock

讀寫鎖雖然分離了讀和寫的功能,使得讀與讀之間可以併發。但是,讀和寫之間依然是衝突的。讀鎖會完全阻塞寫鎖,它使用的依然是悲觀鎖的策略,如果有大量的讀線程,它也有可能引起寫線程的“飢餓”。

StampedLock是併發包裏面Jdk8版本新增的一個鎖,可以很好的解決上述問題。

  • 寫鎖
  • 樂觀讀鎖
  • 悲觀讀鎖

Jdk8提供的一個示例:

public class Point {

    // 成員變量
    private double x, y;

    // 鎖實例
    private final StampedLock sl = new StampedLock();

    // 排它鎖-寫鎖(writeLock)
    void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    // 樂觀讀鎖(tryOptimisticRead)
    double distanceFromOrigin() {

        // 嘗試獲取樂觀讀鎖
        long stamp = sl.tryOptimisticRead();
        // 將全部變量拷貝到方法體棧內
        double currentX = x, currentY = y;
        // 檢查在獲取到讀鎖票據後,鎖有沒被其他寫線程排它性搶佔
        if (!sl.validate(stamp)) {
            // 如果被搶佔則獲取一個共享讀鎖(悲觀獲取)
            stamp = sl.readLock();
            try {
                // 將全部變量拷貝到方法體棧內
                currentX = x;
                currentY = y;
            } finally {
                // 釋放共享讀鎖
                sl.unlockRead(stamp);
            }
        }
        // 返回計算結果
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    // 使用悲觀鎖獲取讀鎖,並嘗試轉換爲寫鎖
    void moveIfAtOrigin(double newX, double newY) {
        // 這裏可以使用樂觀讀鎖替換
        long stamp = sl.readLock();
        try {
            // 如果當前點在原點則移動
            while (x == 0.0 && y == 0.0) {
                // 嘗試將獲取的讀鎖升級爲寫鎖
                long ws = sl.tryConvertToWriteLock(stamp);
                // 升級成功,則更新票據,並設置座標值,然後退出循環
                if (ws != 0L) {
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    // 讀鎖升級寫鎖失敗則釋放讀鎖,顯示獲取獨佔寫鎖,然後循環重試
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
        } finally {
            // 釋放鎖(6)
            sl.unlock(stamp);
        }
    }
}

2.6 Condition接口

Condition定義了等待/通知兩種類型的方法,當前線程調用這些方法時,需要提前獲取到Condition對象關聯的鎖。

public class ConditionUseCase {

    private Lock lock;
    private Condition condition;

    public ConditionUseCase() {
        lock = new ReentrantLock();
        condition = lock.newCondition();
    }


    public void conditionWait() throws InterruptedException {
        lock.lock();
        try {
            System.out.println("conditionWait start");
            condition.await();
            System.out.println("conditionWait end");
        } finally {
            lock.unlock();
        }
    }


    public void conditionSignal() throws InterruptedException {
        lock.lock();
        try {
            System.out.println("conditionSignal start");
            Thread.sleep(3000);
            condition.signal();
            System.out.println("conditionSignal start");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] argv) throws Exception{
        ConditionUseCase conditionUseCase = new ConditionUseCase();
        new Thread() {
            @Override
            public void run() {
                try {
                    conditionUseCase.conditionWait();
                } catch (Exception e) {

                }
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                try {
                    conditionUseCase.conditionSignal();
                } catch (Exception e) {

                }
            }
        }.start();

    }
}
測試結果:
conditionWait start
conditionSignal start
conditionSignal start
conditionWait end

3.Java併發容器及併發工具類

3.1 ConcurrentHashMap

ConcurrentHashMap是線程安全且高效的HashMap。相對於HashMap,它是線程安全的,相對於HashTable(所有訪問線程間競爭一把鎖)更加高效。

ConcurrentHashMap採用的是鎖分段技術。如需深入瞭解,可以閱讀源碼(設計的很巧妙)

3.2 Java中的阻塞隊列

對於Java中的阻塞隊列,均是使用通知模式實現。所謂通知模式,就是當生產者往滿的隊列裏添加元素時會阻塞住生產者,當消費者消費了一個隊列中的元素後,會通知生產者當前隊列可用。

Jdk1.7中提供了7個阻塞隊列,如下:

  • ArrayBlockingQueue:一個由數組結構組成的有界阻塞隊列

    // 
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }
  • LinkedBlockingQueue:一個由鏈表結構組成的有界阻塞隊列,此隊列的默認和最大長度爲Integer.MAX_VALUE,平時使用的最多

  • PriorityBlockingQueue:一個支持優先級排序的無界阻塞隊列,可以自定義類實現compareTo()接口或者指定構造參數Comparator來對元素進行排序

  • DelayQueue:一個使用優先級隊列實現的無界阻塞隊列,支持延時獲取元素。隊列中的元素必須實現Delayed接口,在創建元素時可以指定多久才能從隊列中獲取當前元素。只有在延遲期滿時才能從隊列中提取元素。

    • 緩存系統的設計中
    • 定時任務調度
  • SynchronousQueue:一個不存儲元素的阻塞隊列,每一個put操作必須等待一個take操作,否則不能繼續添加元素

  • LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列

  • LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列

3.3 Fork/Join框架

Jdk1.7提供的一個用於並行執行任務的框架,是一個把大任務分割成若干個小任務,最終彙總每個小任務結果後得到大任務結果的框架。
這裏寫圖片描述

3.4 併發工具類

Jdk還提供了幾個比較實用的併發工具類:CountDownLatch、CyclicBarrier和Semaphore。

3.4.1 CountDownLatch

CountDownLatch允許一個或多個線程等待其他線程完成操作。

示例代碼:

public class CountDownLatchTest {

    // 計數器定義爲3,表示需要三個線程執行c.countDown,否則調用c.wait的線程會一直阻塞
    static CountDownLatch c = new CountDownLatch(3);

    public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(1);
                c.countDown();
                System.out.println(2);
                c.countDown();
            }
        }).start();
        c.await();
        System.out.println("3");
    }
}

3.4.2 CyclicBarrier

CyclicBarrier實現讓一組線程到達一個屏障時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,所有被屏障攔截的線程纔會繼續運行。

CyclicBarrier可以用於多線程計算數據,最後合併計算結果的場景

示例代碼:

public class CyclicBarrierTest {

    // 定義屏障線程數爲3,即需要達到三個線程進入阻塞,屏障纔會打開
    static CyclicBarrier c = new CyclicBarrier(3);

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    c.await();
                } catch (Exception e) {
                }
                System.out.println(1);
            }
        }).start();

        try {
            c.await();
        } catch (Exception e) {
        }
        System.out.println(2);
    }
}

3.4.3 Semaphore

Semaphore(信號量)是用來控制同時訪問特定資源的線程數量,它通過協調各個線程,以保證合理的使用公共資源。

4.Atomic

Java從JDK 1.5開始提供了java.util.concurrent.atomic包(以下簡稱Atomic包),這個包中的原子操作類提供了一種用法簡單、性能高效、線程安全地更新一個變量的方式。具體如下:
這裏寫圖片描述

  • 基本類型:AtomicBoolean、AtomicInteger、AtomicLong

  • 引用類型(帶有標記位):AtomicReference、AtomicStampedRerence、AtomicMarkableReference,後面兩個是爲了解決ABA問題的

  • 數組類型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray,注意:Atomic數組類型會將當前數組
    複製一份,內部修改不會影響傳入的數組

  • 更新指定對象的屬性:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

  • Jdk1.8新增:DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder,在高併發相比於AtomicLonggen更高效,LongAdder更多地用於收集統計數據,而不是細粒度的同步控制

具體示例:


public class CounterThread extends Thread {

    // 使用原子類型
    private static AtomicLong counter = new AtomicLong(0);

    public static long addOne() {
        return counter.incrementAndGet();
    }

    @Override
    public void run() {
        try {
            Thread.sleep(100);
            if (addOne() == 100) {
                System.out.println("計數器值最終值爲100");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class AtomicMain {

    public static void main(String[] argv) {

        AtomicInteger atomicInteger = new AtomicInteger();
        atomicInteger.set(1);
        atomicInteger.incrementAndGet();
        System.out.println(atomicInteger.get());
    }
}

5.Java中的線程池及Executor框架

Java中的線程池是運用場景最多的併發框架,幾乎所有需要異步或併發執行任務的程序都可以使用線程池。

  • 降低資源消耗
  • 提高響應速度
  • 提高線程的可管理性

5.1 ThreadPoolExecutor

5.1.1 線程池實現原理

這裏寫圖片描述

 /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */

5.1.2 線程池實用

  • 1.線程池創建
/**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     * @param handler the handler to use when execution is blocked
     *        because the thread bounds and queue capacities are reached
     * @throws IllegalArgumentException if one of the following holds:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue}
     *         or {@code threadFactory} or {@code handler} is null
     */
    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,千分之一微秒)。

  • BlockingQueue(任務隊列):用於保存等待執行的任務的阻塞隊列。可以選擇以下幾個阻塞隊列。

    • ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按FIFO(先進先出)原則對元素進行排序。
    • LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按FIFO排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。
    • SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
    • PriorityBlockingQueue:一個具有優先級的無限阻塞隊列。
  • ThreadFactory:用於設置創建線程的工廠,可以通過線程工廠給每個創建出來的線程設置更有意義的名字。

  • RejectedExecutionHandler(飽和策略):當隊列和線程池都滿了,說明線程池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略默認情況下是AbortPolicy,表示無法處理新任務時拋出異常。在JDK 1.5中Java線程池框架提供了以下4種策略。

    • AbortPolicy:直接拋出異常。
    • CallerRunsPolicy:只用調用者所在線程來運行任務。
    • DiscardOldestPolicy:丟棄隊列裏最近的一個任務,並執行當前任務。
    • DiscardPolicy:不處理,丟棄掉。
      當然,也可以根據應用場景需要來實現RejectedExecutionHandler接口自定義策略。如記錄日誌或持久化存儲不能處理的任務。
  • 2.任務提交

存在兩個方法向線程池提交任務,分別爲execute()和submit()方法。

  • execute():不需要返回值的任務
  • submit():需要返回值的任務

  • 3.關閉線程池

通過調用線程池的shutdown或shutdownNow方法來關閉線程池。

  • shutdownNow首先將線程池的狀態設置成STOP,然後嘗試停止所有的正在執行或暫停任務的線程,並返回等待執行任務的列表
  • shutdown只是將線程池的狀態設置成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的線程。

具體採用哪種方式,需要根據任務的特性。

  • 4.具體示例
public class NamedThreadFactory implements ThreadFactory {
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;
    private final boolean daemon;

    public NamedThreadFactory(String namePrefix) {
        this.namePrefix = namePrefix;
        this.daemon = false;
    }

    public NamedThreadFactory(String namePrefix, boolean isDaemon) {
        this.namePrefix = namePrefix;
        this.daemon = isDaemon;
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, this.namePrefix + "-" + this.threadNumber.getAndIncrement());
        t.setDaemon(this.daemon);
        if (t.getPriority() != 5) {
            t.setPriority(5);
        }

        return t;
    }
}

public class WorkThread implements Runnable {

    private String command;

    public WorkThread(String s){
        this.command=s;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+" Start. Command = "+command);
        processCommand();
        System.out.println(Thread.currentThread().getName()+" End.");
    }

    private void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString(){
        return this.command;
    }
}

public class ThreadPoolExecutorTest {

    private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 10,
            20L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1),
            new NamedThreadFactory("Thread-Worker"),
            new ThreadPoolExecutor.CallerRunsPolicy());

    public static void main (String[] argv) {

        for (int i = 0; i < 10; i++) {
            Runnable worker = new WorkThread(String.valueOf(i));
            poolExecutor.execute(worker);
        }
        poolExecutor.shutdown();
        while (!poolExecutor.isTerminated()) {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {

            }
        }
        System.out.println("All Threads have finished!!");

    }
}
  • 5.ThreadPoolExecutor延伸的三種類型

    • CachedThreadPool:創建一個可緩存的線程池,如果當前線程池的規模超出了處理需求,將回收空閒的線程;當需求增加時,會增加工作線程數量;線程池規模無限制;空閒線程被保留60秒。
    • FixedThreadPool:創建一個固定長度的線程池,當到達線程最大數量時,線程池的規模將不再變化。
    • SingleThreadExecutor:線程池中任意時刻只有一個線程,隊列中的任務按照順序執行。

5.2 ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor。它主要用來在給定的延遲之後運行任務,或者定期執行任務。

注意ScheduledThreadPoolExecutor使用的阻塞隊列是DelayQueue-無界阻塞隊列。

  • 1.線程池執行原理
    這裏寫圖片描述

  • 4.具體示例

示例同5.2
public class ScheduledThreadPoolExecutorTest {

    public static void main(String[] argv) throws Exception {

        ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5,
                new NamedThreadFactory("scheduler-pool"));

        for (int i = 0; i < 10; i++) {
            WorkThread workThread = new WorkThread(String.valueOf(i));

            threadPoolExecutor.scheduleAtFixedRate(workThread, 2, 3, TimeUnit.SECONDS);

        }

        Thread.sleep(30000);

        threadPoolExecutor.shutdown();
        while (!threadPoolExecutor.isTerminated()) {
            Thread.sleep(1000);
        }
        System.out.println("All Threads have finished!!");
    }
}

5.3 FutureTask

FutureTask一個可取消的異步計算,實現了Future的基本方法,提供啓動start 和cancel操作。可以查詢計算是否已經完成,獲取計算的結果。結果只能在計算完成之後調用get()獲取,否則會阻塞,直到計算完成。

FutureTask除了實現Future接口外,還實現了Runnable接口。因此,FutureTask可以交給Executor執行,或者調用線程直接執(FutureTask.run())

FutureTask的get方法和cancel方法
這裏寫圖片描述

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