一、線程的狀態
線程的狀態包括新建(初始狀態)、就緒、運行、死亡(終止)、阻塞;
(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