Java多線程即時面試中進行被問及到的高階知識點,也是衡量一名Java程序員是否資深的關鍵標準之一。今天這篇文章做者將對Java多線程進行一次全面的總結,但願可以對各位朋友進一步理解Java多線程起到幫助!java
若是對什麼是線程、什麼是進程仍存有疑惑,請先 Google 之,由於這兩個概念不在本文的範圍以內。程序員
用多線程只有一個目的,那就是更好的利用 CPU 的資源,由於全部的多線程代碼均可以用單線程來實現。說這個話其實只有一半對,由於反應「多角色」的程序代碼,最起碼每一個角色要給他一個線程吧,不然連實際場景都沒法模擬,固然也無法說能用單線程來實現:好比最多見的「生產者,消費者模型」。面試
不少人都對其中的一些概念不夠明確,如同步、併發等等,讓咱們先創建一個數據字典,以避免產生誤會。算法
多線程:指的是這個程序(一個進程)運行時產生了不止一個線程緩存
並行與併發:安全
並行:多個 CPU 實例或者多臺機器同時執行一段處理邏輯,是真正的同時。bash
併發:經過 CPU 調度算法,讓用戶看上去同時執行,實際上從 CPU 操做層面不是真正的同時。併發每每在場景中有公用的資源,那麼針對這個公用的資源每每產生瓶頸,咱們會用 TPS 或者 QPS 來反應這個系統的處理能力。session
併發與並行多線程
void transferMoney(User from, User to, float amount){
to.setMoney(to.getBalance() + amount);
from.setMoney(from.getBalance() - amount);
}
複製代碼
好了,讓咱們開始吧。我準備分紅幾部分來總結涉及到多線程的內容:併發
先來兩張圖:
線程狀態
線程狀態轉換
各類狀態一目瞭然,值得一提的是 "Blocked" 和 "Waiting" 這兩個狀態的區別:
線程在 Running 的過程當中可能會遇到阻塞 (Blocked) 狀況 對 Running 狀態的線程加同步鎖 (Synchronized) 使其進入 (lock blocked pool),同步鎖被釋放進入可運行狀 (Runnable)。從 jdk 源碼註釋來看,blocked 指的是對 monitor 的等待(能夠參考下文的圖)即該線程位於等待區。
線程在 Running 的過程當中可能會遇到等待(Waiting)狀況 線程能夠主動調用 object.wait 或者 sleep,或者 join(join內部調用的是 sleep ,因此可當作 sleep 的一種)進入。從 jdk 源碼註釋來看,Waiting 是等待另外一個線程完成某一個操做,如 join 等待另外一個完成執行,object.wait() 等待object.notify() 方法執行。
Waiting 狀態和 Blocked 狀態有點費解,我我的的理解是:Blocked 其實也是一種 wait ,等待的是 monitor ,可是和Waiting 狀態不同,舉個例子,有三個線程進入了同步塊,其中兩個調用了 object.wait(),進入了 Waiting 狀態,這時第三個調用了 object.notifyAll() ,這時候前兩個線程就一個轉移到了 Runnable,一個轉移到了 Blocked。
從下文的 monitor 結構圖來區別:每一個 Monitor 在某個時刻,只能被一個線程擁有,該線程就是 「Active Thread」,而其它線程都是 「Waiting Thread」,分別在兩個隊列 「 Entry Set」 和 「Wait Set」 裏面等候。在 「Entry Set」 中等待的線程狀態 Blocked,從 jstack 的dump 中來看是 「Waiting for monitor entry」,而在 「Wait Set」 中等待的線程狀態是 Waiting,表如今 jstack 的 dump 中是 「in Object.wait()」。
此外,在 runnable 狀態的線程是處於被調度的線程,此時的調度順序是不必定的。Thread 類中的 yield 方法可讓一個 running 狀態的線程轉入 runnable。
synchronized, wait, notify 是任何對象都具備的同步工具。讓咱們先來了解他們
他們是應用於同步問題的人工線程調度工具。講其本質,首先就要明確 monitor 的概念,Java 中的每一個對象都有一個監視器,來監測併發代碼的重入。在非多線程編碼時該監視器不發揮做用,反之若是在 synchronized 範圍內,監視器發揮做用。
wait/notify 必須存在於 synchronized 塊中。而且,這三個關鍵字針對的是同一個監視器(某對象的監視器)。這意味着 wait以後,其餘線程能夠進入同步塊執行。
當某代碼並不持有監視器的使用權時(如圖中5的狀態,即脫離同步塊)去 wait 或 notify,會拋出java.lang.IllegalMonitorStateException。
也包括在 synchronized 塊中去調用另外一個對象的 wait/notify,由於不一樣對象的監視器不一樣,一樣會拋出此異常。
再講用法:
synchronized 單獨使用:
代碼塊:以下,在多線程環境下,synchronized 塊中的方法獲取了 lock 實例的 monitor,若是實例相同,那麼只有一個線程能執行該塊內容
public class Thread1 implements Runnable {
Object lock;
public void run() {
synchronized(lock){
..do something
}
}
}
複製代碼
public class Thread1 implements Runnable {
public synchronized void run() {
..do something
}
}
複製代碼
/**
* 生產者生產出來的產品交給店員
*/
public synchronized void produce()
{
if(this.product >= MAX_PRODUCT)
{
try
{
wait();
System.out.println("產品已滿,請稍候再生產");
}
catch(InterruptedException e)
{
e.printStackTrace () ;
}
return;
}
this.product++;
System.out.println("生產者生產第" + this.product + "個產品.");
notifyAll(); //通知等待區的消費者能夠取出產品了
}
/**
* 消費者從店員取產品
*/
public synchronized void consume()
{
if(this.product <= MIN_PRODUCT)
{
try
{
wait();
System.out.println("缺貨,稍候再取");
}
catch (InterruptedException e)
{
e.printStackTrace();
}
return;
}
System.out.println("消費者取走了第" + this.product + "個產品.");
this.product--;
notifyAll(); //通知等待去的生產者能夠生產產品了
}
複製代碼
volatile
多線程的內存模型:main memory(主存)、working memory(線程棧),在處理數據時,線程會把值從主存 load 到本地棧,完成操做後再 save 回去 (volatile 關鍵詞的做用:每次針對該變量的操做都激發一次 load and save) 。
針對多線程使用的變量若是不是 volatile 或者 final 修飾的,頗有可能產生不可預知的結果(另外一個線程修改了這個值,可是以後在某線程看到的是修改以前的值)。其實道理上講同一實例的同一屬性自己只有一個副本。可是多線程是會緩存值的,本質上,volatile 就是不去緩存,直接取值。在線程安全的狀況下加 volatile 會犧牲性能。
基本線程類指的是 Thread 類,Runnable 接口,Callable 接口
Thread 類實現了 Runnable 接口,啓動一個線程的方法:
MyThread my = new MyThread();
my.start();
複製代碼
Thread類相關方法
//當前線程可轉讓 cpu 控制權,讓別的就緒狀態線程運行(切換)
public static Thread.yield()
//暫停一段時間
public static Thread.sleep()
//在一個線程中調用 other.join(),將等待other執行完後才繼續本線程。    
public join()
//後兩個函數皆能夠被打斷
public interrupte()
複製代碼
關於中斷:它並不像 stop 方法那樣會中斷一個正在運行的線程。線程會不時地檢測中斷標識位,以判斷線程是否應該被中斷(中斷標識值是否爲 true )。終端只會影響到 wait 狀態、sleep 狀態和 join 狀態。被打斷的線程會拋出 InterruptedException。 Thread.interrupted() 檢查當前線程是否發生中斷,返回boolean
synchronized 在獲鎖的過程當中是不能被中斷的。
中斷是一個狀態!interrupt()方法只是將這個狀態置爲 true 而已。因此說正常運行的程序不去檢測狀態,就不會終止,而 wait 等阻塞方法會去檢查並拋出異常。若是在正常運行的程序中添加while(!Thread.interrupted()) ,則一樣能夠在中斷後離開代碼體
Thread類最佳實踐:
寫的時候最好要設置線程名稱 Thread.name,並設置線程組 ThreadGroup,目的是方便管理。在出現問題的時候,打印線程棧 (jstack -pid) 一眼就能夠看出是哪一個線程出的問題,這個線程是幹什麼的。
如何獲取線程中的異常
Runnable
與 Thread 相似
Callable
future 模式:併發模式的一種,能夠有兩種形式,即無阻塞和阻塞,分別是 isDone 和 get。其中 Future 對象用來存放該線程的返回值以及狀態
ExecutorService e = Executors.newFixedThreadPool(3);
//submit 方法有多重參數版本,及支持 callable 也可以支持runnable 接口類型.
Future future = e.submit(new myCallable());
future.isDone() //return true,false 無阻塞
future.get() // return 返回值,阻塞直到該線程運行結束
複製代碼
以上都屬於內功心法,接下來是實際項目中經常使用到的工具了,Java1.5 提供了一個很是高效實用的多線程包: java.util.concurrent, 提供了大量高級工具,能夠幫助開發者編寫高效、易維護、結構清晰的 Java 多線程程序。
1.ThreadLocal類
用處:保存線程的獨立變量。對一個線程類(繼承自 Thread ) 當使用 ThreadLocal 維護變量時,ThreadLocal 爲每一個使用該變量的線程提供獨立的變量副本,因此每個線程均可以獨立地改變本身的副本,而不會影響其它線程所對應的副本。經常使用於用戶登陸控制,如記錄 session 信息。
實現:每一個Thread 都持有一個 TreadLocalMap 類型的變量(該類是一個輕量級的 Map,功能與 map 同樣,區別是桶裏放的是 entry 而不是 entry 的鏈表。功能仍是一個 map 。)以自己爲 key,以目標爲 value。 主要方法是 get() 和 set(T a),set 以後在 map 裏維護一個threadLocal -> a,get 時將 a 返回。ThreadLocal 是一個特殊的容器。
2.原子類(AtomicInteger、AtomicBoolean……)
若是使用 atomic wrapper class 如 atomicInteger,或者使用本身保證原子的操做,則等同於 synchronized
//返回值爲 boolean
AtomicInteger.compareAndSet(int expect,int update)
複製代碼
該方法可用於實現樂觀鎖,考慮文中最初提到的以下場景:a 給 b 付款10元,a 扣了 10 元,b 要加 10 元。此時 c 給 b 2 元,可是 b的加十元代碼約爲:
if(b.value.compareAndSet(old, value)){
return ;
}else{
//try again
// if that fails, rollback and log
}
複製代碼
AtomicReference
對於 AtomicReference 來說,也許對象會出現,屬性丟失的狀況,即 oldObject == current,可是 oldObject.getPropertyA != current.getPropertyA。 這時候,AtomicStampedReference 就派上用場了。這也是一個很經常使用的思路,即加上版本號
3.Lock類
lock: 在 java.util.concurrent 包內。共有三個實現:
ReentrantLock
ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock
主要目的是和 synchronized 同樣, 二者都是爲了解決同步問題,處理資源爭端而產生的技術。功能相似但有一些區別。
區別以下:
lock 更靈活,能夠自由定義多把鎖的枷鎖解鎖順(synchronized 要按照先加的後解順序)
提供多種加鎖方案,lock 阻塞式, trylock 無阻塞式, lockInterruptily 可打斷式, 還有 trylock 的帶超時時間版本
本質上和監視器鎖(即 synchronized 是同樣的)
能力越大,責任越大,必須控制好加鎖和解鎖,不然會致使災難。
和 Condition 類的結合。
性能更高,對好比下圖:
ReentrantLock
可重入的意義在於持有鎖的線程能夠繼續持有,而且要釋放對等的次數後才真正釋放該鎖。
使用方法是:
1.先 new 一個實例
static ReentrantLock r=new ReentrantLock();
複製代碼
2.加鎖
r.lock()或 r.lockInterruptibly();
複製代碼
此處也是個不一樣,後者可被打斷。當 a 線程 lock 後,b 線程阻塞,此時若是是 lockInterruptibly,那麼在調用 b.interrupt() 以後,b 線程退出阻塞,並放棄對資源的爭搶,進入 catch 塊。(若是使用後者,必須 throw interruptable exception 或 catch)
3.釋放鎖
r.unlock()
複製代碼
必須作!何爲必須作呢,要放在 finally 裏面。以防止異常跳出了正常流程,致使災難。這裏補充一個小知識點,finally 是能夠信任的:通過測試,哪怕是發生了 OutofMemoryError ,finally 塊中的語句執行也可以獲得保證。
ReentrantReadWriteLock
可重入讀寫鎖(讀寫鎖的一個實現)
ReentrantReadWriteLock lock = new ReentrantReadWriteLock()
ReadLock r = lock.readLock(); 
WriteLock w = lock.writeLock();
複製代碼
二者都有 lock,unlock 方法。寫寫,寫讀互斥;讀讀不互斥。能夠實現併發讀的高效線程安全代碼
4.容器類
這裏就討論比較經常使用的兩個:
BlockingQueue
ConcurrentHashMap
BlockingQueue
阻塞隊列。該類是 java.util.concurrent 包下的重要類,經過對 Queue 的學習能夠得知,這個 queue 是單向隊列,能夠在隊列頭添加元素和在隊尾刪除或取出元素。相似於一個管道,特別適用於先進先出策略的一些應用場景。普通的 queue 接口主要實現有 PriorityQueue(優先隊列),有興趣能夠研究
BlockingQueue 在隊列的基礎上添加了多線程協做的功能:
除了傳統的 queue 功能(表格左邊的兩列)以外,還提供了阻塞接口 put 和 take,帶超時功能的阻塞接口 offer 和 poll。put 會在隊列滿的時候阻塞,直到有空間時被喚醒;take 在隊 列空的時候阻塞,直到有東西拿的時候才被喚醒。用於生產者-消費者模型尤爲好用,堪稱神器。
常見的阻塞隊列有:
ArrayListBlockingQueue
LinkedListBlockingQueue
DelayQueue
SynchronousQueue
ConcurrentHashMap
高效的線程安全哈希 map。請對比 hashTable , concurrentHashMap, HashMap
5.管理類
管理類的概念比較泛,用於管理線程,自己不是多線程的,但提供了一些機制來利用上述的工具作一些封裝。
瞭解到的值得一提的管理類:ThreadPoolExecutor 和 JMX框架下的系統級管理類 ThreadMXBean
ThreadPoolExecutor
若是不瞭解這個類,應該瞭解前面提到的 ExecutorService,開一個本身的線程池很是方便
ExecutorService e = Executors.newCachedThreadPool();
ExecutorService e =Executors.newSingleThreadExecutor();
ExecutorService e = Executors.newFixedThreadPool(3);
// 第一種是可變大小線程池,按照任務數來分配線程,
// 第二種是單線程池,至關於 FixedThreadPool(1)
// 第三種是固定大小線程池。
// 而後運行
e.execute(new MyRunnableImpl());
複製代碼
該類內部是經過 ThreadPoolExecutor 實現的,掌握該類有助於理解線程池的管理,本質上,他們都是 ThreadPoolExecutor 類的各類實現版本。請參見 javadoc:
翻譯一下:
corePoolSize: 池內線程初始值與最小值,就算是空閒狀態,也會保持該數量線程。
maximumPoolSize: 線程最大值,線程的增加始終不會超過該值。
keepAliveTime:當池內線程數高於 corePoolSize 時,通過多少時間多餘的空閒線程纔會被回收。回收前處於 wait 狀態
unit: 時間單位,可使用 TimeUnit 的實例,如 TimeUnit.MILLISECONDS workQueue: 待入任務(Runnable)的等待場所,該參數主要影響調度策略,如公平與否,是否產生餓死 (starving)
threadFactory: 線程工廠類,有默認實現,若是有自定義的須要則須要本身實現 ThreadFactory 接口並做爲參數傳入。
請注意:該類十分經常使用,做者80%的多線程問題靠他。