Java面試總結匯總,整理了包括Java重點知識,以及經常使用開源框架,歡迎你們閱讀。文章可能有錯誤的地方,由於我的知識有限,歡迎各位大佬指出!文章持續更新中......java
ID 標題 地址 1 設計模式面試題(總結最全面的面試題) juejin.cn/post/684490… 2 Java基礎知識面試題(總結最全面的面試題) juejin.cn/post/684490… 3 Java集合面試題(總結最全面的面試題) juejin.cn/post/684490… 4 JavaIO、BIO、NIO、AIO、Netty面試題(總結最全面的面試題) juejin.cn/post/684490… 5 Java併發編程面試題(總結最全面的面試題) juejin.cn/post/684490… 6 Java異常面試題(總結最全面的面試題) juejin.cn/post/684490… 7 Java虛擬機(JVM)面試題(總結最全面的面試題) juejin.cn/post/684490… 8 Spring面試題(總結最全面的面試題) juejin.cn/post/684490… 9 Spring MVC面試題(總結最全面的面試題) juejin.cn/post/684490… 10 Spring Boot面試題(總結最全面的面試題) juejin.cn/post/684490… 11 Spring Cloud面試題(總結最全面的面試題) juejin.cn/post/684490… 12 Redis面試題(總結最全面的面試題) juejin.cn/post/684490… 13 MyBatis面試題(總結最全面的面試題) juejin.cn/post/684490… 14 MySQL面試題(總結最全面的面試題) juejin.cn/post/684490… 15 TCP、UDP、Socket、HTTP面試題(總結最全面的面試題) juejin.cn/post/684490… 16 Nginx面試題(總結最全面的面試題) juejin.cn/post/684490… 17 ElasticSearch面試題 18 kafka面試題 19 RabbitMQ面試題(總結最全面的面試題) juejin.cn/post/684490… 20 Dubbo面試題(總結最全面的面試題) juejin.cn/post/684490… 21 ZooKeeper面試題(總結最全面的面試題) juejin.cn/post/684490… 22 Netty面試題(總結最全面的面試題) 23 Tomcat面試題(總結最全面的面試題) juejin.cn/post/684490… 24 Linux面試題(總結最全面的面試題) juejin.cn/post/684490… 25 互聯網相關面試題(總結最全面的面試題) 26 互聯網安全面試題(總結最全面的面試題)
提高多核CPU的利用率:通常來講一臺主機上的會有多個CPU核心,咱們能夠建立多個線程,理論上講操做系統能夠將多個線程分配給不一樣的CPU去執行,每一個CPU執行一個線程,這樣就提升了CPU的使用效率,若是使用單線程就只能有一個CPU核心被使用。linux
好比當咱們在網上購物時,爲了提高響應速度,須要拆分,減庫存,生成訂單等等這些操做,就能夠進行拆分利用多線程的技術完成。面對複雜業務模型,並行程序會比串行程序更適應業務需求,而併發編程更能吻合這種業務拆分 。程序員
簡單來講就是:面試
出現線程安全問題的緣由通常都是三個緣由:算法
線程切換帶來的原子性問題 解決辦法:使用多線程之間同步synchronized或使用鎖(lock)。數據庫
緩存致使的可見性問題 解決辦法:synchronized、volatile、LOCK,能夠解決可見性問題編程
編譯優化帶來的有序性問題 解決辦法:Happens-Before 規則能夠解決有序性問題windows
作一個形象的比喻:設計模式
併發 = 倆我的用一臺電腦。數組
並行 = 倆我的分配了倆臺電腦。
串行 = 倆我的排隊使用一臺電腦。
線程也是程序,因此線程須要佔用內存,線程越多佔用內存也越多;
多線程須要協調和管理,因此須要 CPU 時間跟蹤線程;
線程之間對共享資源的訪問會相互影響,必須解決競用共享資源的問題。
什麼是線程和進程?
進程
一個在內存中運行的應用程序。 每一個正在系統上運行的程序都是一個進程
線程
進程中的一個執行任務(控制單元), 它負責在程序裏獨立執行。
一個進程至少有一個線程,一個進程能夠運行多個線程,多個線程可共享數據。
進程與線程的區別
根本區別:進程是操做系統資源分配的基本單位,而線程是處理器任務調度和執行的基本單位
資源開銷:每一個進程都有獨立的代碼和數據空間(程序上下文),程序之間的切換會有較大的開銷;線程能夠看作輕量級的進程,同一類線程共享代碼和數據空間,每一個線程都有本身獨立的運行棧和程序計數器(PC),線程之間切換的開銷小。
包含關係:若是一個進程內有多個線程,則執行過程不是一條線的,而是多條線(線程)共同完成的;線程是進程的一部分,因此線程也被稱爲輕權進程或者輕量級進程。
內存分配:同一進程的線程共享本進程的地址空間和資源,而進程與進程之間的地址空間和資源是相互獨立的
影響關係:一個進程崩潰後,在保護模式下不會對其餘進程產生影響,可是一個線程崩潰有可能致使整個進程都死掉。因此多進程要比多線程健壯。
執行過程:每一個獨立的進程有程序運行的入口、順序執行序列和程序出口。可是線程不能獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制,二者都可併發執行
多線程編程中通常線程的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,爲了讓這些線程都能獲得有效執行,CPU 採起的策略是爲每一個線程分配時間片並輪轉的形式。當一個線程的時間片用完的時候就會從新處於就緒狀態讓給其餘線程使用,這個過程就屬於一次上下文切換。
歸納來講就是:當前任務在執行完 CPU 時間片切換到另外一個任務以前會先保存本身的狀態,以便下次再切換回這個任務時,能夠再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換。
上下文切換一般是計算密集型的。也就是說,它須要至關可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都須要納秒量級的時間。因此,上下文切換對系統來講意味着消耗大量的 CPU 時間,事實上,多是操做系統中時間消耗最大的操做。
Linux 相比與其餘操做系統(包括其餘類 Unix 系統)有不少的優勢,其中有一項就是,其上下文切換和模式切換的時間消耗很是少。
或者直接使用JDK自帶的工具查看「jconsole」 、「visualVm」,這都是JDK自帶的,能夠直接在JDK的bin目錄下找到直接使用
繼承 Thread 類;
public class MyThread extends Thread { @Override public void run() { System.out.println(Thread.currentThread().getName() + " run()方法正在執行..."); } 複製代碼
實現 Runnable 接口;
public class MyRunnable implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + " run()方法執行中..."); } 複製代碼
實現 Callable 接口;
public class MyCallable implements Callable<Integer> { @Override public Integer call() { System.out.println(Thread.currentThread().getName() + " call()方法執行中..."); return 1; } 複製代碼
使用匿名內部類方式
public class CreateRunnable { public static void main(String[] args) { //建立多線程建立開始 Thread thread = new Thread(new Runnable() { public void run() { for (int i = 0; i < 10; i++) { System.out.println("i:" + i); } } }); thread.start(); } } 複製代碼
相同點:
主要區別:
每一個線程都是經過某個特定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 用於獲取結果。
新建(new):新建立了一個線程對象。
就緒(可運行狀態)(runnable):線程對象建立後,當調用線程對象的 start()方法,該線程處於就緒狀態,等待被線程調度選中,獲取cpu的使用權。
運行(running):可運行狀態(runnable)的線程得到了cpu時間片(timeslice),執行程序代碼。注:就緒狀態是進入到運行狀態的惟一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中;
阻塞(block):處於運行狀態中的線程因爲某種緣由,暫時放棄對 CPU的使用權,中止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才 有機會再次被 CPU 調用以進入到運行狀態。
死亡(dead)(結束):線程run()、main()方法執行結束,或者因異常退出了run()方法,則該線程結束生命週期。死亡的線程不可再次復生。
計算機一般只有一個 CPU,在任意時刻只能執行一條機器指令,每一個線程只有得到CPU 的使用權才能執行指令。所謂多線程的併發運行,實際上是指從宏觀上看,各個線程輪流得到 CPU 的使用權,分別執行各自的任務。在運行池中,會有多個處於就緒狀態的線程在等待 CPU,JAVA 虛擬機的一項任務就是負責線程的調度,線程調度是指按照特定機制爲多個線程分配 CPU 的使用權。(Java是由JVM中的線程計數器來實現線程調度)
有兩種調度模型:分時調度模型和搶佔式調度模型。
分時調度模型是指讓全部的線程輪流得到 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(); } // 處理其餘的業務邏輯 } 複製代碼
由於Java全部類的都繼承了Object,Java想讓任何對象均可以做爲鎖,而且 wait(),notify()等方法用於等待對象的鎖或者喚醒線程,在 Java 的線程中並無可供任何對象使用的鎖,因此任意對象調用方法必定定義在Object類中。
有的人會說,既然是線程放棄對象鎖,那也能夠把wait()定義在Thread類裏面啊,新定義的線程繼承於Thread類,也不須要從新定義wait()方法的實現。然而,這樣作有一個很是大的問題,一個線程徹底能夠持有不少鎖,你一個線程放棄鎖的時候,到底要放棄哪一個鎖?固然了,這種設計並非不能實現,只是管理起來更加複雜。
使當前線程從執行狀態(運行狀態)變爲可執行態(就緒狀態)。
當前線程到了就緒狀態,那麼接下來哪一個線程會從就緒狀態變成執行狀態呢?多是當前線程,也多是其餘線程,看系統的分配了。
(1) sleep()方法給其餘線程運行機會時不考慮線程的優先級,所以會給低優先級的線程以運行的機會;yield()方法只會給相同優先級或更高優先級的線程以運行的機會;
(2) 線程執行 sleep()方法後轉入阻塞(blocked)狀態,而執行 yield()方法後轉入就緒(ready)狀態;
(3)sleep()方法聲明拋出 InterruptedException,而 yield()方法沒有聲明任何異常;
(4)sleep()方法比 yield()方法(跟操做系統 CPU 調度相關)具備更好的可移植性,一般不建議使用yield()方法來控制併發線程的執行。
interrupt:用於中斷線程。調用該方法的線程的狀態爲將被置爲」中斷」狀態。
注意:線程中斷僅僅是置線程的中斷狀態位,不會中止線程。須要用戶本身去監視線程的狀態爲並作處理。支持線程中斷的方法(也就是線程中斷後會拋出interruptedException 的方法)就是在監視線程的中斷狀態,一旦線程的中斷狀態被置爲「中斷狀態」,就會拋出中斷異常。
interrupted:是靜態方法,查看當前中斷信號是true仍是false而且清除中斷信號。若是一個線程被中斷了,第一次調用 interrupted 則返回 true,第二次和後面的就返回 false 了。
isInterrupted:是能夠返回當前中斷信號是true仍是false,與interrupt最大的差異
首先 ,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()
線程間直接的數據交換:
同步塊是更好的選擇,由於它不會鎖住整個對象(固然你也可讓它鎖住整個對象)。同步方法會鎖住整個對象,哪怕這個類中有多個不相關聯的同步塊,這一般會致使他們中止執行並須要等待得到這個對象上的鎖。
同步塊更要符合開放調用的原則,只在須要鎖住的代碼塊鎖住相應的對象,這樣從側面來講也能夠避免死鎖。
請知道一條原則:同步的範圍越小越好。
當一個線程對共享的數據進行操做時,應使之成爲一個」原子操做「,即在沒有完成相關操做以前,不容許其餘線程打斷它,不然,就會破壞數據的完整性,必然會獲得錯誤的處理結果,這就是線程的同步。
在多線程應用中,考慮不一樣線程之間的數據同步和防止死鎖。當兩個或多個線程之間同時等待對方釋放資源的時候就會造成線程之間的死鎖。爲了防止死鎖的發生,須要經過同步來實現線程安全。
線程互斥是指對於共享的進程系統資源,在各單個線程訪問時的排它性。當有若干個線程都要使用某一共享資源時,任什麼時候刻最多隻容許一個線程去使用,其它要使用該資源的線程必須等待,直到佔用資源者釋放該資源。線程互斥能夠當作是一種特殊的線程同步。
線程間的同步方法大致可分爲兩類:用戶模式和內核模式。顧名思義,內核模式就是指利用系統內核對象的單一性來進行同步,使用時須要切換內核態與用戶態,而用戶模式就是不須要切換到內核態,只在用戶態完成操做。
用戶模式下的方法有:原子操做(例如一個單一的全局變量),臨界區。內核模式下的方法有:事件,信號量,互斥量。
實現線程同步的方法
同步代碼方法:sychronized 關鍵字修飾的方法
同步代碼塊:sychronized 關鍵字修飾的代碼塊
使用特殊變量域volatile實現線程同步:volatile關鍵字爲域變量的訪問提供了一種免鎖機制
使用重入鎖實現線程同步:reentrantlock類是可衝入、互斥、實現了lock接口的鎖他與sychronized方法具備相同的基本行爲和語義
在 java 虛擬機中,監視器和鎖在Java虛擬機中是一塊使用的。監視器監視一塊同步代碼塊,確保一次只有一個線程執行同步代碼塊。每個監視器都和一個對象引用相關聯。線程在獲取鎖以前不容許執行同步代碼。
一旦方法或者代碼塊被 synchronized 修飾,那麼這個部分就放入了監視器的監視區域,確保一次只能有一個線程執行該部分的代碼,線程在獲取鎖以前不容許執行該部分的代碼
另外 java 還提供了顯式監視器( Lock )和隱式監視器( synchronized )兩種鎖方案
有倆種可能:
(1)若是使用的是無界隊列 LinkedBlockingQueue,也就是無界隊列的話,不要緊,繼續添加任務到阻塞隊列中等待執行,由於 LinkedBlockingQueue 能夠近乎認爲是一個無窮大的隊列,能夠無限存聽任務
(2)若是使用的是有界隊列好比 ArrayBlockingQueue,任務首先會被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 滿了,會根據maximumPoolSize 的值增長線程數量,若是增長了線程數量仍是處理不過來,ArrayBlockingQueue 繼續滿,那麼則會使用拒絕策略RejectedExecutionHandler 處理滿了的任務,默認是 AbortPolicy
線程安全是編程中的術語,指某個方法在多線程環境中被調用時,可以正確地處理多個線程之間的共享變量,使程序功能正確完成。
Servlet 不是線程安全的,servlet 是單實例多線程的,當多個線程同時訪問同一個方法,是不能保證共享變量的線程安全性的。
Struts2 的 action 是多實例多線程的,是線程安全的,每一個請求過來都會 new 一個新的 action 分配給這個請求,請求完成後銷燬。
SpringMVC 的 Controller 是線程安全的嗎?不是的,和 Servlet 相似的處理流程。
Struts2 好處是不用考慮線程安全問題;Servlet 和 SpringMVC 須要考慮線程安全問題,可是性能能夠提高不用處理太多的 gc,能夠使用 ThreadLocal 來處理多線程的問題。
方法一:使用安全類,好比 java.util.concurrent 下的類,使用原子類AtomicInteger
方法二:使用自動鎖 synchronized。
方法三:使用手動鎖 Lock。
手動鎖 Java 示例代碼以下:
Lock lock = new ReentrantLock(); lock. lock(); try { System. out. println("得到鎖"); } catch (Exception e) { // TODO: handle exception } finally { System. out. println("釋放鎖"); lock. unlock(); } 複製代碼
每個線程都是有優先級的,通常來講,高優先級的線程在運行時會具備優先權,但這依賴於線程調度的實現,這個實現是和操做系統相關的(OS dependent)。咱們能夠定義線程的優先級,可是這並不能保證高優先級的線程會在低優先級的線程前執行。線程優先級是一個 int 變量(從 1-10),1 表明最低優先級,10 表明最高優先級。
Java 的線程優先級調度會委託給操做系統去處理,因此與具體的操做系統優先級有關,如非特別須要,通常無需設置線程優先級。
固然,若是你真的想設置優先級能夠經過setPriority()方法設置,可是設置了不必定會該變,這個是不許確的
這是一個很是刁鑽和狡猾的問題。請記住:線程類的構造方法、靜態塊是被 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 文件打印到標準輸出或錯誤文件中,它可能打印在控制檯或者日誌文件中,具體位置依賴應用的配置。
線程的生命週期開銷很是高
消耗過多的 CPU
資源若是可運行的線程數量多於可用處理器的數量,那麼有線程將會被閒置。大量空閒的線程會佔用許多內存,給垃圾回收器帶來壓力,並且大量的線程在競爭 CPU資源時還將產生其餘性能的開銷。
下降穩定性JVM
在可建立線程的數量上存在一個限制,這個限制值將隨着平臺的不一樣而不一樣,而且承受着多個因素制約,包括 JVM 的啓動參數、Thread 構造函數中請求棧的大小,以及底層操做系統對線程的限制等。若是破壞了這些限制,那麼可能拋出OutOfMemoryError 異常。
方法 名 | 描述 |
---|---|
sleep() | 強迫一個線程睡眠N毫秒 |
isAlive() | 判斷一個線程是否存活。 |
join() | 等待線程終止。 |
activeCount() | 程序中活躍的線程數。 |
enumerate() | 枚舉程序中的線程。 |
currentThread() | 獲得當前線程。 |
isDaemon() | 一個線程是否爲守護線程。 |
setDaemon() | 設置一個線程爲守護線程。 |
setName() | 爲線程設置一個名稱。 |
wait() | 強迫一個線程等待。 |
notify() | 通知一個線程繼續運行。 |
setPriority() | 設置一個線程的優先級。 |
垃圾回收是在內存中存在沒有引用的對象或超過做用域的對象時進行的。
垃圾回收的目的是識別而且丟棄應用再也不使用的對象來釋放和重用資源。
在併發編程中,咱們須要處理兩個關鍵問題:線程之間如何通訊及線程之間如何同步。通訊是指線程之間以如何來交換信息。通常線程之間的通訊機制有兩種:共享內存和消息傳遞。
Java的併發採用的是共享內存模型,Java線程之間的通訊老是隱式進行,整個通訊過程對程序員徹底透明。若是編寫多線程程序的Java程序員不理解隱式進行的線程之間通訊的工做機制,極可能會遇到各類奇怪的內存可見性問題。
下面經過示意圖來講明線程之間的通訊
不會,在下一個垃圾回調週期中,這個對象將是被可回收的。
也就是說並不會當即被垃圾收集器馬上回收,而是在下一次垃圾回收時纔會釋放其佔用的內存。
1.垃圾回收器(garbage colector)決定回收某對象時,就會運行該對象的finalize()方法; finalize是Object類的一個方法,該方法在Object類中的聲明protected void finalize() throws Throwable { } 在垃圾回收器執行時會調用被回收對象的finalize()方法,能夠覆蓋此方法來實現對其資源的回收。注意:一旦垃圾回收器準備釋放對象佔用的內存,將首先調用該對象的finalize()方法,而且下一次垃圾回收動做發生時,才真正回收對象佔用的內存空間
int a = 5; //語句1 int r = 3; //語句2 a = a + 2; //語句3 r = a*a; //語句4 複製代碼
as-if-serial語義保證單線程內程序的執行結果不被改變,happens-before關係保證正確同步的多線程程序的執行結果不被改變。
as-if-serial語義給編寫單線程程序的程序員創造了一個幻境:單線程程序是按程序的順序來執行的。happens-before關係給編寫正確同步的多線程程序的程序員創造了一個幻境:正確同步的多線程程序是按happens-before指定的順序來執行的。
as-if-serial語義和happens-before這麼作的目的,都是爲了在不改變程序執行結果的前提下,儘量地提升程序執行的並行度。
在 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中,字符串常量池具備緩存功能!
雙重校驗鎖實現對象單例(線程安全)
說明:
雙鎖機制的出現是爲了解決前面同步問題和性能問題,看下面的代碼,簡單分析下確實是解決了多線程並行進來不會出現重複new對象,並且也實現了懶加載
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;
}
複製代碼
}
另外,須要注意 uniqueInstance 採用 volatile 關鍵字修飾也是頗有必要。
可是因爲 JVM 具備指令重排的特性,執行順序有可能變成 1->3->2。指令重排在單線程環境下不會出現問題,可是在多線程環境下會致使一個線程得到尚未初始化的實例。例如,線程 T1 執行了 1 和 3,此時 T2 調用 getUniqueInstance() 後發現 uniqueInstance 不爲空,所以返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。
使用 volatile 能夠禁止 JVM 的指令重排,保證在多線程環境下也能正常運行。
Synchronized的語義底層是經過一個monitor(監視器鎖)的對象來完成,
每一個對象有一個監視器鎖(monitor)。每一個Synchronized修飾過的代碼當它的monitor被佔用時就會處於鎖定狀態而且嘗試獲取monitor的全部權 ,過程:
一、若是monitor的進入數爲0,則該線程進入monitor,而後將進入數設置爲1,該線程即爲monitor的全部者。
二、若是線程已經佔有該monitor,只是從新進入,則進入monitor的進入數加1.
三、若是其餘線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲0,再從新嘗試獲取monitor的全部權。
synchronized是能夠經過 反彙編指令 javap命令,查看相應的字節碼文件。
鎖的升級的目的:鎖升級是爲了減低了鎖帶來的性能消耗。在 Java 6 以後優化 synchronized 的實現方式,使用了偏向鎖升級爲輕量級鎖再升級到重量級鎖的方式,從而減低了鎖帶來的性能消耗。
偏向鎖,顧名思義,它會偏向於第一個訪問鎖的線程,若是在運行過程當中,同步鎖只有一個線程訪問,不存在多線程爭用的狀況,則線程是不須要觸發同步的,減小加鎖/解鎖的一些CAS操做(好比等待隊列的一些CAS操做),這種狀況下,就會給線程加一個偏向鎖。 若是在運行過程當中,遇到了其餘線程搶佔鎖,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復到標準的輕量級鎖。
輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進入同步塊的狀況下,當第二個線程加入鎖爭用的時候,輕量級鎖就會升級爲重量級鎖;
重量級鎖是synchronized ,是 Java 虛擬機中最爲基礎的鎖實現。在這種狀態下,Java 虛擬機會阻塞加鎖失敗的線程,而且在目標鎖被釋放的時候,喚醒這些線程。
(1)volatile 修飾變量
(2)synchronized 修飾修改變量的方法
(3)wait/notify
(4)while 輪詢
(1)synchronized 是悲觀鎖,屬於搶佔式,會引發其餘線程阻塞。
(2)volatile 提供多線程共享變量可見性和禁止指令重排序優化。
(3)CAS 是基於衝突檢測的樂觀鎖(非阻塞)
synchronized 是和 if、else、for、while 同樣的關鍵字,ReentrantLock 是類,這是兩者的本質區別。既然 ReentrantLock 是類,那麼它就提供了比synchronized 更多更靈活的特性,能夠被繼承、能夠有方法、能夠有各類各樣的類變量
synchronized 早期的實現比較低效,對比 ReentrantLock,大多數場景性能都相差較大,可是在 Java 6 中對 synchronized 進行了很是多的改進。
相同點:二者都是可重入鎖
二者都是可重入鎖。「可重入鎖」概念是:本身能夠再次獲取本身的內部鎖。好比一個線程得到了某個對象的鎖,此時這個對象鎖尚未釋放,當其再次想要獲取這個對象的鎖的時候仍是能夠獲取的,若是不可鎖重入的話,就會形成死鎖。同一個線程每次獲取鎖,鎖的計數器都自增1,因此要等到鎖的計數器降低爲0時才能釋放鎖。
主要區別以下:
Java中每個對象均可以做爲鎖,這是synchronized實現同步的基礎:
對於可見性,Java 提供了 volatile 關鍵字來保證可見性和禁止指令重排。 volatile 提供 happens-before 的保證,確保一個線程的修改能對其餘線程是可見的。當一個共享變量被 volatile 修飾時,它會保證修改的值會當即被更新到主內存中,當有其餘線程須要讀取時,它會去內存中讀取新值。
從實踐角度而言,volatile 的一個重要做用就是和 CAS 結合,保證了原子性,詳細的能夠參見 java.util.concurrent.atomic 包下的類,好比 AtomicInteger。
volatile 經常使用於多線程環境下的單次操做(單次讀或者單次寫)。
volatile 變量能夠確保先行關係,即寫操做會發生在後續的讀操做以前, 但它並不能保證原子性。例如用 volatile 修飾 count 變量,那麼 count++ 操做就不是原子性的。
而 AtomicInteger 類提供的 atomic 方法可讓這種操做具備原子性如getAndIncrement()方法會原子性的進行增量操做把當前值加一,其它數據類型和引用變量也能夠進行類似操做。
關鍵字volatile的主要做用是使變量在多個線程間可見,但沒法保證原子性,對於多個線程訪問同一個實例變量須要加鎖進行同步。
雖然volatile只能保證可見性不能保證原子性,但用volatile修飾long和double能夠保證其操做原子性。
因此從Oracle Java Spec裏面能夠看到:
synchronized 表示只有一個線程能夠獲取做用對象的鎖,執行代碼,阻塞其餘線程。
volatile 表示變量在 CPU 的寄存器中是不肯定的,必須從主存中讀取。保證多線程環境下變量的可見性;禁止指令重排序。
區別
volatile 是變量修飾符;synchronized 能夠修飾類、方法、變量。
volatile 僅能實現變量的修改可見性,不能保證原子性;而 synchronized 則能夠保證變量的修改可見性和原子性。
volatile 不會形成線程的阻塞;synchronized 可能會形成線程的阻塞。
volatile標記的變量不會被編譯器優化;synchronized標記的變量能夠被編譯器優化。
volatile關鍵字是線程同步的輕量級實現,因此volatile性能確定比synchronized關鍵字要好。可是volatile關鍵字只能用於變量而synchronized關鍵字能夠修飾方法以及代碼塊。synchronized關鍵字在JavaSE1.6以後進行了主要包括爲了減小得到鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各類優化以後執行效率有了顯著提高,實際開發中使用 synchronized 關鍵字的場景仍是更多一些。
不可變對象(Immutable Objects)即對象一旦被建立它的狀態(對象的數據,也即對象屬性值)就不能改變,反之即爲可變對象(Mutable Objects)。
不可變對象的類即爲不可變類(Immutable Class)。Java 平臺類庫中包含許多不可變類,如 String、基本類型的包裝類、BigInteger 和 BigDecimal 等。
只有知足以下狀態,一個對象纔是不可變的;
它的狀態不能在建立後再被修改;
全部域都是 final 類型;而且,它被正確建立(建立期間沒有發生 this 引用的逸出)。
不可變對象保證了對象的內存可見性,對不可變對象的讀取不須要進行額外的同步手段,提高了代碼執行效率。
Lock 接口比同步方法和同步塊提供了更具擴展性的鎖操做。他們容許更靈活的結構,能夠具備徹底不一樣的性質,而且能夠支持多個相關類的條件對象。
它的優點有:
(1)能夠使鎖更公平
(2)能夠使線程在等待鎖的時候響應中斷
(3)可讓線程嘗試獲取鎖,並在沒法獲取鎖的時候當即返回或者等待一段時間
(4)能夠在不一樣的範圍,以不一樣的順序獲取和釋放鎖
總體上來講 Lock 是 synchronized 的擴展版,Lock 提供了無條件的、可輪詢的(tryLock 方法)、定時的(tryLock 帶參方法)、可中斷的(lockInterruptibly)、可多條件隊列的(newCondition 方法)鎖操做。另外 Lock 的實現類基本都支持非公平鎖(默認)和公平鎖,synchronized 只支持非公平鎖,固然,在大部分狀況下,非公平鎖是高效的選擇。
悲觀鎖:老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。再好比 Java 裏面的同步原語 synchronized 關鍵字的實現也是悲觀鎖。
樂觀鎖:顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,能夠使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量,像數據庫提供的相似於 write_condition 機制,其實都是提供的樂觀鎖。在 Java中 java.util.concurrent.atomic 包下面的原子變量類就是使用了樂觀鎖的一種實現方式 CAS 實現的。
CAS 是 compare and swap 的縮寫,即咱們所說的比較交換。
cas 是一種基於鎖的操做,並且是樂觀鎖。在 java 中鎖分爲樂觀鎖和悲觀鎖。悲觀鎖是將資源鎖住,等一個以前得到鎖的線程釋放鎖以後,下一個線程才能夠訪問。而樂觀鎖採起了一種寬泛的態度,經過某種方式不加鎖來處理資源,好比經過給記錄加 version 來獲取數據,性能較悲觀鎖有很大的提升。
CAS 操做包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)。若是內存地址裏面的值和 A 的值是同樣的,那麼就將內存裏面的值更新成 B。CAS是經過無限循環來獲取數據的,若果在第一輪循環中,a 線程獲取地址裏面的值被b 線程修改了,那麼 a 線程須要自旋,到下次循環纔有可能機會執行。
java.util.concurrent.atomic 包下的類大可能是使用 CAS 操做來實現的(AtomicInteger,AtomicBoolean,AtomicLong)
一、ABA 問題:
好比說一個線程 one 從內存位置 V 中取出 A,這時候另外一個線程 two 也從內存中取出 A,而且 two 進行了一些操做變成了 B,而後 two 又將 V 位置的數據變成 A,這時候線程 one 進行 CAS 操做發現內存中仍然是 A,而後 one 操做成功。儘管線程 one 的 CAS 操做成功,但可能存在潛藏的問題。從 Java1.5 開始 JDK 的 atomic包裏提供了一個類 AtomicStampedReference 來解決 ABA 問題。
二、循環時間長開銷大:
對於資源競爭嚴重(線程衝突嚴重)的狀況,CAS 自旋的機率會比較大,從而浪費更多的 CPU 資源,效率低於 synchronized。
三、只能保證一個共享變量的原子操做:
當對一個共享變量執行操做時,咱們能夠使用循環 CAS 的方式來保證原子操做,可是對多個共享變量操做時,循環 CAS 就沒法保證操做的原子性,這個時候就能夠用鎖。
java.util.concurrent.atomic包:是原子類的小工具包,支持在單個變量上解除鎖的線程安全編程 原子變量類至關於一種泛化的 volatile 變量,可以支持原子的和有條件的讀-改-寫操做。
好比:AtomicInteger 表示一個int類型的值,並提供了 get 和 set 方法,這些 Volatile 類型的int變量在讀取和寫入上有着相同的內存語義。它還提供了一個原子的 compareAndSet 方法(若是該方法成功執行,那麼將實現與讀取/寫入一個 volatile 變量相同的內存效果),以及原子的添加、遞增和遞減等方法。AtomicInteger 表面上很是像一個擴展的 Counter 類,但在發生競爭的狀況下能提供更高的可伸縮性,由於它直接利用了硬件對併發的支持。
簡單來講就是原子類來實現CAS無鎖模式的算法
死鎖:是指兩個或兩個以上的進程(或線程)在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。
活鎖:任務或者執行者沒有被阻塞,因爲某些條件沒有知足,致使一直重複嘗試,失敗,嘗試,失敗。
活鎖和死鎖的區別在於,處於活鎖的實體是在不斷的改變狀態,這就是所謂的「活」, 而處於死鎖的實體表現爲等待;活鎖有可能自行解開,死鎖則不能。
飢餓:一個或者多個線程由於種種緣由沒法得到所須要的資源,致使一直沒法執行的狀態。
Java 中致使飢餓的緣由:
一、高優先級線程吞噬全部的低優先級線程的 CPU 時間。
二、線程被永久堵塞在一個等待進入同步塊的狀態,由於其餘線程老是能在它以前持續地對該同步塊進行訪問。
三、線程在等待一個自己也處於永久等待完成的對象(好比調用這個對象的 wait 方法),由於其餘線程老是被持續地得到喚醒。
線程池是爲忽然大量爆發的線程設計的,經過有限的幾個固定線程爲大量的操做服務,減小了建立和銷燬線程所需的時間,從而提升效率。
若是一個線程所須要執行的時間很是長的話,就不必用線程池了(不是不能做長時間操做,而是不宜。原本下降線程建立和銷燬,結果你那麼久我還很差控制還不如直接建立線程),何況咱們還不能控制線程池中線程的開始、掛起、和停止。
下降資源消耗:重用存在的線程,減小對象建立銷燬的開銷。
提升響應速度。可有效的控制最大併發線程數,提升系統資源的使用率,同時避免過多資源競爭,避免堵塞。當任務到達時,任務能夠不須要的等到線程建立就能當即執行。
提升線程的可管理性。線程是稀缺資源,若是無限制的建立,不只會消耗系統資源,還會下降系統的穩定性,使用線程池能夠進行統一的分配,調優和監控。
附加功能:提供定時執行、按期執行、單線程、併發數控制等功能。
ThreadPoolExecutor就是線程池
ThreadPoolExecutor其實也是JAVA的一個類,咱們通常經過Executors工廠類的方法,經過傳入不一樣的參數,就能夠構造出適用於不一樣應用場景下的ThreadPoolExecutor(線程池)
構造參數圖:
構造參數參數介紹:
corePoolSize 核心線程數量
maximumPoolSize 最大線程數量
keepAliveTime 線程保持時間,N個時間單位
unit 時間單位(好比秒,分)
workQueue 阻塞隊列
threadFactory 線程工廠
handler 線程池拒絕策略
複製代碼
Executors框架實現的就是線程池的功能。
Executors工廠類中提供的newCachedThreadPool、newFixedThreadPool 、newScheduledThreadPool 、newSingleThreadExecutor 等方法其實也只是ThreadPoolExecutor的構造函數參數不一樣而已。經過傳入不一樣的參數,就能夠構造出適用於不一樣應用場景下的線程池,
Executor工廠類如何建立線程池圖:
Executors 工具類的不一樣方法按照咱們的需求建立了不一樣的線程池,來知足業務的需求。
Executor 接口對象能執行咱們的線程任務。
ExecutorService 接口繼承了 Executor 接口並進行了擴展,提供了更多的方法咱們能得到任務執行的狀態而且能夠獲取任務的返回值。
使用 ThreadPoolExecutor 能夠建立自定義線程池。
特色:newCachedThreadPool建立一個可緩存線程池,若是當前線程池的長度超過了處理的須要時,它能夠靈活的回收空閒的線程,當須要增長時, 它能夠靈活的添加新的線程,而不會對池的長度做任何限制
缺點:他雖然能夠無線的新建線程,可是容易形成堆外內存溢出,由於它的最大值是在初始化的時候設置爲 Integer.MAX_VALUE,通常來講機器都沒那麼大內存給它不斷使用。固然知道可能出問題的點,就能夠去重寫一個方法限制一下這個最大值
總結:線程池爲無限大,當執行第二個任務時第一個任務已經完成,會複用執行第一個任務的線程,而不用每次新建線程。
代碼示例:
package com.lijie; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestNewCachedThreadPool { public static void main(String[] args) { // 建立無限大小線程池,由jvm自動回收 ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { final int temp = i; newCachedThreadPool.execute(new Runnable() { public void run() { try { Thread.sleep(100); } catch (Exception e) { } System.out.println(Thread.currentThread().getName() + ",i==" + temp); } }); } } } 複製代碼
特色:建立一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。定長線程池的大小最好根據系統資源進行設置。
缺點:線程數量是固定的,可是阻塞隊列是無界隊列。若是有不少請求積壓,阻塞隊列愈來愈長,容易致使OOM(超出內存空間)
總結:請求的擠壓必定要和分配的線程池大小匹配,定線程池的大小最好根據系統資源進行設置。如Runtime.getRuntime().availableProcessors()
Runtime.getRuntime().availableProcessors()方法是查看電腦CPU核心數量)
代碼示例:
package com.lijie; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestNewFixedThreadPool { public static void main(String[] args) { ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3); for (int i = 0; i < 10; i++) { final int temp = i; newFixedThreadPool.execute(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName() + ",i==" + temp); } }); } } } 複製代碼
特色:建立一個固定長度的線程池,並且支持定時的以及週期性的任務執行,相似於Timer(Timer是Java的一個定時器類)
缺點:因爲全部任務都是由同一個線程來調度,所以全部任務都是串行執行的,同一時間只能有一個任務在執行,前一個任務的延遲或異常都將會影響到以後的任務(好比:一個任務出錯,之後的任務都沒法繼續)。
代碼示例:
package com.lijie; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class TestNewScheduledThreadPool { public static void main(String[] args) { //定義線程池大小爲3 ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3); for (int i = 0; i < 10; i++) { final int temp = i; newScheduledThreadPool.schedule(new Runnable() { public void run() { System.out.println("i:" + temp); } }, 3, TimeUnit.SECONDS);//這裏表示延遲3秒執行。 } } } 複製代碼
特色:建立一個單線程化的線程池,它只會用惟一的工做線程來執行任務,若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它,他必須保證前一項任務執行完畢才能執行後一項。保證全部任務按照指定順序(FIFO, LIFO, 優先級)執行。
缺點:缺點的話,很明顯,他是單線程的,高併發業務下有點無力
總結:保證全部任務按照指定順序執行的,若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它
代碼示例:
package com.lijie; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestNewSingleThreadExecutor { public static void main(String[] args) { ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor(); for (int i = 0; i < 10; i++) { final int index = i; newSingleThreadExecutor.execute(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName() + " index:" + index); try { Thread.sleep(200); } catch (Exception e) { } } }); } } } 複製代碼
ThreadGroup 類,能夠把線程歸屬到某一個線程組中,線程組中能夠有線程對象,也能夠有線程組,組中還能夠有線程,這樣的組織結構有點相似於樹的形式。
線程組和線程池是兩個不一樣的概念,他們的做用徹底不一樣,前者是爲了方便線程的管理,後者是爲了管理線程的生命週期,複用線程,減小建立銷燬線程的開銷。
爲何不推薦使用線程組?由於使用有不少的安全隱患吧,沒有具體追究,若是須要使用,推薦使用線程池。
若是當前同時運行的線程數量達到最大線程數量而且隊列也已經被放滿了任時,ThreadPoolTaskExecutor 定義一些策略:
先看ThreadPoolExecutor(線程池)這個類的構造參數
corePoolSize 核心線程數量
maximumPoolSize 最大線程數量
keepAliveTime 線程保持時間,N個時間單位
unit 時間單位(好比秒,分)
workQueue 阻塞隊列
threadFactory 線程工廠
handler 線程池拒絕策略
複製代碼
代碼示例:
package com.lijie; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class Test001 { public static void main(String[] args) { //建立線程池 ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3)); for (int i = 1; i <= 6; i++) { TaskThred t1 = new TaskThred("任務" + i); //executor.execute(t1);是執行線程方法 executor.execute(t1); } //executor.shutdown()再也不接受新的任務,而且等待以前提交的任務都執行完再關閉,阻塞隊列中的任務不會再執行。 executor.shutdown(); } } class TaskThred implements Runnable { private String taskName; public TaskThred(String taskName) { this.taskName = taskName; } public void run() { System.out.println(Thread.currentThread().getName() + taskName); } } 複製代碼
提交一個任務到線程池中,線程池的處理流程以下:
判斷線程池裏的核心線程是否都在執行任務,若是不是(核心線程空閒或者還有核心線程沒有被建立)則建立一個新的工做線程來執行任務。若是核心線程都在執行任務,則進入下個流程。
線程池判斷工做隊列是否已滿,若是工做隊列沒有滿,則將新提交的任務存儲在這個工做隊列裏。若是工做隊列滿了,則進入下個流程。
判斷線程池裏的線程是否都處於工做狀態,若是沒有,則建立一個新的工做線程來執行任務。若是已經滿了,則交給飽和策略來處理這個任務。
CPU密集的意思是該任務須要大量的運算,而沒有阻塞,CPU一直全速運行。
CPU密集任務只有在真正的多核CPU上纔可能獲得加速(經過多線程),而在單核CPU上,不管你開幾個模擬的多線程,該任務都不可能獲得加速,由於CPU總的運算能力就那樣。
CPU密集型時,任務能夠少配置線程數,大概和機器的cpu核數至關,這樣能夠使得每一個線程都在執行任務
IO密集型時,大部分線程都阻塞,故須要多配置線程數,2*cpu核數
從如下幾個角度分析任務的特性:
任務的性質:CPU密集型任務、IO密集型任務、混合型任務。
任務的優先級:高、中、低。
任務的執行時間:長、中、短。
任務的依賴性:是否依賴其餘系統資源,如數據庫鏈接等。
能夠得出一個結論:
答:Vector、ConcurrentHashMap、HasTable
通常軟件開發中容器用的最多的就是HashMap、ArrayList,LinkedList ,等等
可是在多線程開發中就不能亂用容器,若是使用了未加鎖(非同步)的的集合,你的數據就會很是的混亂。由此在多線程開發中須要使用的容器必須是加鎖(同步)的容器。
Vector與ArrayList同樣,也是經過數組實現的,不一樣的是它支持線程的同步,即某一時刻只有一個線程可以寫Vector,避免多線程同時寫而引發的不一致性,但實現同步須要很高的花費,訪問它比訪問ArrayList慢不少
(ArrayList是最經常使用的List實現類,內部是經過數組實現的,它容許對元素進行快速隨機訪問。當從ArrayList的中間位置插入或者刪除元素時,須要對數組進行復制、移動、代價比較高。所以,它適合隨機查找和遍歷,不適合插入和刪除。ArrayList的缺點是每一個元素之間不能有間隔。
)
ConcurrentHashMap是Java5中支持高併發、高吞吐量的線程安全HashMap實現。它由Segment數組結構和HashEntry數組結構組成。Segment數組在ConcurrentHashMap裏扮演鎖的角色,HashEntry則用於存儲鍵-值對數據。一個ConcurrentHashMap裏包含一個Segment數組,Segment的結構和HashMap相似,是一種數組和鏈表結構;一個Segment裏包含一個HashEntry數組,每一個HashEntry是一個鏈表結構的元素;每一個Segment守護着一個HashEntry數組裏的元素,當對HashEntry數組的數據進行修改時,必須首先得到它對應的Segment鎖。
看不懂???很正常,我也看不懂
總結:
注意:* 號表明後面是還有內容的
此方法是幹什麼的呢,他完徹底全的能夠把List、Map、Set接口底下的集合變成線程安全的集合
Collections.synchronized * :原理是什麼,我猜的話是代理模式:Java代理模式理解
ConcurrentHashMap 把實際 map 劃分紅若干部分來實現它的可擴展性和線程安全。這種劃分是使用併發度得到的,它是 ConcurrentHashMap 類構造函數的一個可選參數,默認值爲 16,這樣在多線程狀況下就能避免爭用。
在 JDK8 後,它摒棄了 Segment(鎖段)的概念,而是啓用了一種全新的方式實現,利用 CAS 算法。同時加入了更多的輔助變量來提升併發度,具體內容仍是查看源碼吧。
何爲同步容器:能夠簡單地理解爲經過 synchronized 來實現同步的容器,若是有多個線程調用同步容器的方法,它們將會串行執行。好比 Vector,Hashtable,以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。能夠經過查看 Vector,Hashtable 等這些同步容器的實現代碼,能夠看到這些容器實現線程安全的方式就是將它們的狀態封裝起來,並在須要同步的方法上加上關鍵字 synchronized。
併發容器使用了與同步容器徹底不一樣的加鎖策略來提供更高的併發性和伸縮性,例如在 ConcurrentHashMap 中採用了一種粒度更細的加鎖機制,能夠稱爲分段鎖,在這種鎖機制下,容許任意數量的讀線程併發地訪問 map,而且執行讀操做的線程和寫操做的線程也能夠併發的訪問 map,同時容許必定數量的寫操做線程併發地修改 map,因此它能夠在併發環境下實現更高的吞吐量。
SynchronizedMap 一次鎖住整張表來保證線程安全,因此每次只能有一個線程來訪爲 map。
ConcurrentHashMap 使用分段鎖來保證在多線程下的性能。
ConcurrentHashMap 中則是一次鎖住一個桶。ConcurrentHashMap 默認將hash 表分爲 16 個桶,諸如 get,put,remove 等經常使用操做只鎖當前須要用到的桶。
這樣,原來只能一個線程進入,如今卻能同時有 16 個寫線程執行,併發性能的提高是顯而易見的。
另外 ConcurrentHashMap 使用了一種不一樣的迭代方式。在這種迭代方式中,當iterator 被建立後集合再發生改變就再也不是拋出ConcurrentModificationException,取而代之的是在改變時 new 新的數據從而不影響原有的數據,iterator 完成後再將頭指針替換爲新的數據 ,這樣 iterator線程能夠使用原來老的數據,而寫線程也能夠併發的完成改變。
CopyOnWriteArrayList 是一個併發容器。有不少人稱它是線程安全的,我認爲這句話不嚴謹,缺乏一個前提條件,那就是非複合場景下操做它是線程安全的。
CopyOnWriteArrayList(免鎖容器)的好處之一是當多個迭代器同時遍歷和修改這個列表時,不會拋出 ConcurrentModificationException。在CopyOnWriteArrayList 中,寫入將致使建立整個底層數組的副本,而源數組將保留在原地,使得複製的數組在被修改時,讀取操做能夠安全地執行。
消息隊列不少人知道:消息隊列是分佈式系統中重要的組件,是系統與系統直接的通訊
併發隊列是什麼:併發隊列多個線程以有次序共享數據的重要組件
那就有可能要說了,咱們併發集合不是也能夠實現多線程之間的數據共享嗎,其實也是有區別的:
隊列遵循「先進先出」的規則,能夠想象成排隊檢票,隊列通常用來解決大數據量採集處理和顯示的。
併發集合就是在多個線程中共享數據的
當隊列阻塞隊列爲空的時,從隊列中獲取元素的操做將會被阻塞。
或者當阻塞隊列是滿時,往隊列裏添加元素的操做會被阻塞。
或者試圖從空的阻塞隊列中獲取元素的線程將會被阻塞,直到其餘的線程往空的隊列插入新的元素。
試圖往已滿的阻塞隊列中添加新元素的線程一樣也會被阻塞,直到其餘的線程使隊列從新變得空閒起來
非堵塞隊列:
ArrayDeque, (數組雙端隊列)
ArrayDeque (非堵塞隊列)是JDK容器中的一個雙端隊列實現,內部使用數組進行元素存儲,不容許存儲null值,能夠高效的進行元素查找和尾部插入取出,是用做隊列、雙端隊列、棧的絕佳選擇,性能比LinkedList還要好。
PriorityQueue, (優先級隊列)
PriorityQueue (非堵塞隊列) 一個基於優先級的無界優先級隊列。優先級隊列的元素按照其天然順序進行排序,或者根據構造隊列時提供的 Comparator 進行排序,具體取決於所使用的構造方法。該隊列不容許使用 null 元素也不容許插入不可比較的對象
ConcurrentLinkedQueue, (基於鏈表的併發隊列)
ConcurrentLinkedQueue (非堵塞隊列): 是一個適用於高併發場景下的隊列,經過無鎖的方式,實現了高併發狀態下的高性能。ConcurrentLinkedQueue的性能要好於BlockingQueue接口,它是一個基於連接節點的無界線程安全隊列。該隊列的元素遵循先進先出的原則。該隊列不容許null元素。
堵塞隊列:
DelayQueue, (基於時間優先級的隊列,延期阻塞隊列)
DelayQueue是一個沒有邊界BlockingQueue實現,加入其中的元素必需實現Delayed接口。當生產者線程調用put之類的方法加入元素時,會觸發Delayed接口中的compareTo方法進行排序,也就是說隊列中元素的順序是按到期時間排序的,而非它們進入隊列的順序。排在隊列頭部的元素是最先到期的,越日後到期時間赿晚。
ArrayBlockingQueue, (基於數組的併發阻塞隊列)
ArrayBlockingQueue是一個有邊界的阻塞隊列,它的內部實現是一個數組。有邊界的意思是它的容量是有限的,咱們必須在其初始化的時候指定它的容量大小,容量大小一旦指定就不可改變。ArrayBlockingQueue是以先進先出的方式存儲數據
LinkedBlockingQueue, (基於鏈表的FIFO阻塞隊列)
LinkedBlockingQueue阻塞隊列大小的配置是可選的,若是咱們初始化時指定一個大小,它就是有邊界的,若是不指定,它就是無邊界的。說是無邊界,實際上是採用了默認大小爲Integer.MAX_VALUE的容量 。它的內部實現是一個鏈表。
LinkedBlockingDeque, (基於鏈表的FIFO雙端阻塞隊列)
LinkedBlockingDeque是一個由鏈表結構組成的雙向阻塞隊列,便可以從隊列的兩端插入和移除元素。雙向隊列由於多了一個操做隊列的入口,在多線程同時入隊時,也就減小了一半的競爭。
相比於其餘阻塞隊列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法,以first結尾的方法,表示插入、獲取獲移除雙端隊列的第一個元素。以last結尾的方法,表示插入、獲取獲移除雙端隊列的最後一個元素。
LinkedBlockingDeque是可選容量的,在初始化時能夠設置容量防止其過分膨脹,若是不設置,默認容量大小爲Integer.MAX_VALUE。
PriorityBlockingQueue, (帶優先級的無界阻塞隊列)
priorityBlockingQueue是一個無界隊列,它沒有限制,在內存容許的狀況下能夠無限添加元素;它又是具備優先級的隊列,是經過構造函數傳入的對象來判斷,傳入的對象必須實現comparable接口。
SynchronousQueue (併發同步阻塞隊列)
SynchronousQueue是一個內部只能包含一個元素的隊列。插入元素到隊列的線程被阻塞,直到另外一個線程從隊列中獲取了隊列中存儲的元素。一樣,若是線程嘗試獲取元素而且當前不存在任何元素,則該線程將被阻塞,直到線程將元素插入隊列。
將這個類稱爲隊列有點誇大其詞。這更像是一個點。
無論是那種列隊,是那個類,當是他們使用的方法都是差很少的
方法名 | 描述 |
---|---|
add() | 在不超出隊列長度的狀況下插入元素,能夠當即執行,成功返回true,若是隊列滿了就拋出異常。 |
offer() | 在不超出隊列長度的狀況下插入元素的時候則能夠當即在隊列的尾部插入指定元素,成功時返回true,若是此隊列已滿,則返回false。 |
put() | 插入元素的時候,若是隊列滿了就進行等待,直到隊列可用。 |
take() | 從隊列中獲取值,若是隊列中沒有值,線程會一直阻塞,直到隊列中有值,而且該方法取得了該值。 |
poll(long timeout, TimeUnit unit) | 在給定的時間裏,從隊列中獲取值,若是沒有取到會拋出異常。 |
remainingCapacity() | 獲取隊列中剩餘的空間。 |
remove(Object o) | 從隊列中移除指定的值。 |
contains(Object o) | 判斷隊列中是否擁有該值。 |
drainTo(Collection c) | 將隊列中值,所有移除,併發設置到給定的集合中。 |
CountDownLatch
CountDownLatch 類位於java.util.concurrent包下,利用它能夠實現相似計數器的功能。好比有一個任務A,它要等待其餘3個任務執行完畢以後才能執行,此時就能夠利用CountDownLatch來實現這種功能了。
CyclicBarrier (迴環柵欄) CyclicBarrier它的做用就是會讓全部線程都等待完成後纔會繼續下一步行動。
CyclicBarrier初始化時規定一個數目,而後計算調用了CyclicBarrier.await()進入等待的線程數。當線程數達到了這個數目時,全部進入等待狀態的線程被喚醒並繼續。
CyclicBarrier初始時還可帶一個Runnable的參數, 此Runnable任務在CyclicBarrier的數目達到後,全部其它線程被喚醒前被執行。
Semaphore (信號量) Semaphore 是 synchronized 的增強版,做用是控制線程的併發數量(容許自定義多少線程同時訪問)。就這一點而言,單純的synchronized 關鍵字是實現不了的。
Semaphore是一種基於計數的信號量。它能夠設定一個閾值,基於此,多個線程競爭獲取許可信號,作本身的申請後歸還,超過閾值後,線程申請許可信號將會被阻塞。Semaphore能夠用來構建一些對象池,資源池之類的,好比數據庫鏈接池,咱們也能夠建立計數爲1的Semaphore,將其做爲一種相似互斥鎖的機制,這也叫二元信號量,表示兩種互斥狀態。它的用法以下: