java併發---線程通信和線程池原理

一、線程的狀態

  線程的狀態包括新建(初始狀態)、就緒、運行、死亡(終止)、阻塞;

  (1)簡化版本

  

     

  (2)結合java線程方法版本

  

(2)線程通信

  • wait():導致當前線程等待,直到其他線程調用該同步監視器的notify()方法或notifyAll()方法來喚醒該線程。該wait()方法有3種形式——無時間參數的wait(一直等待,直到其他線程通知),帶毫秒參數的wait和帶毫秒、毫微秒參數的wait(這兩種方法都是等待指定時間後自動甦醒)。調用wait()方法的當前線程會釋放對該同步監視器的鎖定。

  • notify():喚醒在此同步監視器上等待的單個線程。如果所有線程都在此同步監視器上等待,則會選擇喚醒其中一個線程。選擇是任意性的。只有當前線程放棄對該同步監視器的鎖定後(使用wait()方法),纔可以執行被喚醒的線程。

  • notifyAll():喚醒在此同步監視器上等待的所有線程。只有當前線程放棄對該同步監視器的鎖定後,纔可以執行被喚醒的線程。

  使用示例:

package test;

public class ThreadComm {

    public static boolean WASHED = false;

    public static void wash(int i) {
        System.out.println(i + "已經洗手");
        WASHED = true;
    }

    public static void eat(int i) {
        System.out.println(i + "已經喫飯");
        WASHED = false;
    }

    public static void main(String[] args) {

        // wash線程
        for (int i = 0; i <= 5; i++) {
            int j = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    doWash(j);
                    doEat(j);
                }

                private synchronized void doWash(int i) {
                    if (!WASHED) {// 如果還沒洗手,就執行洗手操作,否則,阻塞當前線程,直到喫飯完成
                        ThreadComm.wash(i);
                        notifyAll();
                    } else {
                        try {
                            wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }// doWash

                private synchronized void doEat(int i) {
                    if (WASHED) {// 已經洗完手,喚起當前喫飯線程
                        ThreadComm.eat(i);
                        notifyAll();
                    } else {
                        try {
                            wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }// doEat

            }).start();

        }// for
    }

}

 

二、線程池

(1)常用線程池的類結構

  

  普通線程執行完,就會進入TERMINATED銷燬掉,而線程池就是創建一個緩衝池存放線程,執行結束以後,該線程並不會死亡,而是再次返回線程池中成爲空閒狀態,等候下次任務來臨,這使得線程池比手動創建線程有着更多的優勢:

  •   降低系統資源消耗,通過重用已存在的線程,降低線程創建和銷燬造成的消耗;
  •   提高系統響應速度,當有任務到達時,通過複用已存在的線程,無需等待新線程的創建便能立即執行;
  •   方便線程併發數的管控。因爲線程若是無限制的創建,可能會導致內存佔用過多而產生OOM;
  •   節省cpu切換線程的時間成本(需要保持當前執行線程的現場,並恢復要執行線程的現場)。
  •   提供更強大的功能,延時定時線程池。(eg:ScheduledThreadPoolExecutor可以代替Timer執行定時任務)

(2)線程池的工作狀態

  

  • RUNNING:初始化狀態是RUNNING。線程池被一旦被創建,就處於RUNNING狀態,並且線程池中的任務數爲0。RUNNING狀態下,能夠接收新任務,以及對已添加的任務進行處理。
  • SHUTDOWN:SHUTDOWN狀態時,不接收新任務,但能處理已添加的任務。調用線程池的shutdown()接口時,線程池由RUNNING -> SHUTDOWN。
  • STOP:不接收新任務,不處理已添加的任務,並且會中斷正在處理的任務。調用線程池的shutdownNow()接口時,線程池由(RUNNING 或 SHUTDOWN ) -> STOP。

   注意:運行中的任務還會打印,直到結束,因爲調的是Thread.interrupt

  • TIDYING:所有的任務已終止,隊列中的”任務數量”爲0,線程池會變爲TIDYING。線程池變爲TIDYING狀態時,會執行鉤子函數terminated(),可以通過重載terminated()函數來實現自定義行爲。
  • TERMINATED:線程池處在TIDYING狀態時,執行完terminated()之後,就會由 TIDYING -> TERMINATED

(3)線程池原理

  

  • 添加任務,如果線程池中線程數沒達到coreSize,直接創建新線程執行
  • 達到core,放入queue
  • queue已滿,未達到maxSize繼續創建線程
  • 達到maxSize,根據reject策略處理
  • 超時後,線程被釋放,下降到coreSize

   (4)線程池源碼分析

    1)線程池是如何保證線程不被銷燬的呢?

   如果隊列中沒有任務時,核心線程會一直阻塞在獲取任務的方法,直到返回任務。而任務執行完後,又會進 下一輪 work.runWork()中循環

   驗證:祕密就藏在覈心源碼裏 ThreadPoolExecutor.getTask()

//work.runWork():
while (task != null || (task = getTask()) != null)
//work.getTask():
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();

  2)那麼線程池中的線程會處於什麼狀態?

  答案:RUNNABLE,WAITING

  驗證:起一個線程池,放置一個任務sleep,debug查看結束前後的狀態

//debug add watcher:
((ThreadPoolExecutor) poolExecutor).workers.iterator().next().thread.getState()
ThreadPoolExecutor poolExecutor = Executors.newFixedThreadPool(5);
poolExecutor.execute(new Runnable() {
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println("ok");

  3)核心線程與非核心線程有區別嗎?

  答案:沒有。被銷燬的線程和創建的先後無關。即便是第一個被創建的核心線程,仍然有可能被銷燬

  驗證:看源碼,每個works在runWork的時候去getTask,在getTask內部,並沒有針對性的區分當前work是否是核 心線程或者類似的標記。只要判斷works數量超出core,就會調用poll(),否則take()

(5)線程池調優

  1)Executors剖析

  1.1)newCachedThreadPool

//core=0
//max=Integer
//timeout=60s
//queue=1
//也就是隻要線程不夠用,就一直開,不用就全部釋放。線程數0‐max之間彈性伸縮
//注意:任務併發太高且耗時較長時,造成cpu高消耗,同時要警惕OOM
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());

  1.2)newFixedThreadPool

//core=max=指定數量
//timeout=0
//queue=無界鏈表
//也就是說,線程數一直保持制定數量,不增不減,永不超時
//如果不夠用,就沿着隊列一直追加上去,排隊等候
//注意:併發太高時,容易造成長時間等待無響應,如果任務臨時變量數據過多,容易OOM
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);

  1.3)newSingleThreadExecutor

//core=max=1
//timeout=0
//queue=無界鏈表
//只有一個線程在慢吞吞的幹活,可以認爲是fix的特例
//適用於任務零散提交,不緊急的情況
new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));

  1.4)newScheduledThreadPool

//core=制定數
//max=Integer
//timeout=0
//queue=DelayedWorkQueue(重點!)
//用於任務調度,DelayedWorkQueue限制住了任務可被獲取的時機(getTask方法),也就實現了時間控制
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory);

   2)優化建議

  2.1)corePoolSize

  基本線程數,一旦有任務進來,在core範圍內會立刻創建線程進入工作。所以這個值應該參考業務併發量在絕大多數時間內的併發情況。同時分析任務的特性。

  高併發,執行時間短的,要儘可能小的線程數,如配置CPU個數+1,減少線程上下文的切換。因爲它不怎麼佔時 間,讓少量線程快跑幹活。

  併發不高、任務執行時間長的要分開看:如果時間都花在了IO上,那就調大CPU,如配置兩倍CPU個數+1。不能 讓CPU閒下來,線程多了並行處理更快。如果時間都花在了運算上,運算的任務還很重,本身就很佔cpu,那儘量 減少cpu,減少切換時間。參考第一條。

  如果高併發,執行時間還很長……

  2.2)workQueue

  任務隊列,用於傳輸和保存等待執行任務的阻塞隊列。這個需要根據你的業務可接受的等待時間。是一個需要權衡 時間還是空間的地方,如果你的機器cpu資源緊張,jvm內存夠大,同時任務又不是那麼緊迫,減少coresize,加大 這裏。如果你的cpu不是問題,對內存比較敏感比較害怕內存溢出,同時任務又要求快點響應。那麼減少這裏。

  2.3)maximumPoolSize

  線程池最大數量,這個值和隊列要搭配使用,如果你採用了無界隊列,這個參數失效。同時要注意,隊列盛滿,同 時達到max的時候,再來的任務可能會丟失(下面的handler會講)。 如果你的任務波動較大,同時對任務波峯來的時候,實時性要求比較高。也就是來的很突然並且都是着急的。那麼 調小隊列,加大這裏。如果你的任務不那麼着急,可以慢慢做,那就扔隊列吧。 隊列與max是一個權衡。隊列空間換時間,多花內存少佔cpu,輕視任務緊迫度。max捨得cpu線程開銷,少佔內存,給任務最快的響應。

  2.4)keepaliveTime

  線程存活保持時間,超出該時間後,線程會從max下降到core,很明顯,這個決定了你養閒人所花的代價。如果 你不缺cpu,同時任務來的時間沒法琢磨,波峯波谷的間隔比較短。經常性的來一波。那麼實當的延長銷燬時間, 避免頻繁創建和銷燬線程帶來的開銷。如果你的任務波峯出現後,很長一段時間不再出現,間隔比較久,那麼要適當調小該值,讓閒着不幹活的線程儘快銷燬,不要佔據資源。

    2.5)threadFactory(自定義展示實例)

  線程工廠,用於創建新線程。threadFactory創建的線程也是採用new Thread()方式,threadFactory創建的線程名都具有統一的風格:pool-m-thread-n(m爲線程池的編號,n爲線程池內的線程編號)。如果需要自己定義線程 的某些屬性,如個性化的線程名,可以在這裏動手。一般不需要折騰它。

    2.6)handler

  線程飽和策略,當線程池和隊列都滿了,再加入線程會執行此策略。默認不處理的話會扔出異常,打進日誌。這個與任務處理的數據重要程度有關。如果數據是可丟棄的,那不需要額外處理。如果數據極其重要,那需要在這裏採取措施防止數據丟失,如扔消息隊列或者至少詳細打入日誌文件可追蹤。

  優化總結:

  1)線程池的線程數量設置不宜過大,因爲一旦線程池的工作線程總數超過系統所擁有的處理器數量,就會導致過多的上下文切換。

  2)慎用Executors,尤其如newCachedThreadPool。這個方法如果任務過多會無休止創建過多線 程,增加了上下文的切換。最好根據業務情況,自己創建線程池參數。

 

(6)線程使用

package test;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadPoolExecutor;

public class Test {
    public static void main(String[] args) {
        // 繼承Thread
        ThreadTest th1 = new ThreadTest();
        th1.setName("thread");
        th1.start();

        // 實現Runnable
        RunnableTest runnable = new RunnableTest();
        Thread th2 = new Thread(runnable);
        th2.setName("runnable");
        th2.start();

        // 實現Callable<> 接口,java5新增,可返回執行結果
        CallableTest callable = new CallableTest();
        FutureTask<Integer> future = new FutureTask<>(callable);
        new Thread(future, "callable").start();
        try {
            Integer r = future.get();
            System.out.println(r);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 線程池
        ExecutorService pool = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor executor = (ThreadPoolExecutor) pool;
        executor.execute(new PoolHandler());
    }
}

// 方式一
class ThreadTest extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

// 方式二
class RunnableTest implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

// 方式三
class CallableTest implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            sum += i;
        }
        return sum;
    }
}

/**
 * 方式四 線程池實現方式
 *     注意:使用線程池時,使用實現Runnable的方式可避免java中單一繼承造成的侷限性
 */
class PoolHandler implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

 

查閱和參考了不少資料,感謝各路大佬分享,如需轉載請註明出處,謝謝:https://www.cnblogs.com/huyangshu-fs/p/11374573.html

 

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