瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高併發實戰》 面試必備 + 面試必備 + 面試必備 【博客園總入口 】html
瘋狂創客圈 經典圖書 : 《SpringCloud、Nginx高併發核心編程》 大廠必備 + 大廠必備 + 大廠必備 【博客園總入口 】java
入大廠+漲工資 必備的 高併發社羣: 【博客園總入口 】linux
工欲善其事 必先利其器 |
---|
地表最強 開發環境: vagrant+java+springcloud+redis+zookeeper鏡像下載(&製做詳解) |
地表最強 熱部署:java SpringBoot SpringCloud 熱部署 熱加載 熱調試 |
地表最強 發請求工具(再見吧, PostMan ):IDEA HTTP Client(史上最全) |
地表最強 PPT 小工具: 屌炸天,像寫代碼同樣寫PPT |
無編程不創客,無編程不創客,一大波編程高手正在瘋狂創客圈交流、學習中! 找組織,GO |
推薦閱讀 |
---|
nacos 實戰(史上最全) |
sentinel (史上最全+入門教程) |
springcloud + webflux 高併發實戰 |
Webflux(史上最全) |
SpringCloud gateway (史上最全) |
無編程不創客,無編程不創客,一大波編程高手正在瘋狂創客圈交流、學習中! 找組織,GO |
併發編程的目的就是爲了能提升程序的執行效率,提升程序運行速度,可是併發編程並不老是能提升程序運行速度的,並且併發編程可能會遇到不少問題,好比:內存泄漏、上下文切換、線程安全、死鎖等問題。程序員
併發編程三要素(線程的安全性問題體如今):web
原子性:原子,即一個不可再被分割的顆粒。原子性指的是一個或多個操做要麼所有執行成功要麼所有執行失敗。面試
可見性:一個線程對共享變量的修改,另外一個線程可以馬上看到。(synchronized,volatile)redis
有序性:程序執行的順序按照代碼的前後順序執行。(處理器可能會對指令進行重排序)算法
出現線程安全問題的緣由:spring
解決辦法:數據庫
作一個形象的比喻:
併發 = 兩個隊列和一臺咖啡機。
並行 = 兩個隊列和兩臺咖啡機。
串行 = 一個隊列和一臺咖啡機。
多線程:多線程是指程序中包含多個執行流,即在一個程序中能夠同時運行多個不一樣的線程來執行不一樣的任務。
多線程的好處:
能夠提升 CPU 的利用率。在多線程程序中,一個線程必須等待的時候,CPU 能夠運行其它的線程而不是等待,這樣就大大提升了程序的效率。也就是說容許單個程序建立多個並行執行的線程來完成各自的任務。
多線程的劣勢:
一個在內存中運行的應用程序。每一個進程都有本身獨立的一塊內存空間,一個進程能夠有多個線程,好比在Windows系統中,一個運行的xx.exe就是一個進程。
進程中的一個執行任務(控制單元),負責當前進程中程序的執行。一個進程至少有一個線程,一個進程能夠運行多個線程,多個線程可共享數據。
線程具備許多傳統進程所具備的特徵,故又稱爲輕型進程(Light—Weight Process)或進程元;而把傳統的進程稱爲重型進程(Heavy—Weight Process),它至關於只有一個線程的任務。在引入了線程的操做系統中,一般一個進程都有若干個線程,至少包含一個線程。
根本區別:進程是操做系統資源分配的基本單位,而線程是處理器任務調度和執行的基本單位
資源開銷:每一個進程都有獨立的代碼和數據空間(程序上下文),程序之間的切換會有較大的開銷;線程能夠看作輕量級的進程,同一類線程共享代碼和數據空間,每一個線程都有本身獨立的運行棧和程序計數器(PC),線程之間切換的開銷小。
包含關係:若是一個進程內有多個線程,則執行過程不是一條線的,而是多條線(線程)共同完成的;線程是進程的一部分,因此線程也被稱爲輕權進程或者輕量級進程。
內存分配:同一進程的線程共享本進程的地址空間和資源,而進程之間的地址空間和資源是相互獨立的
影響關係:一個進程崩潰後,在保護模式下不會對其餘進程產生影響,可是一個線程崩潰整個進程都死掉。因此多進程要比多線程健壯。
執行過程:每一個獨立的進程有程序運行的入口、順序執行序列和程序出口。可是線程不能獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制,二者都可併發執行
多線程編程中通常線程的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,爲了讓這些線程都能獲得有效執行,CPU 採起的策略是爲每一個線程分配時間片並輪轉的形式。當一個線程的時間片用完的時候就會從新處於就緒狀態讓給其餘線程使用,這個過程就屬於一次上下文切換。
歸納來講就是:當前任務在執行完 CPU 時間片切換到另外一個任務以前會先保存本身的狀態,以便下次再切換回這個任務時,能夠再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換。
上下文切換一般是計算密集型的。也就是說,它須要至關可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都須要納秒量級的時間。因此,上下文切換對系統來講意味着消耗大量的 CPU 時間,事實上,多是操做系統中時間消耗最大的操做。
Linux 相比與其餘操做系統(包括其餘類 Unix 系統)有不少的優勢,其中有一項就是,其上下文切換和模式切換的時間消耗很是少。
守護線程和用戶線程
main 函數所在的線程就是一個用戶線程啊,main 函數啓動的同時在 JVM 內部同時還啓動了好多守護線程,好比垃圾回收線程。
比較明顯的區別之一是用戶線程結束,JVM 退出,無論這個時候有沒有守護線程運行。而守護線程不會影響 JVM 的退出。
注意事項:
setDaemon(true)
必須在start()
方法前執行,不然會拋出 IllegalThreadStateException
異常windows上面用任務管理器看,linux下能夠用 top 這個工具看。
建立線程有四種方式:
1繼承 Thread 類
步驟
public class MyThread extends Thread { @Override public void run() { System.out.println(Thread.currentThread().getName() + " run()方法正在執行..."); } } 12345678 public class TheadTest { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); System.out.println(Thread.currentThread().getName() + " main()方法執行結束"); } } 12345678910
運行結果
main main()方法執行結束 Thread-0 run()方法正在執行... 12
2實現 Runnable 接口
步驟
public class MyRunnable implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + " run()方法執行中..."); } } 12345678 public class RunnableTest { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); thread.start(); System.out.println(Thread.currentThread().getName() + " main()方法執行完成"); } } 12345678910
執行結果
main main()方法執行完成 Thread-0 run()方法執行中... 12
3實現 Callable 接口
步驟
public class MyCallable implements Callable<Integer> { @Override public Integer call() { System.out.println(Thread.currentThread().getName() + " call()方法執行中..."); return 1; } } 123456789 public class CallableTest { public static void main(String[] args) { FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable()); Thread thread = new Thread(futureTask); thread.start(); try { Thread.sleep(1000); System.out.println("返回結果 " + futureTask.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " main()方法執行完成"); } } 12345678910111213141516171819
執行結果
Thread-0 call()方法執行中... 返回結果 1 main main()方法執行完成 123
4使用 Executors 工具類建立線程池
Executors提供了一系列工廠方法用於創先線程池,返回的線程池都實現了ExecutorService接口。
主要有newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool,後續詳細介紹這四種線程池
public class MyRunnable implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + " run()方法執行中..."); } } 12345678 public class SingleThreadExecutorTest { public static void main(String[] args) { ExecutorService executorService = Executors.newSingleThreadExecutor(); MyRunnable runnableTest = new MyRunnable(); for (int i = 0; i < 5; i++) { executorService.execute(runnableTest); } System.out.println("線程任務開始執行"); executorService.shutdown(); } } 1234567891011121314
執行結果
線程任務開始執行 pool-1-thread-1 is running... pool-1-thread-1 is running... pool-1-thread-1 is running... pool-1-thread-1 is running... pool-1-thread-1 is running... 123456
相同點
主要區別
注:Callalbe接口支持返回執行結果,須要調用FutureTask.get()獲得,此方法會阻塞主進程的繼續往下執行,若是不調用不會阻塞。
每一個線程都是經過某個特定Thread對象所對應的方法run()來完成其操做的,run()方法稱爲線程體。經過調用Thread類的start()方法來啓動一個線程。
start() 方法用於啓動線程,run() 方法用於執行線程的運行時代碼。run() 能夠重複調用,而 start() 只能調用一次。
start()方法來啓動一個線程,真正實現了多線程運行。調用start()方法無需等待run方法體代碼執行完畢,能夠直接繼續執行其餘的代碼; 此時線程是處於就緒狀態,並無運行。 而後經過此Thread類調用方法run()來完成其運行狀態, run()方法運行結束, 此線程終止。而後CPU再調度其它線程。
run()方法是在本線程裏的,只是線程裏的一個函數,而不是多線程的。 若是直接調用run(),其實就至關因而調用了一個普通函數而已,直接待用run()方法必須等待run()方法執行完畢才能執行下面的代碼,因此執行路徑仍是隻有一條,根本就沒有線程的特徵,因此在多線程執行時要使用start()方法而不是run()方法。
這是另外一個很是經典的 java 多線程面試問題,並且在面試中會常常被問到。很簡單,可是不少人都會答不上來!
new 一個 Thread,線程進入了新建狀態。調用 start() 方法,會啓動一個線程並使線程進入了就緒狀態,當分配到時間片後就能夠開始運行了。 start() 會執行線程的相應準備工做,而後自動執行 run() 方法的內容,這是真正的多線程工做。
而直接執行 run() 方法,會把 run 方法當成一個 main 線程下的普通方法去執行,並不會在某個線程中執行它,因此這並非多線程工做。
總結: 調用 start 方法方可啓動線程並使線程進入就緒狀態,而 run 方法只是 thread 的一個普通方法調用,仍是在主線程裏執行。
Callable 接口相似於 Runnable,從名字就能夠看出來了,可是 Runnable 不會返回結果,而且沒法拋出返回結果的異常,而 Callable 功能更強大一些,被線程執行後,能夠返回值,這個返回值能夠被 Future 拿到,也就是說,Future 能夠拿到異步執行任務的返回值。
Future 接口表示異步任務,是一個可能尚未完成的異步任務的結果。因此說 Callable用於產生結果,Future 用於獲取結果。
FutureTask 表示一個異步運算的任務。FutureTask 裏面能夠傳入一個 Callable 的具體實現類,能夠對這個異步運算的任務的結果進行等待獲取、判斷是否已經完成、取消任務等操做。只有當運算完成的時候結果才能取回,若是運算還沒有完成 get 方法將會阻塞。一個 FutureTask 對象能夠對調用了 Callable 和 Runnable 的對象進行包裝,因爲 FutureTask 也是Runnable 接口的實現類,因此 FutureTask 也能夠放入線程池中。
新建(new):新建立了一個線程對象。
可運行(runnable):線程對象建立後,當調用線程對象的 start()方法,該線程處於就緒狀態,等待被線程調度選中,獲取cpu的使用權。
運行(running):可運行狀態(runnable)的線程得到了cpu時間片(timeslice),執行程序代碼。注:就緒狀態是進入到運行狀態的惟一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中;
阻塞(block):處於運行狀態中的線程因爲某種緣由,暫時放棄對 CPU的使用權,中止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才 有機會再次被 CPU 調用以進入到運行狀態。
阻塞的狀況分三種:
(一). 等待阻塞:運行狀態中的線程執行 wait()方法,JVM會把該線程放入等待隊列(waitting queue)中,使本線程進入到等待阻塞狀態;
(二). 同步阻塞:線程在獲取 synchronized 同步鎖失敗(由於鎖被其它線程所佔用),,則JVM會把該線程放入鎖池(lock pool)中,線程會進入同步阻塞狀態;
(三). 其餘阻塞: 經過調用線程的 sleep()或 join()或發出了 I/O 請求時,線程會進入到阻塞狀態。當 sleep()狀態超時、join()等待線程終止或者超時、或者 I/O 處理完畢時,線程從新轉入就緒狀態。
死亡(dead):線程run()、main()方法執行結束,或者因異常退出了run()方法,則該線程結束生命週期。死亡的線程不可再次復生。
計算機一般只有一個 CPU,在任意時刻只能執行一條機器指令,每一個線程只有得到CPU 的使用權才能執行指令。所謂多線程的併發運行,實際上是指從宏觀上看,各個線程輪流得到 CPU 的使用權,分別執行各自的任務。在運行池中,會有多個處於就緒狀態的線程在等待 CPU,JAVA 虛擬機的一項任務就是負責線程的調度,線程調度是指按照特定機制爲多個線程分配 CPU 的使用權。
有兩種調度模型:分時調度模型和搶佔式調度模型。
分時調度模型是指讓全部的線程輪流得到 cpu 的使用權,而且平均分配每一個線程佔用的 CPU 的時間片這個也比較好理解。
Java虛擬機採用搶佔式調度模型,是指優先讓可運行池中優先級高的線程佔用CPU,若是可運行池中的線程優先級相同,那麼就隨機選擇一個線程,使其佔用CPU。處於運行狀態的線程會一直運行,直至它不得不放棄 CPU。
線程調度器選擇優先級最高的線程運行,可是,若是發生如下狀況,就會終止線程的運行:
(1)線程體中調用了 yield 方法讓出了對 cpu 的佔用權利
(2)線程體中調用了 sleep 方法使線程進入睡眠狀態
(3)線程因爲 IO 操做受到阻塞
(4)另一個更高優先級線程出現
(5)在支持時間片的系統中,該線程的時間片用完
線程調度器是一個操做系統服務,它負責爲 Runnable 狀態的線程分配 CPU 時間。一旦咱們建立一個線程並啓動它,它的執行便依賴於線程調度器的實現。
時間分片是指將可用的 CPU 時間分配給可用的 Runnable 線程的過程。分配 CPU 時間能夠基於線程優先級或者線程等待的時間。
線程調度並不受到 Java 虛擬機控制,因此由應用程序來控制它是更好的選擇(也就是說不要讓你的程序依賴於線程的優先級)。
(1) wait():使一個線程處於等待(阻塞)狀態,而且釋放所持有的對象的鎖;
(2)sleep():使一個正在運行的線程處於睡眠狀態,是一個靜態方法,調用此方法要處理 InterruptedException 異常;
(3)notify():喚醒一個處於等待狀態的線程,固然在調用此方法的時候,並不能確切的喚醒某一個等待狀態的線程,而是由 JVM 肯定喚醒哪一個線程,並且與優先級無關;
(4)notityAll():喚醒全部處於等待狀態的線程,該方法並非將對象的鎖給全部線程,而是讓它們競爭,只有得到鎖的線程才能進入就緒狀態;
二者均可以暫停線程的執行
處於等待狀態的線程可能會收到錯誤警報和僞喚醒,若是不在循環中檢查等待條件,程序就會在沒有知足結束條件的狀況下退出。
wait() 方法應該在循環調用,由於當線程獲取到 CPU 開始執行的時候,其餘條件可能尚未知足,因此在處理前,循環檢測條件是否知足會更好。下面是一段標準的使用 wait 和 notify 方法的代碼:
synchronized (monitor) { // 判斷條件謂詞是否獲得知足 while(!locked) { // 等待喚醒 monitor.wait(); } // 處理其餘的業務邏輯 } 12345678
Java中,任何對象均可以做爲鎖,而且 wait(),notify()等方法用於等待對象的鎖或者喚醒線程,在 Java 的線程中並無可供任何對象使用的鎖,因此任意對象調用方法必定定義在Object類中。
wait(), notify()和 notifyAll()這些方法在同步代碼塊中調用
有的人會說,既然是線程放棄對象鎖,那也能夠把wait()定義在Thread類裏面啊,新定義的線程繼承於Thread類,也不須要從新定義wait()方法的實現。然而,這樣作有一個很是大的問題,一個線程徹底能夠持有不少鎖,你一個線程放棄鎖的時候,到底要放棄哪一個鎖?固然了,這種設計並非不能實現,只是管理起來更加複雜。
綜上所述,wait()、notify()和notifyAll()方法要定義在Object類中。
當一個線程須要調用對象的 wait()方法的時候,這個線程必須擁有該對象的鎖,接着它就會釋放這個對象鎖並進入等待狀態直到其餘線程調用這個對象上的 notify()方法。一樣的,當一個線程須要調用對象的 notify()方法時,它會釋放這個對象的鎖,以便其餘在等待的線程就能夠獲得這個對象鎖。因爲全部的這些方法都須要線程持有對象的鎖,這樣就只能經過同步來實現,因此他們只能在同步方法或者同步塊中被調用。
使當前線程從執行狀態(運行狀態)變爲可執行態(就緒狀態)。
當前線程到了就緒狀態,那麼接下來哪一個線程會從就緒狀態變成執行狀態呢?多是當前線程,也多是其餘線程,看系統的分配了。
Thread 類的 sleep()和 yield()方法將在當前正在執行的線程上運行。因此在其餘處於等待狀態的線程上調用這些方法是沒有意義的。這就是爲何這些方法是靜態的。它們能夠在當前正在執行的線程中工做,並避免程序員錯誤的認爲能夠在其餘非運行線程調用這些方法。
(1) sleep()方法給其餘線程運行機會時不考慮線程的優先級,所以會給低優先級的線程以運行的機會;yield()方法只會給相同優先級或更高優先級的線程以運行的機會;
(2) 線程執行 sleep()方法後轉入阻塞(blocked)狀態,而執行 yield()方法後轉入就緒(ready)狀態;
(3)sleep()方法聲明拋出 InterruptedException,而 yield()方法沒有聲明任何異常;
(4)sleep()方法比 yield()方法(跟操做系統 CPU 調度相關)具備更好的可移植性,一般不建議使用yield()方法來控制併發線程的執行。
在java中有如下3種方法能夠終止正在運行的線程:
interrupt:用於中斷線程。調用該方法的線程的狀態爲將被置爲」中斷」狀態。
注意:線程中斷僅僅是置線程的中斷狀態位,不會中止線程。須要用戶本身去監視線程的狀態爲並作處理。支持線程中斷的方法(也就是線程中斷後會拋出interruptedException 的方法)就是在監視線程的中斷狀態,一旦線程的中斷狀態被置爲「中斷狀態」,就會拋出中斷異常。
interrupted:是靜態方法,查看當前中斷信號是true仍是false而且清除中斷信號。若是一個線程被中斷了,第一次調用 interrupted 則返回 true,第二次和後面的就返回 false 了。
isInterrupted:查看當前中斷信號是true仍是false
阻塞式方法是指程序會一直等待該方法完成期間不作其餘事情,ServerSocket 的accept()方法就是一直等待客戶端鏈接。這裏的阻塞是指調用結果返回以前,當前線程會被掛起,直到獲得結果以後纔會返回。此外,還有異步和非阻塞式方法在任務完成前就返回。
首先 ,wait()、notify() 方法是針對對象的,調用任意對象的 wait()方法都將致使線程阻塞,阻塞的同時也將釋放該對象的鎖,相應地,調用任意對象的 notify()方法則將隨機解除該對象阻塞的線程,但它須要從新獲取該對象的鎖,直到獲取成功才能往下執行;
其次,wait、notify 方法必須在 synchronized 塊或方法中被調用,而且要保證同步塊或方法的鎖對象與調用 wait、notify 方法的對象是同一個,如此一來在調用 wait 以前當前線程就已經成功獲取某對象的鎖,執行 wait 阻塞後當前線程就將以前獲取的對象鎖釋放。
若是線程調用了對象的 wait()方法,那麼線程便會處於該對象的等待池中,等待池中的線程不會去競爭該對象的鎖。
notifyAll() 會喚醒全部的線程,notify() 只會喚醒一個線程。
notifyAll() 調用後,會將所有線程由等待池移到鎖池,而後參與鎖的競爭,競爭成功則繼續執行,若是不成功則留在鎖池等待鎖被釋放後再次參與競爭。而 notify()只會喚醒一個線程,具體喚醒哪個線程由虛擬機控制。
能夠經過中斷 和 共享變量的方式實現線程間的通信和協做
好比說最經典的生產者-消費者模型:當隊列滿時,生產者須要等待隊列有空間才能繼續往裏面放入商品,而在等待的期間內,生產者必須釋放對臨界資源(即隊列)的佔用權。由於生產者若是不釋放對臨界資源的佔用權,那麼消費者就沒法消費隊列中的商品,就不會讓隊列有空間,那麼生產者就會一直無限等待下去。所以,通常狀況下,當隊列滿時,會讓生產者交出對臨界資源的佔用權,並進入掛起狀態。而後等待消費者消費了商品,而後消費者通知生產者隊列有空間了。一樣地,當隊列空時,消費者也必須等待,等待生產者通知它隊列中有商品了。這種互相通訊的過程就是線程間的協做。
Java中線程通訊協做的最多見的兩種方式:
一.syncrhoized加鎖的線程的Object類的wait()/notify()/notifyAll()
二.ReentrantLock類加鎖的線程的Condition類的await()/signal()/signalAll()
線程間直接的數據交換:
三.經過管道進行線程間通訊:1)字節流;2)字符流
在兩個線程間共享變量便可實現共享。
通常來講,共享變量要求變量自己是線程安全的,而後在線程內使用的時候,若是有對共享變量的複合操做,那麼也得保證複合操做的線程安全性。
同步塊是更好的選擇,由於它不會鎖住整個對象(固然你也可讓它鎖住整個對象)。同步方法會鎖住整個對象,哪怕這個類中有多個不相關聯的同步塊,這一般會致使他們中止執行並須要等待得到這個對象上的鎖。
同步塊更要符合開放調用的原則,只在須要鎖住的代碼塊鎖住相應的對象,這樣從側面來講也能夠避免死鎖。
請知道一條原則:同步的範圍越小越好。
當一個線程對共享的數據進行操做時,應使之成爲一個」原子操做「,即在沒有完成相關操做以前,不容許其餘線程打斷它,不然,就會破壞數據的完整性,必然會獲得錯誤的處理結果,這就是線程的同步。
在多線程應用中,考慮不一樣線程之間的數據同步和防止死鎖。當兩個或多個線程之間同時等待對方釋放資源的時候就會造成線程之間的死鎖。爲了防止死鎖的發生,須要經過同步來實現線程安全。
線程互斥是指對於共享的進程系統資源,在各單個線程訪問時的排它性。當有若干個線程都要使用某一共享資源時,任什麼時候刻最多隻容許一個線程去使用,其它要使用該資源的線程必須等待,直到佔用資源者釋放該資源。線程互斥能夠當作是一種特殊的線程同步。
線程間的同步方法大致可分爲兩類:用戶模式和內核模式。顧名思義,內核模式就是指利用系統內核對象的單一性來進行同步,使用時須要切換內核態與用戶態,而用戶模式就是不須要切換到內核態,只在用戶態完成操做。
用戶模式下的方法有:原子操做(例如一個單一的全局變量),臨界區。內核模式下的方法有:事件,信號量,互斥量。
實現線程同步的方法
在 java 虛擬機中,每一個對象( Object 和 class )經過某種邏輯關聯監視器,每一個監視器和一個對象引用相關聯,爲了實現監視器的互斥功能,每一個對象都關聯着一把鎖。
一旦方法或者代碼塊被 synchronized 修飾,那麼這個部分就放入了監視器的監視區域,確保一次只能有一個線程執行該部分的代碼,線程在獲取鎖以前不容許執行該部分的代碼
另外 java 還提供了顯式監視器( Lock )和隱式監視器( synchronized )兩種鎖方案
線程安全是編程中的術語,指某個方法在多線程環境中被調用時,可以正確地處理多個線程之間的共享變量,使程序功能正確完成。
Servlet 不是線程安全的,servlet 是單實例多線程的,當多個線程同時訪問同一個方法,是不能保證共享變量的線程安全性的。
Struts2 的 action 是多實例多線程的,是線程安全的,每一個請求過來都會 new 一個新的 action 分配給這個請求,請求完成後銷燬。
SpringMVC 的 Controller 是線程安全的嗎?不是的,和 Servlet 相似的處理流程。
Struts2 好處是不用考慮線程安全問題;Servlet 和 SpringMVC 須要考慮線程安全問題,可是性能能夠提高不用處理太多的 gc,可使用 ThreadLocal 來處理多線程的問題。
手動鎖 Java 示例代碼以下:
Lock lock = new ReentrantLock(); lock. lock(); try { System. out. println("得到鎖"); } catch (Exception e) { // TODO: handle exception } finally { System. out. println("釋放鎖"); lock. unlock(); } 12345678910
每個線程都是有優先級的,通常來講,高優先級的線程在運行時會具備優先權,但這依賴於線程調度的實現,這個實現是和操做系統相關的(OS dependent)。咱們能夠定義線程的優先級,可是這並不能保證高優先級的線程會在低優先級的線程前執行。線程優先級是一個 int 變量(從 1-10),1 表明最低優先級,10 表明最高優先級。
Java 的線程優先級調度會委託給操做系統去處理,因此與具體的操做系統優先級有關,如非特別須要,通常無需設置線程優先級。
這是一個很是刁鑽和狡猾的問題。請記住:線程類的構造方法、靜態塊是被 new這個線程類所在的線程所調用的,而 run 方法裏面的代碼纔是被線程自身所調用的。
若是說上面的說法讓你感到困惑,那麼我舉個例子,假設 Thread2 中 new 了Thread1,main 函數中 new 了 Thread2,那麼:
(1)Thread2 的構造方法、靜態塊是 main 線程調用的,Thread2 的 run()方法是Thread2 本身調用的
(2)Thread1 的構造方法、靜態塊是 Thread2 調用的,Thread1 的 run()方法是Thread1 本身調用的
Dump文件是進程的內存鏡像。能夠把程序的執行狀態經過調試器保存到dump文件中。
在 Linux 下,你能夠經過命令 kill -3 PID (Java 進程的進程 ID)來獲取 Java應用的 dump 文件。
在 Windows 下,你能夠按下 Ctrl + Break 來獲取。這樣 JVM 就會將線程的 dump 文件打印到標準輸出或錯誤文件中,它可能打印在控制檯或者日誌文件中,具體位置依賴應用的配置。
若是異常沒有被捕獲該線程將會中止執行。Thread.UncaughtExceptionHandler是用於處理未捕獲異常形成線程忽然中斷狀況的一個內嵌接口。當一個未捕獲異常將形成線程中斷的時候,JVM 會使用 Thread.getUncaughtExceptionHandler()來查詢線程的 UncaughtExceptionHandler 並將線程和異常做爲參數傳遞給 handler 的 uncaughtException()方法進行處理。
線程的生命週期開銷很是高
消耗過多的 CPU
資源若是可運行的線程數量多於可用處理器的數量,那麼有線程將會被閒置。大量空閒的線程會佔用許多內存,給垃圾回收器帶來壓力,並且大量的線程在競爭 CPU資源時還將產生其餘性能的開銷。
下降穩定性JVM
在可建立線程的數量上存在一個限制,這個限制值將隨着平臺的不一樣而不一樣,而且承受着多個因素制約,包括 JVM 的啓動參數、Thread 構造函數中請求棧的大小,以及底層操做系統對線程的限制等。若是破壞了這些限制,那麼可能拋出OutOfMemoryError 異常。
在 Java 中,synchronized 關鍵字是用來控制線程同步的,就是在多線程的環境下,控制 synchronized 代碼段不被多個線程同時執行。synchronized 能夠修飾類、方法、變量。
另外,在 Java 早期版本中,synchronized屬於重量級鎖,效率低下,由於監視器鎖(monitor)是依賴於底層的操做系統的 Mutex Lock 來實現的,Java 的線程是映射到操做系統的原生線程之上的。若是要掛起或者喚醒一個線程,都須要操做系統幫忙完成,而操做系統實現線程之間的切換時須要從用戶態轉換到內核態,這個狀態之間的轉換須要相對比較長的時間,時間成本相對較高,這也是爲何早期的 synchronized 效率低的緣由。慶幸的是在 Java 6 以後 Java 官方對從 JVM 層面對synchronized 較大優化,因此如今的 synchronized 鎖效率也優化得很不錯了。JDK1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減小鎖操做的開銷。
synchronized關鍵字最主要的三種使用方式:
總結: synchronized 關鍵字加到 static 靜態方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖。synchronized 關鍵字加到實例方法上是給對象實例上鎖。儘可能不要使用 synchronized(String a) 由於JVM中,字符串常量池具備緩存功能!
下面我以一個常見的面試題爲例講解一下 synchronized 關鍵字的具體使用。
面試中面試官常常會說:「單例模式瞭解嗎?來給我手寫一下!給我解釋一下雙重檢驗鎖方式實現單例模式的原理唄!」
雙重校驗鎖實現對象單例(線程安全)
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { //先判斷對象是否已經實例過,沒有實例化過才進入加鎖代碼 if (uniqueInstance == null) { //類對象加鎖 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } } 1234567891011121314151617181920
另外,須要注意 uniqueInstance 採用 volatile 關鍵字修飾也是頗有必要。
uniqueInstance 採用 volatile 關鍵字修飾也是頗有必要的, uniqueInstance = new Singleton(); 這段代碼實際上是分爲三步執行:
可是因爲 JVM 具備指令重排的特性,執行順序有可能變成 1->3->2。指令重排在單線程環境下不會出現問題,可是在多線程環境下會致使一個線程得到尚未初始化的實例。例如,線程 T1 執行了 1 和 3,此時 T2 調用 getUniqueInstance() 後發現 uniqueInstance 不爲空,所以返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。
使用 volatile 能夠禁止 JVM 的指令重排,保證在多線程環境下也能正常運行。
synchronized是Java中的一個關鍵字,在使用的過程當中並無看到顯示的加鎖和解鎖過程。所以有必要經過javap命令,查看相應的字節碼文件。
synchronized 同步語句塊的狀況
public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("synchronized 代碼塊"); } } } 1234567
經過JDK 反彙編指令 javap -c -v SynchronizedDemo
能夠看出在執行同步代碼塊以前以後都有一個monitor字樣,其中前面的是monitorenter,後面的是離開monitorexit,不難想象一個線程也執行同步代碼塊,首先要獲取鎖,而獲取鎖的過程就是monitorenter ,在執行完代碼塊以後,要釋放鎖,釋放鎖就是執行monitorexit指令。
爲何會有兩個monitorexit呢?
這個主要是防止在同步代碼塊中線程因異常退出,而鎖沒有獲得釋放,這必然會形成死鎖(等待的線程永遠獲取不到鎖)。所以最後一個monitorexit是保證在異常狀況下,鎖也能夠獲得釋放,避免死鎖。
僅有ACC_SYNCHRONIZED這麼一個標誌,該標記代表線程進入該方法時,須要monitorenter,退出該方法時須要monitorexit。
synchronized可重入的原理
重入鎖是指一個線程獲取到該鎖以後,該線程能夠繼續得到該鎖。底層原理維護一個計數器,當線程獲取該鎖時,計數器加一,再次得到該鎖時繼續加一,釋放鎖時,計數器減一,當計數器值爲0時,代表該鎖未被任何線程所持有,其它線程能夠競爭獲取鎖。
不少 synchronized 裏面的代碼只是一些很簡單的代碼,執行時間很是快,此時等待的線程都加鎖多是一種不太值得的操做,由於線程阻塞涉及到用戶態和內核態切換的問題。既然 synchronized 裏面的代碼執行得很是快,不妨讓等待鎖的線程不要被阻塞,而是在 synchronized 的邊界作忙循環,這就是自旋。若是作了屢次循環發現尚未得到鎖,再阻塞,這樣多是一種更好的策略。
synchronized 鎖升級原理:在鎖對象的對象頭裏面有一個 threadid 字段,在第一次訪問的時候 threadid 爲空,jvm 讓其持有偏向鎖,並將 threadid 設置爲其線程 id,再次進入的時候會先判斷 threadid 是否與其線程 id 一致,若是一致則能夠直接使用此對象,若是不一致,則升級偏向鎖爲輕量級鎖,經過自旋循環必定次數來獲取鎖,執行必定次數以後,若是尚未正常獲取到要使用的對象,此時就會把鎖從輕量級升級爲重量級鎖,此過程就構成了 synchronized 鎖的升級。
鎖的升級的目的:鎖升級是爲了減低了鎖帶來的性能消耗。在 Java 6 以後優化 synchronized 的實現方式,使用了偏向鎖升級爲輕量級鎖再升級到重量級鎖的方式,從而減低了鎖帶來的性能消耗。
(1)volatile 修飾變量
(2)synchronized 修飾修改變量的方法
(3)wait/notify
(4)while 輪詢
不能。其它線程只能訪問該對象的非同步方法,同步方法則不能進入。由於非靜態方法上的 synchronized 修飾符要求執行方法時要得到對象的鎖,若是已經進入A 方法說明對象鎖已經被取走,那麼試圖進入 B 方法的線程就只能在等鎖池(注意不是等待池哦)中等待對象的鎖。
(1)synchronized 是悲觀鎖,屬於搶佔式,會引發其餘線程阻塞。
(2)volatile 提供多線程共享變量可見性和禁止指令重排序優化。
(3)CAS 是基於衝突檢測的樂觀鎖(非阻塞)
synchronized 是和 if、else、for、while 同樣的關鍵字,ReentrantLock 是類,這是兩者的本質區別。既然 ReentrantLock 是類,那麼它就提供了比synchronized 更多更靈活的特性,能夠被繼承、能夠有方法、能夠有各類各樣的類變量
synchronized 早期的實現比較低效,對比 ReentrantLock,大多數場景性能都相差較大,可是在 Java 6 中對 synchronized 進行了很是多的改進。
相同點:二者都是可重入鎖
二者都是可重入鎖。「可重入鎖」概念是:本身能夠再次獲取本身的內部鎖。好比一個線程得到了某個對象的鎖,此時這個對象鎖尚未釋放,當其再次想要獲取這個對象的鎖的時候仍是能夠獲取的,若是不可鎖重入的話,就會形成死鎖。同一個線程每次獲取鎖,鎖的計數器都自增1,因此要等到鎖的計數器降低爲0時才能釋放鎖。
主要區別以下:
Java中每個對象均可以做爲鎖,這是synchronized實現同步的基礎:
普通同步方法,鎖是當前實例對象
靜態同步方法,鎖是當前類的class對象
同步方法塊,鎖是括號裏面的對象
在Java中,鎖共有4種狀態,級別從低到高依次爲:無狀態鎖,偏向鎖,輕量級鎖和重量級鎖狀態,這幾個狀態會隨着競爭狀況逐漸升級。鎖能夠升級但不能降級。
百度百科
:死鎖是指兩個或兩個以上的進程(線程)在執行過程當中,因爲競爭資源或者因爲彼此通訊而形成的一種阻塞的現象,若無外力做用,它們都將沒法推動下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程(線程)稱爲死鎖進程(線程)。
多個線程同時被阻塞,它們中的一個或者所有都在等待某個資源被釋放。因爲線程被無限期地阻塞,所以程序不可能正常終止。
以下圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時都想申請對方的資源,因此這兩個線程就會互相等待而進入死鎖狀態。
下面經過一個例子來講明線程死鎖,代碼模擬了上圖的死鎖的狀況 (代碼來源於《併發編程之美》):
public class DeadLockDemo { private static Object resource1 = new Object();//資源 1 private static Object resource2 = new Object();//資源 2 public static void main(String[] args) { new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource2"); synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); } } }, "線程 1").start(); new Thread(() -> { synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource1"); synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); } } }, "線程 2").start(); } } 123456789101112131415161718192021222324252627282930313233343536
輸出結果
Thread[線程 1,5,main]get resource1 Thread[線程 2,5,main]get resource2 Thread[線程 1,5,main]waiting get resource2 Thread[線程 2,5,main]waiting get resource1 1234
線程 A 經過 synchronized (resource1) 得到 resource1 的監視器鎖,而後經過Thread.sleep(1000)
;讓線程 A 休眠 1s 爲的是讓線程 B 獲得CPU執行權,而後獲取到 resource2 的監視器鎖。線程 A 和線程 B 休眠結束了都開始企圖請求獲取對方的資源,而後這兩個線程就會陷入互相等待的狀態,這也就產生了死鎖。上面的例子符合產生死鎖的四個必要條件。
產生死鎖的必要條件:
一、互斥條件:所謂互斥就是進程在某一時間內獨佔資源。
二、請求與保持條件:一個進程因請求資源而阻塞時,對已得到的資源保持不放。
三、不剝奪條件:進程已得到資源,在末使用完以前,不能強行剝奪。
四、循環等待條件:若干進程之間造成一種頭尾相接的循環等待資源關係。
這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之 一不知足,就不會發生死鎖。
理解了死鎖的緣由,尤爲是產生死鎖的四個必要條件,就能夠最大可能地避免、預防和 解除死鎖。
防止死鎖能夠採用如下的方法:
咱們只要破壞產生死鎖的四個條件中的其中一個就能夠了。
破壞互斥條件
這個條件咱們沒有辦法破壞,由於咱們用鎖原本就是想讓他們互斥的(臨界資源須要互斥訪問)。
破壞請求與保持條件
一次性申請全部的資源。
破壞不剝奪條件
佔用部分資源的線程進一步申請其餘資源時,若是申請不到,能夠主動釋放它佔有的資源。
破壞循環等待條件
靠按序申請資源來預防。按某一順序申請資源,釋放資源則反序釋放。破壞循環等待條件。
咱們對線程 2 的代碼修改爲下面這樣就不會產生死鎖了。
new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource2"); synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); } } }, "線程 2").start(); 1234567891011121314
輸出結果
Thread[線程 1,5,main]get resource1 Thread[線程 1,5,main]waiting get resource2 Thread[線程 1,5,main]get resource2 Thread[線程 2,5,main]get resource1 Thread[線程 2,5,main]waiting get resource2 Thread[線程 2,5,main]get resource2 123456
咱們分析一下上面的代碼爲何避免了死鎖的發生?
線程 1 首先得到到 resource1 的監視器鎖,這時候線程 2 就獲取不到了。而後線程 1 再去獲取 resource2 的監視器鎖,能夠獲取到。而後線程 1 釋放了對 resource一、resource2 的監視器鎖的佔用,線程 2 獲取到就能夠執行了。這樣就破壞了破壞循環等待條件,所以避免了死鎖。
死鎖:是指兩個或兩個以上的進程(或線程)在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。
活鎖:任務或者執行者沒有被阻塞,因爲某些條件沒有知足,致使一直重複嘗試,失敗,嘗試,失敗。
活鎖和死鎖的區別在於,處於活鎖的實體是在不斷的改變狀態,這就是所謂的「活」, 而處於死鎖的實體表現爲等待;活鎖有可能自行解開,死鎖則不能。
飢餓:一個或者多個線程由於種種緣由沒法得到所須要的資源,致使一直沒法執行的狀態。
Java 中致使飢餓的緣由:
一、高優先級線程吞噬全部的低優先級線程的 CPU 時間。
二、線程被永久堵塞在一個等待進入同步塊的狀態,由於其餘線程老是能在它以前持續地對該同步塊進行訪問。
三、線程在等待一個自己也處於永久等待完成的對象(好比調用這個對象的 wait 方法),由於其餘線程老是被持續地得到喚醒。
池化技術相比你們已經家常便飯了,線程池、數據庫鏈接池、Http 鏈接池等等都是對這個思想的應用。池化技術的思想主要是爲了減小每次獲取資源的消耗,提升對資源的利用率。
在面向對象編程中,建立和銷燬對象是很費時間的,由於建立一個對象要獲取內存資源或者其它更多資源。在 Java 中更是如此,虛擬機將試圖跟蹤每個對象,以便可以在對象銷燬後進行垃圾回收。因此提升服務程序效率的一個手段就是儘量減小建立和銷燬對象的次數,特別是一些很耗資源的對象建立和銷燬,這就是」池化資源」技術產生的緣由。
線程池顧名思義就是事先建立若干個可執行的線程放入一個池(容器)中,須要的時候從池中獲取線程不用自行建立,使用完畢不須要銷燬線程而是放回池中,從而減小建立和銷燬線程對象的開銷。Java 5+中的 Executor 接口定義一個執行線程的工具。它的子類型即線程池接口是 ExecutorService。要配置一個線程池是比較複雜的,尤爲是對於線程池的原理不是很清楚的狀況下,所以在工具類 Executors 面提供了一些靜態工廠方法,生成一些經常使用的線程池,以下所示:
(1)newSingleThreadExecutor:建立一個單線程的線程池。這個線程池只有一個線程在工做,也就是至關於單線程串行執行全部任務。若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它。此線程池保證全部任務的執行順序按照任務的提交順序執行。
(2)newFixedThreadPool:建立固定大小的線程池。每次提交一個任務就建立一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,若是某個線程由於執行異常而結束,那麼線程池會補充一個新線程。若是但願在服務器上使用線程池,建議使用 newFixedThreadPool方法來建立線程池,這樣能得到更好的性能。
(3) newCachedThreadPool:建立一個可緩存的線程池。若是線程池的大小超過了處理任務所須要的線程,那麼就會回收部分空閒(60 秒不執行任務)的線程,當任務數增長時,此線程池又能夠智能的添加新線程來處理任務。此線程池不會對線程池大小作限制,線程池大小徹底依賴於操做系統(或者說 JVM)可以建立的最大線程大小。
(4)newScheduledThreadPool:建立一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。
綜上所述使用線程池框架 Executor 能更好的管理線程、提供系統資源使用率。
Executor 框架是一個根據一組執行策略調用,調度,執行和控制的異步任務的框架。
每次執行任務建立線程 new Thread()比較消耗性能,建立一個線程是比較耗時、耗資源的,並且無限制的建立線程會引發應用程序內存溢出。
因此建立一個線程池是個更好的的解決方案,由於能夠限制線程的數量而且能夠回收再利用這些線程。利用Executors 框架能夠很是方便的建立一個線程池。
接收參數:execute()只能執行 Runnable 類型的任務。submit()能夠執行 Runnable 和 Callable 類型的任務。
返回值:submit()方法能夠返回持有計算結果的 Future 對象,而execute()沒有
異常處理:submit()方便Exception處理
ThreadGroup 類,能夠把線程歸屬到某一個線程組中,線程組中能夠有線程對象,也能夠有線程組,組中還能夠有線程,這樣的組織結構有點相似於樹的形式。
線程組和線程池是兩個不一樣的概念,他們的做用徹底不一樣,前者是爲了方便線程的管理,後者是爲了管理線程的生命週期,複用線程,減小建立銷燬線程的開銷。
爲何不推薦使用線程組?由於使用有不少的安全隱患吧,沒有具體追究,若是須要使用,推薦使用線程池。
《阿里巴巴Java開發手冊》中強制線程池不容許使用 Executors 去建立,而是經過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同窗更加明確線程池的運行規則,規避資源耗盡的風險
Executors 各個方法的弊端:
ThreaPoolExecutor建立線程池方式只有一種,就是走它的構造函數,參數本身指定
建立線程池的方式有多種,這裏你只須要答 ThreadPoolExecutor 便可。
ThreadPoolExecutor() 是最原始的線程池建立,也是阿里巴巴 Java 開發手冊中明確規範的建立線程池的方式。
ThreadPoolExecutor 3 個最重要的參數:
ThreadPoolExecutor
其餘常見參數:
corePoolSize
的時候,若是這時沒有新的任務提交,核心線程外的線程不會當即銷燬,而是會等待,直到等待的時間超過了 keepAliveTime
纔會被回收銷燬;keepAliveTime
參數的時間單位。ThreadPoolExecutor 飽和策略定義:
若是當前同時運行的線程數量達到最大線程數量而且隊列也已經被放滿了任時,ThreadPoolTaskExecutor
定義一些策略:
RejectedExecutionException
來拒絕新任務的處理。舉個例子: Spring 經過 ThreadPoolTaskExecutor
或者咱們直接經過 ThreadPoolExecutor
的構造函數建立線程池的時候,當咱們不指定 RejectedExecutionHandler
飽和策略的話來配置線程池的時候默認使用的是 ThreadPoolExecutor.AbortPolicy
。在默認狀況下,ThreadPoolExecutor
將拋出 RejectedExecutionException
來拒絕新來的任務 ,這表明你將丟失對這個任務的處理。 對於可伸縮的應用程序,建議使用 ThreadPoolExecutor.CallerRunsPolicy
。當最大池被填滿時,此策略爲咱們提供可伸縮隊列。(這個直接查看 ThreadPoolExecutor
的構造函數源碼就能夠看出,比較簡單的緣由,這裏就不貼代碼了)
Runnable
+ThreadPoolExecutor
線程池實現原理
爲了讓你們更清楚上面的面試題中的一些概念,我寫了一個簡單的線程池 Demo。
首先建立一個 Runnable
接口的實現類(固然也能夠是 Callable
接口,咱們上面也說了二者的區別。)
import java.util.Date; /** * 這是一個簡單的Runnable類,須要大約5秒鐘來執行其任務。 */ public class MyRunnable implements Runnable { private String command; public MyRunnable(String s) { this.command = s; } @Override public void run() { System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date()); processCommand(); System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date()); } private void processCommand() { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } @Override public String toString() { return this.command; } } 123456789101112131415161718192021222324252627282930313233
編寫測試程序,咱們這裏以阿里巴巴推薦的使用 ThreadPoolExecutor
構造函數自定義參數的方式來建立線程池。
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadPoolExecutorDemo { private static final int CORE_POOL_SIZE = 5; private static final int MAX_POOL_SIZE = 10; private static final int QUEUE_CAPACITY = 100; private static final Long KEEP_ALIVE_TIME = 1L; public static void main(String[] args) { //使用阿里巴巴推薦的建立線程池的方式 //經過ThreadPoolExecutor構造函數自定義參數建立 ThreadPoolExecutor executor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, new ArrayBlockingQueue<>(QUEUE_CAPACITY), new ThreadPoolExecutor.CallerRunsPolicy()); for (int i = 0; i < 10; i++) { //建立WorkerThread對象(WorkerThread類實現了Runnable 接口) Runnable worker = new MyRunnable("" + i); //執行Runnable executor.execute(worker); } //終止線程池 executor.shutdown(); while (!executor.isTerminated()) { } System.out.println("Finished all threads"); } } 1234567891011121314151617181920212223242526272829303132333435
能夠看到咱們上面的代碼指定了:
corePoolSize
: 核心線程數爲 5。maximumPoolSize
:最大線程數 10keepAliveTime
: 等待時間爲 1L。unit
: 等待時間的單位爲 TimeUnit.SECONDS。workQueue
:任務隊列爲 ArrayBlockingQueue
,而且容量爲 100;handler
:飽和策略爲 CallerRunsPolicy
。Output:
pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019 pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019 pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019 pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019 pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019 pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019 pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019 pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019 pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019 pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019 1234567891011121314151617181920
這裏區分一下:
(1)若是使用的是無界隊列 LinkedBlockingQueue,也就是無界隊列的話,不要緊,繼續添加任務到阻塞隊列中等待執行,由於 LinkedBlockingQueue 能夠近乎認爲是一個無窮大的隊列,能夠無限存聽任務
(2)若是使用的是有界隊列好比 ArrayBlockingQueue,任務首先會被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 滿了,會根據maximumPoolSize 的值增長線程數量,若是增長了線程數量仍是處理不過來,ArrayBlockingQueue 繼續滿,那麼則會使用拒絕策略RejectedExecutionHandler 處理滿了的任務,默認是 AbortPolicy
ThreadLocal 是一個本地線程副本變量工具類,在每一個線程中都建立了一個 ThreadLocalMap 對象,簡單說 ThreadLocal 就是一種以空間換時間的作法,每一個線程能夠訪問本身內部 ThreadLocalMap 對象內的 value。經過這種方式,避免資源在多線程間共享。
原理:線程局部變量是侷限於線程內部的變量,屬於線程自身全部,不在多個線程間共享。Java提供ThreadLocal類來支持線程局部變量,是一種實現線程安全的方式。可是在管理環境下(如 web 服務器)使用線程局部變量的時候要特別當心,在這種狀況下,工做線程的生命週期比任何應用變量的生命週期都要長。任何線程局部變量一旦在工做完成後沒有釋放,Java 應用就存在內存泄露的風險。
經典的使用場景是爲每一個線程分配一個 JDBC 鏈接 Connection。這樣就能夠保證每一個線程的都在各自的 Connection 上進行數據庫的操做,不會出現 A 線程關了 B線程正在使用的 Connection; 還有 Session 管理 等問題。
ThreadLocal 使用例子:
public class TestThreadLocal { //線程本地存儲變量 private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return 0; } }; public static void main(String[] args) { for (int i = 0; i <3; i++) {//啓動三個線程 Thread t = new Thread() { @Override public void run() { add10ByThreadLocal(); } }; t.start(); } } /** * 線程本地存儲變量加 5 */ private static void add10ByThreadLocal() { for (int i = 0; i <5; i++) { Integer n = THREAD_LOCAL_NUM.get(); n += 1; THREAD_LOCAL_NUM.set(n); System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n); } } } 123456789101112131415161718192021222324252627282930313233343536
打印結果:啓動了 3 個線程,每一個線程最後都打印到 「ThreadLocal num=5」,而不是 num 一直在累加直到值等於 15
Thread-0 : ThreadLocal num=1 Thread-1 : ThreadLocal num=1 Thread-0 : ThreadLocal num=2 Thread-0 : ThreadLocal num=3 Thread-1 : ThreadLocal num=2 Thread-2 : ThreadLocal num=1 Thread-0 : ThreadLocal num=4 Thread-2 : ThreadLocal num=2 Thread-1 : ThreadLocal num=3 Thread-1 : ThreadLocal num=4 Thread-2 : ThreadLocal num=3 Thread-0 : ThreadLocal num=5 Thread-2 : ThreadLocal num=4 Thread-2 : ThreadLocal num=5 Thread-1 : ThreadLocal num=5 123456789101112131415
線程局部變量是侷限於線程內部的變量,屬於線程自身全部,不在多個線程間共享。Java 提供 ThreadLocal 類來支持線程局部變量,是一種實現線程安全的方式。可是在管理環境下(如 web 服務器)使用線程局部變量的時候要特別當心,在這種狀況下,工做線程的生命週期比任何應用變量的生命週期都要長。任何線程局部變量一旦在工做完成後沒有釋放,Java 應用就存在內存泄露的風險。
ThreadLocalMap
中使用的 key 爲 ThreadLocal
的弱引用,而 value 是強引用。因此,若是 ThreadLocal
沒有被外部強引用的狀況下,在垃圾回收的時候,key 會被清理掉,而 value 不會被清理掉。這樣一來,ThreadLocalMap
中就會出現key爲null的Entry。假如咱們不作任何措施的話,value 永遠沒法被GC 回收,這個時候就可能會產生內存泄露。ThreadLocalMap實現中已經考慮了這種狀況,在調用 set()
、get()
、remove()
方法的時候,會清理掉 key 爲 null 的記錄。使用完 ThreadLocal
方法後 最好手動調用remove()
方法
瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高併發實戰》 面試必備 + 面試必備 + 面試必備
瘋狂創客圈 - Java高併發研習社羣,爲你們開啓大廠之門