高併發編程線程池篇

使用線程池的目的
    降低資源消耗
        通過重複利用已創建的線程降低線程創建和銷燬造成的消耗
    提高響應效率
        當任務到達時,任務可以不需要等到線程創建就能立即執行
    方便管理
        線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。但是,要做到合理利用。

提交一個任務到線程池中,線程池的處理流程如下:
1、判斷線程池裏的核心線程是否都在執行任務,如果不是(核心線程空閒或者還有核心線程沒有被創建)則創建一個新的工作線程來執行任務。如果核心線程都在執行任務,則進入下個流程。
2、線程池判斷工作隊列是否已滿,如果工作隊列沒有滿,則將新提交的任務存儲在這個工作隊列裏。如果工作隊列滿了,則進入下個流程。
3、判斷線程池裏的線程是否都處於工作狀態,如果沒有,則創建一個新的工作線程來執行任務。如果已經滿了,則交給飽和策略來處理這個任務。

通過Executors(JDK1.5併發包)四種創建方式
    newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。
    newFixedThreadPool創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
    newScheduledThreadPool創建一個定長線程池,支持定時及週期性任務執行。
        schedule(Runable command, long delay, TimeUnit unit):定時開始執行
        scheduleAtFixedRate(Runable command, long initialDelay, long period, TimeUnit unit): 第一次定時開始執行之後,週期性的繼續執行(週期頻率爲:上一次任務開始執行時間爲起點,過了period時間,調度下一次任務,當然如果上一次任務執行還沒結束,就算到了period時間也不會執行下一次任務)。
        scheduleWithFixedFixedDelay(Runable command, long initialDelay, long delay, TimeUnit unit): 第一次定時開始執行之後,週期性的繼續執行。(週期頻率爲:上一個任務結束後,經過delay時間再進行下一次任務調度)。
    newSingleThreadExecutor創建一個單線程話的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO,LIFO,優先級)執行。
    ThreadPoolExecutor上述4中方法實際上底層實現是ThreadPoolExecutor的4個構造器。
    Executors的submit方法
        以submit(Callable task)方法開起的線程可以獲得線程返回值,通過get()獲得當前線程的返回值,當獲取返回值得時候如果這個線程的寫操作還沒有完成則會等待(通常建立一個新的子線程來獲取,這樣不會阻塞主線程)。在獲取該線程的返回值之前,是不會影響其他線程的執行的。
            底層實現:future模式(以後說)
    shutdown()
        關閉線程池,不會暴力關閉,而是等待所有任務執行完成後再關閉。只是發送了一個關閉信號,shutdown()方法執行後,這個線程池不會再接收新的任務。

配置線程池
    CPU密集
        該任務執行的時候,不會產生大量IO阻塞,CPU運行速度快
            配置最大線程數=CPU核數
    IO密集
        該任務需要大量I/O操作,產生阻塞,如果是在單線程中執行,可以使用多線程技術,CPU運算能力不會浪費等待資源
            配置最大線程數:2*CPU核數
    CPU的數量*CPU使用率*(1+等待時間/計算時間)
        CPU數量可以通過java中的Runtime.getRuntime().availableProcessors()方法獲取。

併發包
    stop(棄用)
        原因:暴力終止線程,並釋放所持有的鎖,可能會造成保存的數據不一致,而導致數據永久地破壞和丟失
    interrupt()
        中斷線程
    isInterrupted()
        判斷是否被中斷
    interrupted()
        判斷是否被中斷,並清除當前中斷狀態
    sleep(xx)
        阻塞(xx毫秒後重新進入就緒狀態),會由於中斷而拋出異常,此時,它會清除中斷標記。所以我們可以在catch塊中通過interrupt()再次設置中斷標記爲,就可以防止下次循環無法捕獲這個中斷的問題
    鎖對象.wait()
        讓線程等待,釋放鎖的資源
    鎖對象.notify()
        喚醒當前對象鎖池等待的線程
    join(x)
        讓目標程序先執行,執行結束之後再執行自己(x可選,填的話就是設置最大等待時長,超過這個時長將會回到自己繼續執行)
        本質是讓調用線程wait()再當前線程對象實例上
    yield()
        讓出CPU讓別人先執行,之後再重新爭搶CUP。(Thread.yeild())
    CountDownLatch
        倒計時器:new CountDownLatch(x); x表示計數個數。CountDownLatch,await*(方法,說明x個任務全部完成後,才能繼續執行
    CyclicBarrier    
        循環柵欄:可反覆使用的計數器,當計數任務完成後,進行下一次計數
        特有的異常類:BrokenBarrierException表示CyclicBarrier已經破損,無需繼續等待
    信號量
        允許幾個線程同時訪問
            Semaphore類
                通過new Semaphore(x)創建對象,“x"表示允許幾個線程同時訪問
                acquire()申請信號量,無法獲得會等待,直到有線程釋放信號量
                acquireUniterruptibly()和acquire()類似,但是不響應中斷
                tryAcquire()嘗試獲得一個許可,成功返回true失敗返回false,不會進行等待
                release()釋放信號量
    阻塞工具類:LockSupport
        park():掛起線程(類似suspend())
        unpark():重啓線程(類似resume())
    拒絕策略:均實現了RejectedExecutionHandler接口,若這些策略扔無法滿足應用,則可以自己擴展RejectedExecutionHandler接口(如重寫beforeExecute()(任務開始前),afterExecute()(任務結束後)、terminiated()(整個線程池退出)等)
        AbortPolicy
            直接拋出異常,阻止系統正常工作
        GallerRumsPolicy
            只要線程池未關閉,該策略直接在調用者線程中,運行當前被丟棄的任務。不會真的丟棄任務,但是,任務提交線程的性能極有可能會急劇下降
        DiscardOledestPolicy
            丟棄最老的一個請求,也就是即將被執行的一個任務,並嘗試再次提交當前任務
        DiscardPolicy    
            丟棄無法處理的任務,不予任何處理。(任務丟失)
    一定要在synchronized裏執行,持有同一把鎖。

線程池的優化
    提交的任務和線程數量並不是一對一的關係。絕大多數情況下,一個物理線程實際上是需要處理多個邏輯任務的。因此每個線程必然需要擁有一個任務隊列。在實際執行過程中,可能會出現這麼一種情況:線程A已經把自己的任務都執行完成了,而線程B還有一堆任務等着處理,此時線程A就會“幫助”線程B,從線程B的隊列中拿一個任務過來處理,儘可能地達到平衡。一個值得注意的地方是,當線程試圖幫助別人時,總是從任務隊列的底部開始拿數據,而線程試圖執行自己的任務時,則是從相反的頂部開始拿。因此這種行爲也十分有利於避免數據競爭。

異常處理,幫助我們定位到拋異常的地方:

public class TraceThreadPoolExecutor extends ThreadPoolExecutor{
    public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue){
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    @Override
    public void execute(Runnable task) {
        super.execute(wrap(task, clientTrace(), Thread.currentThread().getName()));
    }

    @Override
    public Future<?> submit(Runnable task) {
        return super.submit(wrap(task, clientTrace(), Thread.currentThread().getName()));
    }
    
    private Exception clientTrace(){
        return new Exception("Client stack trace");
    }

    private Runnable wrap(final Runnable task, final Exception clientStack, String clientTreadName){
        return new Runnable() {
            @Override
            public void run() {
                try{
                    task.run();
                }catch (Exception e){
                    clientStack.printStackTrace();
                    throw e;
                }
            }
        };
    }
}

threadlocal
    給每個線程提供一個局部變量,但是不是通過threadLocal來完成的,而是需要在應用層面上來保證的,如果在應用上爲每一個線程分配了相同的對象實例,那麼ThreadLocal也不能保證線程安全。
     當使用ThreadLocal維護變量時,ThreadLocal爲每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。
    4個方法
        void set(Object value)
        public Object get()
        public void remove()
        protected Object initialValue()

創建threadLocal:

    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        protected Integer initialValue() {

            return 0;
        };

    };

    底層就是一個ThreadLocalMap可以理解爲一個map集合,key是當前線程
    內存泄露
        當我們使用線程池,意味着當前線程未必會退出。我們將一些對象設置到ThreadLocal中(它實際保存在線程持有的ThreadLocalMap內),可能會使系統出現內存泄露的問題(用完未清理),使用完了再也不用了,但是卻無法被回收。ThreadLocalMap更加類似WeakHashMap,它的實現使用了弱引用,java虛擬機在垃圾回收時,如果發現弱引用,就會立即回收
        解決辦法:使用ThreadLocal.remove()方法將變量移除,或者直接賦值null,那麼這個對象會更容易被垃圾回收器發現,從而加速回收。(如:t = null,那麼這個ThreadLocal對應的所有線程的局部變量都有可能被回收)。

參考書籍《JAVA高併發程序設計》

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