從Java多線程基礎(chǔ)到Java內(nèi)存模型;從synchronized關(guān)鍵字到Java并發(fā)工具包JUC。
我們不生產(chǎn)知識(shí),我們只做知識(shí)的搬運(yùn)工!
基石——Java多線程的基本概念
- 線程與進(jìn)程的不同點(diǎn):
起源不同。先有進(jìn)程后有線程。由于處理器的速度遠(yuǎn)遠(yuǎn)大于外設(shè),為了提升程序的執(zhí)行效率,才誕生了線程。
概念不同。進(jìn)程是具有獨(dú)立功能的程序運(yùn)行起來的一個(gè)活動(dòng),是操作系統(tǒng)分配資源和調(diào)度的一個(gè)獨(dú)立單位;線程是CPU的基本調(diào)度單位。
-
內(nèi)存共享方式不同。不同進(jìn)程之間的內(nèi)存數(shù)據(jù)一般是不共享的(除非采用進(jìn)程間通信IPC);同一個(gè)進(jìn)程中的不同線程往往會(huì)共享:
- 進(jìn)程的代碼段
- 進(jìn)程的公有數(shù)據(jù)
- 進(jìn)程打開的文件描述符
- 信號(hào)的處理器
- 進(jìn)程的當(dāng)前目錄
- 進(jìn)程用戶ID和進(jìn)程組ID
-
擁有的資源不同。線程獨(dú)有的內(nèi)容包括:
- 線程ID
- 寄存組的值
- 線程的棧
- 錯(cuò)誤的返回碼
- 線程的信號(hào)屏蔽嗎
進(jìn)程和線程的數(shù)量不同。
-
線程和進(jìn)程創(chuàng)建的開銷不同。
- 線程的創(chuàng)建、終止時(shí)間比進(jìn)程短
- 同一進(jìn)程內(nèi)的線程切換時(shí)間比進(jìn)程短
- 同一進(jìn)程的各個(gè)線程之間共享內(nèi)存和文件資源,可以不通過內(nèi)核進(jìn)行通信。
Java中沒有協(xié)程的概念,協(xié)程往往指程序中的多個(gè)線程可以映射到操作系統(tǒng)級(jí)別的幾個(gè)線程,Java中的線程數(shù)目與操作系統(tǒng)中的線程數(shù)目是一一對應(yīng)的。
-
創(chuàng)建線程只有一種方式就是構(gòu)造Thread類。實(shí)現(xiàn)線程的執(zhí)行單元有兩種方式:
- 實(shí)現(xiàn)Runnable接口的run方法,并把Runnable實(shí)例傳遞給Thread類
- 重寫Thread的run方法
從3個(gè)角度可以得到實(shí)現(xiàn)Runnable接口來完成多線程編程優(yōu)于繼承Thread類的完成多線程編程:
- 代碼架構(gòu)角度,不易于實(shí)現(xiàn)業(yè)務(wù)邏輯的解耦。run方法中作為所執(zhí)行的任務(wù)應(yīng)該與Thread類解耦。
- 新建線程的損耗,不易于實(shí)現(xiàn)線程池的優(yōu)化
- Java不支持多繼承,不易于實(shí)現(xiàn)擴(kuò)展
-
同步與異步:
同步是指被調(diào)用者不會(huì)主動(dòng)告訴被調(diào)用者結(jié)果,需要調(diào)用者不斷的去查看調(diào)用結(jié)果 異步是指被調(diào)用者會(huì)主動(dòng)告訴被調(diào)用者結(jié)果,不需要調(diào)用者不斷的去查看調(diào)用結(jié)果
-
線程的正確啟動(dòng)與停止:
線程的正確啟動(dòng)方法是start()而不是run()。start()方法的本質(zhì)是請求JVM來運(yùn)行當(dāng)前的線程,至于當(dāng)前線程何時(shí)真正運(yùn)行是由線程調(diào)度器決定的。start()方法的內(nèi)部實(shí)現(xiàn)主要是包括三個(gè)步驟:一是檢查要啟動(dòng)的新線程的狀態(tài),二是將該線程加入線程組,三是調(diào)用線程的native方法start0()。
-
線程的正確停止方法是:使用interrupt()來通知,而不是強(qiáng)制結(jié)束指定線程。
public class JavaDemo implements Runnable { @Override public void run() { while (true) { if (Thread.currentThread().isInterrupted()) { break; } System.out.println("go"); interrupt(); } } public void interrupt() { try { Thread.sleep(5000); } catch (InterruptedException e) { System.out.println("出現(xiàn)異常,記錄日志并且停止"); Thread.currentThread().interrupt(); e.printStackTrace(); } } public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new JavaDemoSi()); thread.start(); thread.sleep(1000); thread.interrupt(); } }
-
線程的六種生命周期:
- NEW:已創(chuàng)建但未調(diào)用start()的線程狀態(tài)
- RUNNABLE:可運(yùn)行的。在調(diào)用了start()方法之后,線程便由NEW狀態(tài)轉(zhuǎn)換成Runnable狀態(tài)
- BLOCKED:當(dāng)前線程競爭synchronized修飾的代碼塊,并且當(dāng)前鎖已經(jīng)被其他線程持有,此當(dāng)前線程就由Runnable狀態(tài)進(jìn)入Blocked狀態(tài)。
- WAITING:線程調(diào)用了Object.wait()、Thread.join()、LockSupport.park()方法就會(huì)由Rubbable狀態(tài)進(jìn)入Waiting狀態(tài);當(dāng)線程調(diào)用了Object.notify()、Object.notifyAll()、LockSupport.unpark()之后,線程由Waiting狀態(tài)可能短時(shí)間進(jìn)入Blocked狀態(tài)然后進(jìn)入Runnable狀態(tài)或者直接進(jìn)入Runnable狀態(tài)或者因?yàn)榘l(fā)生異常直接進(jìn)入Terminated狀態(tài)。
- TIMEDWAITING:線程調(diào)用了Object.wait(time)、Thread.join(time)、LockSupport.parkNanos(time)、LockSupport.partUntil(time)、Tread.sleep(time)方法就會(huì)由Rubbable狀態(tài)進(jìn)入TimedWaiting狀態(tài);當(dāng)線程調(diào)用了Object.notify()、Object.notifyAll()、LockSupport.unpark()之后,線程由TimedWaiting狀態(tài)可能短時(shí)間進(jìn)入Blocked狀態(tài)然后進(jìn)入Runnable狀態(tài)或者直接進(jìn)入Runnable狀態(tài)或者因?yàn)榘l(fā)生異常直接進(jìn)入Terminated狀態(tài)。
- TERMINATED:線程的正常結(jié)束或者出現(xiàn)異常線程意外終止。
-
常見方法:
-
wait()方法:在同步代碼塊
synchronized(object){}
中的線程A已經(jīng)獲取到鎖時(shí),其他線程不能獲取當(dāng)前鎖從而會(huì)阻塞進(jìn)入BLOCKED狀態(tài);當(dāng)線程A執(zhí)行object.wait()
時(shí),線程A持有的鎖會(huì)釋放,此時(shí)其他線程獲取到object鎖;其他線程代碼中執(zhí)行了object.notify()
方法時(shí),線程A會(huì)重新獲取到object鎖,可以進(jìn)行線程的調(diào)用。注意notify()、notifyAll()方法必須要在wait()方法之后調(diào)用,若順序改變則程序會(huì)進(jìn)入永久等待。
-
park()方法:在線程中調(diào)用LockSupport.park()進(jìn)行線程的掛起,在其他線程中調(diào)用LockSupport(已掛起的線程對象)進(jìn)行線程的喚醒。park()和unpark()是基于許可證的概念存在的,只要調(diào)用了unpark()在一次park()中就可以實(shí)現(xiàn)線程的一次喚醒(這里的一次是指線程只要調(diào)用了park()就要調(diào)用unpark(),不能實(shí)現(xiàn)調(diào)用多次unpark()后面的park()多次調(diào)用就可以直接實(shí)現(xiàn)線程的喚醒),park()和unpark()沒有調(diào)用順序的限制。
注意park()、unpark()方法不是基于監(jiān)視器鎖實(shí)現(xiàn)的,與wait()方法不同,park()只會(huì)掛起當(dāng)前線程并不會(huì)對鎖進(jìn)行釋放。在線程中使用synchronized關(guān)鍵字的內(nèi)部調(diào)用了park()容易導(dǎo)致死鎖。
-
-
幾個(gè)常見特性: 原子性、內(nèi)存可見性和重排序。
-
原子性:
原子(Atomic)操作指相應(yīng)的操作是單一不可分割的操作。
在多線程中,非原子操作可能會(huì)受到其他線程的干擾,使用關(guān)鍵字synchronized
可以實(shí)現(xiàn)操作的原子性。synchronized
的本質(zhì)是通過該關(guān)鍵字所包括的臨界區(qū)的排他性保證在任何一個(gè)時(shí)刻只有一個(gè)線程能夠執(zhí)行臨界區(qū)中的代碼,從而使的臨界區(qū)中的代碼實(shí)現(xiàn)了原子操作。 -
內(nèi)存可見性:
CPU在執(zhí)行代碼時(shí),為了減少變量訪問的時(shí)間消耗會(huì)將代碼中訪問的變量值緩存到CPU的緩存區(qū)中,代碼在訪問某個(gè)變量時(shí),相應(yīng)的值會(huì)從緩存中讀取而不是在主內(nèi)存中讀取;同樣的,代碼對被緩存過的變量的值的修改可能僅僅是寫入緩存區(qū)而不是寫回到內(nèi)存中。這樣就導(dǎo)致一個(gè)線程對相同變量的修改無法同步到其他線程從而導(dǎo)致了內(nèi)存的不可見性。
-
可以使用`synchronized`或`volatile`來解決內(nèi)存的不可見性問題。兩者又有點(diǎn)不同。`synchronized`仍然是
通過將代碼在臨界區(qū)中對變量進(jìn)行改變,然后使得對稍后執(zhí)行該臨界區(qū)中代碼的線程是可見的。`volatile`不同之處在于,一個(gè)線程對一個(gè)采用volatile關(guān)鍵字修飾的變量的值的更改對于其他使用該變量的線程總是可見的,它是通過將變量的更改直接同步到主內(nèi)存中,同時(shí)其他線程緩存中的對應(yīng)變量失效,從而實(shí)現(xiàn)了變量的每次讀取都是從主內(nèi)存中讀取。
3. 指令重排序:
在CPU多級(jí)緩存場景下,當(dāng)CPU寫緩存時(shí)發(fā)現(xiàn)緩存區(qū)正在被其他CPU占用,為了提高CPU處理性能,可能將后面的讀緩存命令優(yōu)先執(zhí)行。運(yùn)行時(shí)指令重排要遵循as-if-serial語義,即不管怎么重排序,單線程程序的執(zhí)行結(jié)果不能改變并且編譯器和處理器不會(huì)對存在的數(shù)據(jù)依賴關(guān)系的操作做重排序。
指令的重排序?qū)е麓a的執(zhí)行順序改變,這經(jīng)常會(huì)導(dǎo)致一系列的問題,比如在對象的創(chuàng)建過程中,指令的重排序使得我們得到了一個(gè)已經(jīng)分配好的內(nèi)存而對象的初始化并未完成,從而導(dǎo)致空指針的異常。`volatile`關(guān)鍵字可以禁止指令的重排序從而解決這類問題。
總之,`synchronized`可以保證在多線程中操作的原子性和內(nèi)存可見性,但是會(huì)引起上下文切換;而`volatile`關(guān)鍵字僅能保證內(nèi)存可見性,但是可以禁止指令的重排序,同時(shí)不會(huì)引起上下文切換。
Java內(nèi)存模型
首先介紹Java內(nèi)存模型的特性
- Java所有變量都存儲(chǔ)在主內(nèi)存中
- 每個(gè)線程都有自己獨(dú)立的工作內(nèi)存,里面保存該線程的使用到的變量副本(該副本就是主內(nèi)存中該變量的一份拷貝)
- 線程對共享變量的操作都是在自己的內(nèi)存中完成,而不是在主內(nèi)存中完成。
- 線程對共享變量的操作默認(rèn)情況下在其他線程中不可見,可以通過將本地線程的變量同步到共享內(nèi)存中之后將共享變量同步到其他的線程
下面介紹內(nèi)存模型圖
基于JMM,Java提供了多種除了鎖之外的同步機(jī)制來保證線程安全性。Java提供的TreadLocal以及前面概念中提到的volatile就是兩種策略。
下面先介紹volatile關(guān)鍵字,ThreadLocal在下文并發(fā)工具類中介紹
volatile:
volatile最主要的就是實(shí)現(xiàn)了共享變量的內(nèi)存可見性,其實(shí)現(xiàn)的原理是:volatile變量的值每次都會(huì)從高速緩存或者主內(nèi)存中讀取,對于volatile變量,每一個(gè)線程不再會(huì)有一個(gè)副本變量,所有線程對volatile變量的操作都是對同一個(gè)變量的操作。
volatile變量的開銷包括讀變量和寫變量兩個(gè)方面。volatile變量的讀、寫操作都不會(huì)導(dǎo)致上下文的切換,因此volatile的開銷比鎖小。但是volatile變量的值不會(huì)暫存在寄存器中,因此讀取volatile變量的成本要比讀取普通變量的成本更高。
volatile常被稱為"輕量級(jí)鎖"。
JUC工具包
Java中的各種鎖(互斥同步保證線程并發(fā)安全)
互斥同步是指多個(gè)線程對共享資源是獨(dú)占的,當(dāng)一個(gè)線程獲得共享資源時(shí),其他所有的線程都將處于等待獲取狀態(tài),不同線程之間是敵對的。
根據(jù)不同的分類標(biāo)準(zhǔn)存在多種鎖類型,對于一種確定的鎖可以同時(shí)屬于下面的多種類型:
- 多個(gè)線程能否共享一把鎖:可以實(shí)現(xiàn)共享的稱為共享鎖;不可以實(shí)現(xiàn)共享的稱為排他鎖。共享鎖又稱為讀鎖,每一個(gè)線程都可以獲取到讀鎖,之后可以查看數(shù)據(jù)但是無法修改和刪除數(shù)據(jù)。
**`synchronized屬于排他鎖**。
**`ReentrantReadWriteLock`同時(shí)具備共享鎖和排他鎖,其中讀鎖是共享鎖,寫鎖是排他鎖**。
-
線程要不要鎖住同步資源:鎖住同步資源的稱為悲觀鎖(又稱為互斥同步鎖);不鎖住同步資源的稱為樂觀鎖(又稱為非互斥同步鎖)。
優(yōu)缺點(diǎn):
悲觀鎖的性能相對較低:當(dāng)發(fā)生長時(shí)間鎖等不到釋放或者直接出現(xiàn)死鎖時(shí),等待鎖的線程永遠(yuǎn)得不到執(zhí)行;同時(shí)悲觀鎖存在阻塞和喚醒這兩種狀態(tài)都是會(huì)消耗資源的;此外使用了悲觀鎖,線程的優(yōu)先級(jí)屬性設(shè)置將會(huì)失效。
相對于悲觀鎖而言,樂觀鎖性能較高,但是如果獲取鎖的線程數(shù)量過多,那么樂觀鎖會(huì)產(chǎn)生大量的無用自旋等消耗,性能也會(huì)因此而下降
悲觀鎖適用于并發(fā)寫入多或者臨界區(qū)持鎖時(shí)間比較長的情形
樂觀鎖適用于并發(fā)寫入少、并發(fā)讀取多的情形
synchronized
和Lock
都屬于悲觀鎖。
原子類和并發(fā)容器工具都采用了樂觀鎖的思想樂觀鎖基于CAS算法實(shí)現(xiàn)。
CAS算法:
CAS(Compare and Swap),即比較并交換。
CAS有3個(gè)操作數(shù),內(nèi)存值V,舊的預(yù)期值A(chǔ),要修改的新值B。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),將內(nèi)存值V修改為B,否則什么都不做。
當(dāng)然CAS除了有上面提到的樂觀鎖的缺點(diǎn)外,CAS還容易出現(xiàn)ABA問題。即可能存在其他線程修改過預(yù)期值執(zhí)行過其他操作之后又寫會(huì)預(yù)期值,這樣反而不會(huì)被察覺。解決ABA問題的一個(gè)好方式就是增加版本號(hào)version字段,通過每次更新操作都修改version字段以及每次更新之前都檢查version字段來保證線程執(zhí)行的安全性
-
同一個(gè)線程是否可以重復(fù)獲取同一把鎖:可以重復(fù)獲取的稱為可重入鎖;不可以重復(fù)獲取的稱為不可重入鎖
可重入鎖可以有效的避免死鎖,當(dāng)一個(gè)線程獲取到鎖時(shí),可以繼續(xù)獲取該鎖,而不會(huì)出現(xiàn)當(dāng)前線程等待當(dāng)前線程釋放鎖這一情況的發(fā)生。
**`synchronized`和`ReentrantLock`都屬于可重入鎖**。
- 多個(gè)線程競爭時(shí)根據(jù)是否排隊(duì):通過排隊(duì)來獲取的稱為公平鎖;先嘗試插隊(duì),插隊(duì)失敗再排隊(duì)的稱為非公平鎖
**ReentrantLock既可以實(shí)現(xiàn)公平鎖又可以實(shí)現(xiàn)非公平鎖,通過指定ReentrantLock構(gòu)造方法中fair的參數(shù)值來實(shí)現(xiàn)公平與非公平的效果**
- 是否可以響應(yīng)中斷:可響應(yīng)中斷的稱為可中斷鎖;不可響應(yīng)中斷的稱為非可中斷鎖
- 等鎖的過程不同:等鎖的過程中如果不停的嘗試而非阻塞稱為自旋鎖;等鎖的過程中如果阻塞等待稱為非自旋鎖
同步關(guān)鍵字synchronized解析
- 作用: synchronized能夠保證在同一時(shí)刻最多只有一個(gè)線程執(zhí)行該代碼,以達(dá)到保證并發(fā)安全的效果。synchronized是最基本的同步互斥的手段。
- 用法:
- 對象鎖.
-
方法鎖,即默認(rèn)鎖對象為this當(dāng)前實(shí)例對象。同一個(gè)實(shí)例對象下的實(shí)例方法共享同一把鎖,不同的實(shí)例對象的實(shí)例方法鎖不同。
class SynchronizedDemo1 { public synchronized void index1() { //do something... } public synchronized void index2() { //do something... } } class SynchronizedDemo2 { public synchronized void index1() { //do something... } public synchronized void index2() { //do something... } }
以上代碼中,SynchronizedDemo1實(shí)例對象demo1的方法index1和index2共享同一把鎖,SynchronizedDemo2實(shí)例對象demo1的方法index1和index2共享同一把鎖,多個(gè)線程訪問同一個(gè)對象下的synchronized修飾的方法時(shí)是互斥同步的,訪問不同對象的synchronized修飾的方法互不干擾
-
同步代碼塊鎖,即自己指定鎖對象。
class SynchronizedDemo1 { public synchronized void index() { synchronized(this){ //do something... } } }
以上代碼中,只有獲得了當(dāng)前對象鎖的線程才能執(zhí)行同步代碼塊中的代碼,同步代碼塊的出現(xiàn)是為了減小方法鎖的粒度,提高性能
-
- 類鎖.
synchronized修飾靜態(tài)的方法。多個(gè)線程訪問同一類的不同實(shí)例對象的靜態(tài)方法時(shí),由于靜態(tài)方法是類級(jí)別的而不是對象級(jí)別的,所以即便是不同對象,方法之間的訪問也是互斥同步的
-
指定的鎖為Class對象。
class SynchronizedDemo1 { public synchronized void index() { synchronized(SynchronizedDemo1.class){ //do something... } } }
以上代碼中,只有獲得了當(dāng)前類的Class對象鎖的線程才能執(zhí)行同步代碼塊中的代碼,同步代碼塊的出現(xiàn)是為了減小方法鎖的粒度,提高性能
- 對象鎖.
- synchronized是可重入的、不可中斷的。
其他常用的鎖
在jdk1.5之后,并發(fā)包中新增了Lock接口(以及相關(guān)實(shí)現(xiàn)類)用來實(shí)現(xiàn)鎖功能,Lock接口提供了與synchronized關(guān)鍵字類似的同步功能,但需要在使用時(shí)手動(dòng)獲取鎖和釋放鎖,也正因?yàn)槿绱?基于Lock接口實(shí)現(xiàn)的鎖具備更好的可操作性。
Lock接口中的方法:
-
lock()
: 此方法用于獲取鎖,如果鎖已被其他線程獲取,那么線程進(jìn)入等待狀態(tài),與synchronized不同的是:當(dāng)獲取到鎖并且在執(zhí)行任務(wù)中發(fā)生了異常,synchronized會(huì)自動(dòng)釋放鎖而lock()方法獲取到的鎖不會(huì)自動(dòng)釋放。使用lock()必須在try...finally...中手動(dòng)釋放。 -
tryLock()
:由于lock()不能被中斷,所以一旦陷入死鎖,lock()就會(huì)陷入永久等待中;tryLock()方法是一種更為優(yōu)雅的使用方式,tryLock()用來嘗試獲取鎖,如果當(dāng)前鎖沒有被其他線程占用,那么獲取鎖成功并立刻返回true,否則立刻返回false表示獲取鎖失敗。
ReetrantLock
ReetrantLock 是基于Lock接口最通用的實(shí)現(xiàn),在上文中在介紹鎖分類時(shí)也已經(jīng)多次提到過ReentrantLock,因此也了解過其許多特性,由于ReentrantLock非常值得深入探究,在此也不在一文中過多闡述,在此給出一個(gè)鏈接進(jìn)行參看:
[深入ReentrantLock]https://blog.csdn.net/fuyuwei2015/article/details/83719444#commentBox
ReadWriteLock
讀寫鎖是一種改進(jìn)型的排它鎖。讀寫鎖允許多個(gè)線程可以同時(shí)讀取(只讀)共享變量。讀寫鎖是分為讀鎖和寫鎖兩種角色的,讀線程在訪問共享變量的時(shí)候必須持有相應(yīng)讀寫鎖的讀鎖,而且讀鎖是共享的、多個(gè)線程可以共同持有的;寫鎖是排他的,以一個(gè)線程在持有寫鎖的時(shí)候,其他線程無法獲得相應(yīng)鎖的寫鎖或讀鎖。總之,讀寫鎖通過讀寫鎖的分離從而提高了并發(fā)性。
ReadWriteLock接口是對讀寫鎖的抽象,其默認(rèn)的實(shí)現(xiàn)類是ReentrantReadWriteLock。ReadWriteLock定義了兩個(gè)方法readLock()和writeLock(),分別用于返回相應(yīng)讀寫鎖實(shí)例的讀鎖和寫鎖。這兩個(gè)方法的返回值類型都是Lock。
關(guān)于ReentrantReadWriteLock實(shí)現(xiàn),這里給出一個(gè)鏈接參看:
[ReentrantReadWriteLock詳解]https://www.cnblogs.com/xiaoxi/p/9140541.html
讀寫鎖主要用于讀線程持有鎖的時(shí)間比較長的情景下。
原子類(非互斥同步保證線程并發(fā)安全)
非互斥同步指的是不同的線程不對共享資源進(jìn)行獨(dú)占,不同的線程都可以訪問共享資源,只不過當(dāng)多個(gè)線程同時(shí)對一個(gè)共享變量進(jìn)行修改或刪除時(shí),只有一個(gè)線程的操作能成功其他的都會(huì)失敗。
Java中的原子類分為6種,分別有:
- AtomicInteger、AtomicLong、AtomicBoolean
- AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- AtomicReference、AtomicStampedReference、AtomicMarkableReference
- AtomicIntegerFieldupdater、AtomicLongFieldupdater、AtomicReferenceFieldupdater
- LongAdder、DoubleAdder
- LongAccumulator、DoubleAccumulator
直接使用Java中的原子類進(jìn)行操作即可在并發(fā)情況下保證變量的線程安全,原子類相較于鎖粒度更小,性能更高。原子類也是基于CAS算法來實(shí)現(xiàn)的,其都包括compareAndSet()方法即為先比較當(dāng)前值是否等于預(yù)期的值然后進(jìn)行數(shù)據(jù)的修改從而保證了變量的原子性。
需要注意的是累加器LongAdder是Java8開始引入的,相較于AtomicLong,由于LongAdder在每個(gè)線程操作的過程中并不會(huì)實(shí)時(shí)的進(jìn)行數(shù)據(jù)同步(由于上文所提到的JMM,AtomicLong會(huì)實(shí)時(shí)的進(jìn)行多個(gè)線程之間的數(shù)據(jù)通信),所以效率更高。而LongAccumulator擴(kuò)展了LongAdder使得原子變量不僅只能進(jìn)行累加操作也可以進(jìn)行其他指定公式的計(jì)算
并發(fā)容器(結(jié)合互斥同步與非互斥同步保證線程并發(fā)安全)
Java中并發(fā)容器由來已久,當(dāng)然并發(fā)容器的種類也非常多。但是其中一部分諸如Vector、Hashtable、Collections.synchronizedList()、Collections.synchronizedMap()等底層是基于synchronized來實(shí)現(xiàn)的并發(fā)同步,效率會(huì)比較低,所以即使這些容器可以保證線程安全也不再使用。與之相替代的就是下面的幾種并發(fā)容器類,由于并發(fā)容器在實(shí)現(xiàn)上也有許多可學(xué)習(xí)之處,所以這里不再在一文中介紹而是會(huì)初步引入,并放上我認(rèn)為比較不錯(cuò)的幾個(gè)博客鏈接,這樣可以更好的深入理解。
- ConcurrentHashMap——線程安全的Map
多個(gè)線程往HashMap中同時(shí)進(jìn)行put(),如果有幾個(gè)線程計(jì)算出的鍵的散列值相同,那么就會(huì)出現(xiàn)key丟失的情況,同樣的,如果此時(shí)HashMap容量不夠,多個(gè)線層同時(shí)擴(kuò)容,也會(huì)只保留一個(gè)擴(kuò)容后的Map,從而導(dǎo)致數(shù)據(jù)丟失。而ConcurrentHashMap則在底層數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)上與HashMap又有所區(qū)別,避免了HashMap會(huì)產(chǎn)生的問題。
關(guān)于ConcurrentHashMap的數(shù)據(jù)結(jié)構(gòu)可以參看:
[ConcurrentHashMap的數(shù)據(jù)結(jié)構(gòu)]https://blog.csdn.net/weixin_44460333/article/details/86770169#commentBox
- CopyOnWriteArrayList——線程安全的List、CopyOnWriteArraySet——線程安全的Set
為了保證List的線程安全,又要避免因使用Vector、Collections.synchronized等而產(chǎn)生的鎖粒度過大而造成效率降低的問題,CopyOnWriteArrayList、CopyOnWriteArraySet應(yīng)運(yùn)而生,CopyOnWriteArrayList和CopyOnWriteArraySet在實(shí)現(xiàn)原理上大體一致,這里只給出CopyOnWriteArrayList的介紹.
關(guān)于CopyOnWriteArrayList的數(shù)據(jù)結(jié)構(gòu)可以參看:
[CopyOnWriteArrayList的數(shù)據(jù)結(jié)構(gòu)]https://www.cnblogs.com/chengxiao/p/6881974.html
- BlockingQueue——阻塞隊(duì)列作為數(shù)據(jù)共享的通道
BlockingQueue很好的解決了多線程中,如何高效安全“傳輸”數(shù)據(jù)的問題。通過這些高效并且線程安全的隊(duì)列類,為我們快速搭建高質(zhì)量的多線程程序帶來極大的便利。在Java中,BlockingQueue是一個(gè)接口,它的實(shí)現(xiàn)類有ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等,這些阻塞隊(duì)列的實(shí)現(xiàn)在Java并發(fā)編程中經(jīng)常要用到,其中最常用的就是ArrayBlockingQueue和LinkedBlockingQueue
關(guān)于BlockingQueue可以參看:
[BlockingQueue相關(guān)]https://segmentfault.com/a/1190000016296278
關(guān)于ArrayBlockingQueue可以參看:
[ArrayBlockingQueue相關(guān)]https://blog.csdn.net/u014799292/article/details/90167096
關(guān)于LinkedBlockingQueue可以參看:
[LinkedBlockingQueue相關(guān)]https://blog.csdn.net/tonywu1992/article/details/83419448
- ConcurrentLinkedQueue——非阻塞并發(fā)隊(duì)列(使用鏈表實(shí)現(xiàn)的線程安全的LinkedList)
ConcurrentLinkedQueue是一個(gè)基于鏈接節(jié)點(diǎn)的非阻塞無界線程安全隊(duì)列。
關(guān)于ConcurrentLinkedQueue的數(shù)據(jù)結(jié)構(gòu)可以參看:
[ConcurrentLinkedQueue的數(shù)據(jù)結(jié)構(gòu)]https://blog.csdn.net/qq_38293564/article/details/80798310#commentBox
ThreadLocal(無同步保證線程并發(fā)安全)
ThreadLocal,即線程變量,是一個(gè)以ThreadLocal對象為鍵、任意對象為值的存儲(chǔ)結(jié)構(gòu)。這個(gè)結(jié)構(gòu)被附帶在線程上,也就是說一個(gè)線程可以根據(jù)一個(gè)ThreadLocal對象查詢到綁定在這個(gè)線程上的一個(gè)值。
ThreadLocal采用的是上述策略中的第一種設(shè)計(jì)思想——采用線程的特有對象.采用線程的特有對象,我們可以保障每一個(gè)線程都具有各自的實(shí)例,同一個(gè)對象不會(huì)被多個(gè)線程共享,ThreadLocal是維護(hù)線程封閉性的一種更加規(guī)范的方法,這個(gè)類能使線程中的某個(gè)值與保存值的對象關(guān)聯(lián)起來,從而保證了線程特有對象的固有線程安全性。
ThreadLocal<T>類相當(dāng)于線程訪問其線程特有對象的代理,即各個(gè)線程通過這個(gè)對象可以創(chuàng)建并訪問各自的線程特有對象,泛型T指定了相應(yīng)線程持有對象的類型。一個(gè)線程可以使用不同的ThreadLocal實(shí)例來創(chuàng)建并訪問其不同的線程持有對象。多個(gè)線程使用同一個(gè)ThreadLocal<T>實(shí)例所訪問到的對象時(shí)類型T的不同實(shí)例。代理的關(guān)系圖如下:
ThreadLocal提供了get和set等訪問接口或方法,這些方法為每一個(gè)使用該變量的線程都存有一份獨(dú)立的副本,因此get總是能返回由當(dāng)前執(zhí)行線程在調(diào)用set時(shí)設(shè)置的最新值。其主要使用的方法如下:
public T get(): 獲取與當(dāng)前線程中ThreadLocal實(shí)例關(guān)聯(lián)的線程特有對象。
public void set(T value):重新關(guān)聯(lián)當(dāng)前線程中ThreadLocal實(shí)例所對應(yīng)的線程特有對象。
protected T initValue():如果沒有調(diào)用set(),在初始化threadlocal對象的時(shí)候,該方法的返回值就是當(dāng)前線程中與ThreadLocal實(shí)例關(guān)聯(lián)的線程特有對象。
public void remove():刪除當(dāng)前線程中ThreadLocal和線程特有對象的關(guān)系。
那么ThreadLocal底層是如何實(shí)現(xiàn)Thread持有自己的線程特有對象的?查看set()方法的源代碼:
可以看到,當(dāng)我們調(diào)用threadlocal的set方法來保存當(dāng)前線程的特有對象時(shí),threadlocal會(huì)取出當(dāng)前線程關(guān)聯(lián)的threadlocalmap對象,然后調(diào)用ThreadLocalMap對象的set方法來進(jìn)行當(dāng)前給定值的保存。
每一個(gè)Thread都會(huì)維護(hù)一個(gè)ThreadLocalMap對象,ThreadLocalMap是一個(gè)類似Map的數(shù)據(jù)結(jié)構(gòu),但是它沒有實(shí)現(xiàn)任何Map的相關(guān)接口。ThreadLocalMap是一個(gè)Entry數(shù)組,每一個(gè)Entry對象都是一個(gè)"key-value"結(jié)構(gòu),而且Entry對象的key永遠(yuǎn)都是ThreadLocal對象。當(dāng)我們調(diào)用ThreadLocal的set方法時(shí),實(shí)際上就是以當(dāng)前ThreadLocal對象本身作為key,放入到了ThreadLocalMap中。
可能發(fā)生內(nèi)存泄漏:
通過查看Entry結(jié)構(gòu)可知,Entry屬于WeakReference類型,因此Entry不會(huì)阻止被引用的ThreadLocal實(shí)例被垃圾回收。當(dāng)一個(gè)ThreadLocal實(shí)例沒有對其可達(dá)的強(qiáng)引用時(shí),這個(gè)實(shí)例就可以被垃圾回收,即其所在的Entry的key會(huì)被置為null,但是如果創(chuàng)建ThreadLocal的線程一直持續(xù)運(yùn)行,那么這個(gè)Entry對象中的value就有可能一直得不到回收,從而發(fā)生內(nèi)存泄露。
解決內(nèi)存泄漏的最有效方法就是,在使用完ThreadLocal之后,要注意調(diào)用threadlocal的remove()方法釋放內(nèi)存。
Future
傳統(tǒng)的Runnable來實(shí)現(xiàn)任務(wù)有兩大缺陷,一個(gè)是Runnable中的run()沒有返回值,另一個(gè)是Runnable中的run()無法拋出異常。為了解決上述問題,Callable應(yīng)運(yùn)而生,而Future是為了更好的操作Callable實(shí)現(xiàn)業(yè)務(wù)邏輯而誕生的。
我們可以用Future.get來獲取Callable接口返回的執(zhí)行結(jié)果,還可以通過Future.isDone()來判斷任務(wù)是否已經(jīng)執(zhí)行完了以及取消這個(gè)任務(wù),限時(shí)獲取任務(wù)的結(jié)果等等。
線程池
線程池提供了復(fù)用線程的能力,如果不使用線程池,那么每個(gè)任務(wù)都會(huì)新開一個(gè)線程,上文基石中也已經(jīng)提到Java代碼中的線程數(shù)量對應(yīng)于操作系統(tǒng)的線程數(shù)量,這樣對于線程的創(chuàng)建和銷毀都會(huì)帶來很大的開銷,此外系統(tǒng)可創(chuàng)建的線程數(shù)量是有限的,使用線程池可以有效避免OOM等異常。
線程池的創(chuàng)建一般借助ThreadPoolExecutor
這個(gè)類,其中有5個(gè)參數(shù)比較關(guān)鍵,以下說明:
-
corePoolSize、maxPoolSize、workQueue
:線程池中默認(rèn)存在的線程數(shù)量是corePoolSize,當(dāng)任務(wù)多于corePoolSize時(shí),新來的任務(wù)會(huì)首先存儲(chǔ)在任務(wù)存儲(chǔ)隊(duì)列workQueue
中,當(dāng)任務(wù)數(shù)量超出了任務(wù)存儲(chǔ)隊(duì)列的最大長度,線程池才會(huì)擴(kuò)大其中的線程數(shù)量直到maxPoolSize
,當(dāng)任務(wù)數(shù)量超出maxPoolSize
,線程池執(zhí)行定義的拒絕策略handler
。-
workQueue的三種常用類型:
1.SyncbronousQueue:最簡單的直接交換隊(duì)列,這隊(duì)列長度為0不能存儲(chǔ)新的任務(wù),適用與任務(wù)不太多的場景,此外由于隊(duì)列不能存儲(chǔ)任務(wù)線程池很容易創(chuàng)建新的線程,所以maxPoolSize要設(shè)置的大一點(diǎn),但是如果設(shè)置的maxPoolSize過大,線程創(chuàng)建的過多而不能得到調(diào)度從而產(chǎn)生堆積,就會(huì)引發(fā)OOM。
Executors.newCachedThreadPool()、Executors.newScheduledThreadPool()
即為這種類型,其中Executors.newCachedThreadPool()的maxPoolSize這里設(shè)置的為Integer.MAX_VALUE,corePoolSize默認(rèn)為0,keepAliveTime為60s2.LinkedBlockingQueue:無解隊(duì)列,這個(gè)相較于第一種隊(duì)列屬于另一個(gè)極端,可以存儲(chǔ)任意數(shù)量的任務(wù)。此類隊(duì)列可以存儲(chǔ)較多數(shù)量的任務(wù)并且此時(shí)maxPoolSize會(huì)失效,但是此時(shí)也要注意任務(wù)過多時(shí)會(huì)產(chǎn)生堆積出現(xiàn)OOM。
Executors.newFixedThreadPool()、Executors.newSingleThreadExecutor()
即為這種類型3.ArrayBlockingQueue:有界隊(duì)列,可以設(shè)置隊(duì)列長度,此時(shí)maxPoolSize有效
-
keepAliveTime
:如果線程池當(dāng)前的線程數(shù)量多余corePoolSize
,那么當(dāng)多余線程的空閑時(shí)間超過keepAliveTime
時(shí),它們將被回收。ThreadFactory
:線程池中新創(chuàng)建的線程是由ThreadFactory
創(chuàng)建的,默認(rèn)使用Executors.defaultThreadFactory()
。
線程池應(yīng)該手動(dòng)創(chuàng)建,其中:
當(dāng)任務(wù)屬于CPU密集型時(shí),線程池中的線程數(shù)量應(yīng)該設(shè)置為CPU核心數(shù)的1-2倍;當(dāng)任務(wù)屬于資源密集型時(shí),線程池中的線程數(shù)量一般設(shè)置為cpu核心數(shù)的很多倍,計(jì)算方法一般為num=CPU核心數(shù)*(1+平均等待時(shí)間/平均工作時(shí)間)
線程池停止:
shutdown()
:調(diào)用此方法后,線程池并不會(huì)立刻停止而是拒絕接受新的任務(wù)并等待線程池中已在執(zhí)行的線程任務(wù)和隊(duì)列中的任務(wù)執(zhí)行完畢
shutdownNow()
:調(diào)用此方法后,線程池通過調(diào)用terminated()方法來終止正在執(zhí)行的線程同時(shí)將隊(duì)列中未被調(diào)度的任務(wù)以集合的形式返回。
后記
到此為止,本文要梳理的Java并發(fā)相關(guān)也告一段落,之所以如此說是因?yàn)镴ava并發(fā)相關(guān)確實(shí)是值得深入探究的一個(gè)領(lǐng)域,本文的定位是基于Java來梳理并發(fā)相關(guān)的那些事兒,盡可能通過一篇文章來歸納出Java并發(fā)中應(yīng)該掌握的知識(shí)點(diǎn)。
本文仍然有很多不足之處,比如文中沒有介紹Java的并發(fā)工具類諸如CountdownLatch、Semaphore等,而關(guān)于ReentrantLock這種重要的鎖的實(shí)現(xiàn)原理AQS本文也沒有介紹,希望在之后的文章中能對本文略過的點(diǎn)進(jìn)行深入的歸納總結(jié)。