JAVA多線程(線程池、ThreadLocal、CountDownLatch)

目錄

1、線程池

瞭解線程池使用的幾種隊列(線程池的三種隊列區別)

常用的四種創建方法

ThreadPoolExecutor拒絕策略

ThreadPoolExecutor擴展

2、ThreadLocal

3、CountDownLatch計數器


1、線程池

池化思想主要是爲了避免不停地創建和銷燬應用(線程、數據庫連接)影響系統性能。

創建線程池實際上還是利用 ThreadPoolExecutor 類實現的

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

這幾個核心參數的作用:corePoolSize 爲線程池的基本大小。maximumPoolSize 爲線程池最大線程大小keepAliveTime 和 unit 則是線程空閒後的存活時間。workQueue 用於存放任務的阻塞隊列。handler 當隊列和最大線程池都滿了之後的飽和策略。

瞭解線程池使用的幾種隊列

  1. SynchronousQueue它沒有容量,每一個插入操作都要等待一個相應的刪除操作,每一個刪除操作都要等待對應的插入操作。以確保,如果沒有空閒線程,則嘗試創建新的線程;如果線程數量達到最大值,則使用拒絕策略。此方法直接將任務從生產者移交到工作線程,避免任務排隊,更高效。
  2. ArrayBlockingQueue是一個有界緩存等待隊列,可以指定緩存隊列的大小,當正在執行的線程數等於corePoolSize時,多餘的元素緩存在ArrayBlockingQueue隊列中等待有空閒的線程時繼續執行,當ArrayBlockingQueue已滿時,加入ArrayBlockingQueue失敗,會開啓新的線程去執行,當線程數已經達到最大的maximumPoolSizes時,再有新的元素嘗試加入ArrayBlockingQueue時會報錯。
  3. LinkedBlockingQueue是一個無界緩存等待隊列。當前執行的線程數量達到corePoolSize的數量時,剩餘的元素會在阻塞隊列裏等待。(所以在使用此阻塞隊列時maximumPoolSizes就相當於無效了),每個線程完全獨立於其他線程。生產者和消費者使用獨立的鎖來控制數據的同步,即在高併發的情況下可以並行操作隊列中的數據。
  4. DelayQueue是一個無界阻塞隊列,它的隊列元素只能在該元素的延遲已經結束或者說過期才能被出隊。它怎麼判斷一個元素的延遲是否結束呢,原來DelayQueue隊列元素必須是實現了Delayed接口的實例,該接口有一個getDelay方法需要實現,延遲隊列就是通過實時的調用元素的該方法來判斷當前元素是否延遲已經結束。DelayQueue底層實現是使用PriorityQueue + ReentrantLock來實現延遲獲取功能,它的優先級的值是時間,PriorityQueue的原理是堆排序。

常用的四種創建方法

  1. Executors.newSingleThreadExecutor() 創建一個單線程的線程池,保證所有任務按照指定順序執行(以共享的無界隊列方式來運行這些線程,由於是無界隊列,有可能造成內存溢出)
    public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
        }
  2. Executors.newFixedThreadPool(int n) 創建一個固定線程數量的線程池(無界)
    public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
  3. Executors.newCachedThreadPool()  線程池數量不確定,有空閒線程則複用,沒有就創建新的

    public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }
  4. Executors.newScheduledThreadPool(int n) 定長線程池,支持定時及週期性任務執行。scheduleAtFixedRate()方法按週期調度,比如每隔3秒執行一次,若任務執行時間超過3秒,則任務完成後立即進行下一次調度。scheduleWithFixedDelay()方法按延遲間隔週期執行,比如任務完成後間隔3秒進行下一次任務。
    public ScheduledThreadPoolExecutor(int corePoolSize) {
            super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
                  new DelayedWorkQueue());
        }

線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。 說明:Executors各個方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
  主要問題是堆積的請求處理隊列可能會耗費非常大的內存,甚至OOM。
2)newCachedThreadPool:
  主要問題是線程數最大數是Integer.MAX_VALUE,可能會創建數量非常多的線程,甚至OOM。 

ThreadPoolExecutor拒絕策略

當隊列已經滿了,線程池已經是最大數量並且線程都用完了,此時若還有任務進來,需要有拒絕策略。jdk內置了四種拒絕策略:

  • 直接拋出異常
  • 直接丟棄任務,即不作任何處理
  • 使用當前線程(主線程)處理。從線程池到工作隊列到應用程序再到TCP層,最終到達客戶端,導致服務器在高負載下實現一種平緩的性能降低。
  • 拋棄任務隊列中最舊的任務也就是最先加入隊列的,再把這個新任務添加進去

此外,我們可以通過實現RejectedExecutionHandler接口自定義拒絕策略。

ThreadPoolExecutor擴展

它提供了beforeExecute()、afterExecute()、terminated()等接口方法對線程池進行執行前、執行完成、線程池退出的控制。

強烈推薦:線程池面試題總結

拒絕策略詳解

2、ThreadLocal

用來存儲線程的本地變量,有一個內部類ThreadLocalMap,key=ThreadLocal實例本身,value=線程局部變量緩存值

引申:InheritableThreadLocal主要用於子線程創建時,需要自動繼承父線程的ThreadLocal變量。

ThreadLocal內存泄露?
每個thread中都存在一個map, map的類型是ThreadLocal.ThreadLocalMap. Map中的key爲一個threadlocal實例. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向threadlocal.當把threadlocal實例置爲null以後,沒有任何強引用指向threadlocal實例,所以threadlocal將會被gc回收. 但是,我們的value卻不能回收,因爲存在一條從current thread連接過來的強引用. 只有當前thread結束以後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將全部被GC回收.所以得出一個結論就是隻要這個線程對象被gc回收,就不會出現內存泄露,但在threadLocal設爲null和線程結束這段時間不會被回收的,就發生了我們認爲的內存泄露。

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

3、CountDownLatch計數器

是一個計數器,等其他線程執行完後再執行,計數器的初始值通過構造函數設置(new CountDownLatch(線程數量)),每當一個線程執行完計數器的值就-1,當計數器的值爲0時表示所有線程執行完畢,主線程繼續執行。包含三個重要方法:

//調用await()方法的線程會被掛起,它會等待直到count值爲0才繼續執行
public void await() throws InterruptedException { };   
//和await()類似,只不過等待一定的時間後count值還沒變爲0的話就會繼續執行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
//將count值減1
public void countDown() { };  

CountDownLatch是通過繼承AQS(AbstractQueuedSynchronizer)來實現的,AbstractQueuedSynchronizer學習

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