Android性能優(yōu)化之Java線程機(jī)制與線程調(diào)度原理詳解
本文轉(zhuǎn)載自微信公眾號(hào)「Android開發(fā)編程」,作者Android開發(fā)編程。轉(zhuǎn)載本文請聯(lián)系A(chǔ)ndroid開發(fā)編程公眾號(hào)。
前言小計(jì)
在平時(shí)工作中如若使用不當(dāng)會(huì)出現(xiàn)數(shù)據(jù)錯(cuò)亂、執(zhí)行效率低(還不如單線程去運(yùn)行)或者死鎖程序掛掉等等問題,所以掌握了解多線程至關(guān)重要;
線程有很多優(yōu)勢:
1、提高多處理器的利用效率;
2、簡化業(yè)務(wù)功能設(shè)計(jì);
3、實(shí)現(xiàn)異步處理;
多線程的風(fēng)險(xiǎn):
1、共享數(shù)據(jù)的線程安全性;
2、多線程執(zhí)行時(shí)的活躍性問題;
3、多線程的所帶來的性能損失問題;
多線程相對于其他知識(shí)點(diǎn)來講,有一定的學(xué)習(xí)門檻,并且了解起來比較費(fèi)勁;
線程的優(yōu)勢我們很清楚,線程的風(fēng)險(xiǎn)我們也都知道,但是要做好風(fēng)險(xiǎn)控制就沒有那么簡單了;
本文從基礎(chǔ)概念開始到最后的并發(fā)模型由淺入深,講解下線程方面的知識(shí);
一、什么是線程?
1、線程簡介
- 線程是進(jìn)程中可獨(dú)立執(zhí)行的最小單位,也是 CPU 資源分配的基本單位;
- 進(jìn)程是程序向操作系統(tǒng)申請資源的基本條件,一個(gè)進(jìn)程可以包含多個(gè)線程,同一個(gè)進(jìn)程中的線程可以共享進(jìn)程中的資源,如內(nèi)存空間和文件句柄;
- 操作系統(tǒng)會(huì)把資源分配給進(jìn)程,但是 CPU 資源比較特殊,它是分配給線程的,這里說的 CPU 資源也就是 CPU 時(shí)間片;
- 進(jìn)程與線程的關(guān)系,就像是飯店與員工的關(guān)系,飯店為顧客提供服務(wù),而提供服務(wù)的具體方式是通過一個(gè)個(gè)員工實(shí)現(xiàn)的;
- 線程的作用是執(zhí)行特定任務(wù),這個(gè)任務(wù)可以是下載文件、加載圖片、繪制界面等;
- 下面我們就來看看線程的四個(gè)屬性、六個(gè)方法以及六種狀態(tài);
2、線程的四個(gè)屬性
線程有編號(hào)、名字、類別以及優(yōu)先級(jí)四個(gè)屬性,除此之外,線程的部分屬性還具有繼承性,下面我們就來看看線程的四個(gè)屬性的作用和線程的繼承性;
①編號(hào)
線程的編號(hào)(id)用于標(biāo)識(shí)不同的線程,每條線程擁有不同的編號(hào);
注意事項(xiàng):不能作為唯一標(biāo)識(shí),某個(gè)編號(hào)的線程運(yùn)行結(jié)束后,該編號(hào)可能被后續(xù)創(chuàng)建的線程使用,因此編號(hào)不適合用作唯一標(biāo)識(shí),編號(hào)是只讀屬性,不能修改;
②名字
- 每個(gè)線程都有自己的名字(name),名字的默認(rèn)值是 Thread-線程編號(hào),比如 Thread-0 ;
- 除了默認(rèn)值,我們也可以給線程設(shè)置名字,以我們自己的方式去區(qū)分每一條線程;
- 作用:給線程設(shè)置名字可以讓我們在某條線程出現(xiàn)問題時(shí),用該線程的名字快速定位出問題的地方
③類別
- 線程的類別(daemon)分為守護(hù)線程和用戶線程,我們可以通過 setDaemon(true) 把線程設(shè)置為守護(hù)線程;
- 當(dāng) JVM 要退出時(shí),它會(huì)考慮是否所有的用戶線程都已經(jīng)執(zhí)行完畢,是的話則退出;
- 而對于守護(hù)線程,JVM 在退出時(shí)不會(huì)考慮它是否執(zhí)行完成;
- 作用:守護(hù)線程通常用于執(zhí)行不重要的任務(wù),比如監(jiān)控其他線程的運(yùn)行情況,GC 線程就是一個(gè)守護(hù)線程;
- 注意事項(xiàng):setDaemon() 要在線程啟動(dòng)前設(shè)置,否則 JVM 會(huì)拋出非法線程狀態(tài)異常(IllegalThreadStateException);
④優(yōu)先級(jí)
作用:線程的優(yōu)先級(jí)(Priority)用于表示應(yīng)用希望優(yōu)先運(yùn)行哪個(gè)線程,線程調(diào)度器會(huì)根據(jù)這個(gè)值來決定優(yōu)先運(yùn)行哪個(gè)線程;
⑤取值范圍
Java 中線程優(yōu)先級(jí)的取值范圍為 1~10,默認(rèn)值是 5,Thread 中定義了下面三個(gè)優(yōu)先級(jí)常量;
- 最低優(yōu)先級(jí):MIN_PRIORITY = 1;
- 默認(rèn)優(yōu)先級(jí):NORM_PRIORITY = 5;
- 最高優(yōu)先級(jí):MAX_PRIORITY = 10;
注意事項(xiàng):不保證,線程調(diào)度器把線程的優(yōu)先級(jí)當(dāng)作一個(gè)參考值,不一定會(huì)按我們設(shè)定的優(yōu)先級(jí)順序執(zhí)行線程;
⑥線程饑餓
優(yōu)先級(jí)使用不當(dāng)會(huì)導(dǎo)致某些線程永遠(yuǎn)無法執(zhí)行,也就是線程饑餓的情況;
⑦繼承性
線程的繼承性指的是線程的類別和優(yōu)先級(jí)屬性是會(huì)被繼承的,線程的這兩個(gè)屬性的初始值由開啟該線程的線程決定;
假如優(yōu)先級(jí)為 5 的守護(hù)線程 A 開啟了線程 B,那么線程 B 也是一個(gè)守護(hù)線程,而且優(yōu)先級(jí)也是 5 ;
這時(shí)我們就把線程 A 叫做線程 B 的父線程,把線程 B 叫做線程 A 的子線程;
3、線程的六個(gè)重要方法
線程的常用方法有六個(gè),它們分別是三個(gè)非靜態(tài)方法 start()、run()、join() 和三個(gè)靜態(tài)方法 currentThread()、yield()、sleep() ;
下面我們就來看下這六個(gè)方法都有哪些作用和注意事項(xiàng)
①start()
- 作用:start() 方法的作用是啟動(dòng)線程;
- 注意事項(xiàng):該方法只能調(diào)用一次,再次調(diào)用不僅無法讓線程再次執(zhí)行,還會(huì)拋出非法線程狀態(tài)異常;
② run()
- 作用:run() 方法中放的是任務(wù)的具體邏輯,該方法由 JVM 調(diào)用,一般情況下開發(fā)者不需要直接調(diào)用該方法;
- 注意事項(xiàng):如果你調(diào)用了 run() 方法,加上 JVM 也調(diào)用了一次,那這個(gè)方法就會(huì)執(zhí)行兩次
③ join()
- 作用:join() 方法用于等待其他線程執(zhí)行結(jié)束;如果線程 A 調(diào)用了線程 B 的 join() 方法,那線程 A 會(huì)進(jìn)入等待狀態(tài),直到線程 B 運(yùn)行結(jié)束;
- 注意事項(xiàng):join() 方法導(dǎo)致的等待狀態(tài)是可以被中斷的,所以調(diào)用這個(gè)方法需要捕獲中斷異常
④Thread.currentThread()
- 作用:currentThread() 方法是一個(gè)靜態(tài)方法,用于獲取執(zhí)行當(dāng)前方法的線程;
- 我們可以在任意方法中調(diào)用 Thread.currentThread() 獲取當(dāng)前線程,并設(shè)置它的名字和優(yōu)先級(jí)等屬性;
⑤Thread.yield()
- 作用:yield() 方法是一個(gè)靜態(tài)方法,用于使當(dāng)前線程放棄對處理器的占用,相當(dāng)于是降低線程優(yōu)先級(jí);
- 調(diào)用該方法就像是是對線程調(diào)度器說:“如果其他線程要處理器資源,那就給它們,否則我繼續(xù)用”;
- 注意事項(xiàng):該方法不一定會(huì)讓線程進(jìn)入暫停狀態(tài);
⑥ Thread.sleep(ms)
作用:sleep(ms) 方法是一個(gè)靜態(tài)方法,用于使當(dāng)前線程在指定時(shí)間內(nèi)休眠(暫停)。
4、線程的六種狀態(tài)
①線程的生命周期
線程的生命周期不僅可以由開發(fā)者觸發(fā),還會(huì)受到其他線程的影響,下面是線程各個(gè)狀態(tài)之間的轉(zhuǎn)換示意圖;
我們可以通過 Thread.getState() 獲取線程的狀態(tài),該方法返回的是一個(gè)枚舉類 Thread.State;
線程的狀態(tài)有新建、可運(yùn)行、阻塞、等待、限時(shí)等待和終止 6 種,下面我們就來看看這 6 種狀態(tài)之間的轉(zhuǎn)換過程;
新建狀態(tài):當(dāng)一個(gè)線程創(chuàng)建后未啟動(dòng)時(shí),它就處于新建(NEW)狀態(tài);
②可運(yùn)行狀態(tài):當(dāng)我們調(diào)用線程的 start() 方法后,線程就進(jìn)入了可運(yùn)行(RUNNABLE)狀態(tài),可運(yùn)行狀態(tài)又分為預(yù)備(READY)和運(yùn)行(RUNNING)狀態(tài);
③預(yù)備狀態(tài):處于預(yù)備狀態(tài)的線程可被線程調(diào)度器調(diào)度,調(diào)度后線程的狀態(tài)會(huì)從預(yù)備轉(zhuǎn)換為運(yùn)行狀態(tài),處于預(yù)備狀態(tài)的線程也叫活躍線程,當(dāng)線程的 yield() 方法被調(diào)用后,線程的狀態(tài)可能由運(yùn)行狀態(tài)變?yōu)轭A(yù)備狀態(tài)
④運(yùn)行狀態(tài):運(yùn)行狀態(tài)表示線程正在運(yùn)行,也就是處理器正在執(zhí)行線程的 run() 方法;
⑤阻塞狀態(tài):當(dāng)下面幾種情況發(fā)生時(shí),線程就處于阻塞(BLOCKED)狀態(tài),發(fā)起阻塞式 I/O 操作、申請其他線程持有的鎖、進(jìn)入一個(gè) synchronized 方法或代碼塊失敗;
⑥等待狀態(tài):一個(gè)線程執(zhí)行特定方法后,會(huì)等待其他線程執(zhí)行執(zhí)行完畢,此時(shí)線程進(jìn)入了等待(WAITING)狀態(tài);
⑦等待狀態(tài):下面的幾個(gè)方法可以讓線程進(jìn)入等待狀態(tài);
- Object.wait()
- LockSupport.park()
- Thread.join()
可運(yùn)行狀態(tài):下面的幾個(gè)方法可以讓線程從等待狀態(tài)轉(zhuǎn)變?yōu)榭蛇\(yùn)行狀態(tài),而這種轉(zhuǎn)變又叫喚醒;
- Object.notify()
- Object.notifyAll()
- LockSupport.unpark()
⑧限時(shí)等待狀態(tài)
- 限時(shí)等待狀態(tài) (TIMED_WAITING)與等待狀態(tài)的區(qū)別就是,限時(shí)等待是等待一段時(shí)間,時(shí)間到了之后就會(huì)轉(zhuǎn)換為可運(yùn)行狀態(tài);
- 下面的幾個(gè)方法可以讓線程進(jìn)入限時(shí)等待狀態(tài),下面的方法中的 ms、ns、time 參數(shù)分別代表毫秒、納秒以及絕對時(shí)間;
- Thread.sleep(ms);
- Thread.join(ms);
- Object.wait(ms);
- LockSupport.parkNonos(ns);
- LockSupport.parkUntil(time);
⑨ 終止?fàn)顟B(tài)
當(dāng)線程的任務(wù)執(zhí)行完畢或者任務(wù)執(zhí)行遇到異常時(shí),線程就處于終止(TERMINATED)狀態(tài);
二、線程調(diào)度的原理
線程調(diào)度原理相關(guān)的對 Java 內(nèi)存模型、高速緩存、Java 線程調(diào)度機(jī)制進(jìn)行一個(gè)簡單介紹;
1、Java 的內(nèi)存模型
- Java 內(nèi)存模型規(guī)定了所有變量都存儲(chǔ)在主內(nèi)存中,每條線程都有自己的工作內(nèi)存;
- JVM 把內(nèi)存劃分成了好幾塊,其中方法區(qū)和堆內(nèi)存區(qū)域是線程共享的;
- java內(nèi)存模型詳解
2、高速緩存
①高速緩存簡介
現(xiàn)代處理器的處理能力要遠(yuǎn)勝于主內(nèi)存(DRAM)的訪問速率,主內(nèi)存執(zhí)行一次內(nèi)存讀/寫操作需要的時(shí)間,如果給處理器使用,處理器可以執(zhí)行上百條指令;
為了彌補(bǔ)處理器與主內(nèi)存之間的差距,硬件設(shè)計(jì)者在主內(nèi)存與處理器之間加入了高速緩存(Cache);
處理器執(zhí)行內(nèi)存讀寫操作時(shí),不是直接與主內(nèi)存打交道,而是通過高速緩存進(jìn)行的;
高速緩存相當(dāng)于是一個(gè)由硬件實(shí)現(xiàn)的容量極小的散列表,這個(gè)散列表的 key 是一個(gè)對象的內(nèi)存地址,value 可以是內(nèi)存數(shù)據(jù)的副本,也可以是準(zhǔn)備寫入內(nèi)存的數(shù)據(jù);
②高速緩存內(nèi)部結(jié)構(gòu)
從內(nèi)部結(jié)構(gòu)來看,高速緩存相當(dāng)于是一個(gè)鏈?zhǔn)缴⒘斜?Chained Hash Table),它包含若干個(gè)桶,每個(gè)桶包含若干個(gè)緩存條目(Cache Entry);
③緩存條目結(jié)構(gòu)
緩存條目可進(jìn)一步劃分為 Tag、Data Block 和 Flag 三個(gè)部分;
Tag:包含了與緩存行中數(shù)據(jù)對應(yīng)的內(nèi)存地址的部分信息(內(nèi)存地址的高位部分比特)
Data: Block 也叫緩存行(Cache Line),是高速緩存與主內(nèi)存之間數(shù)據(jù)交換的最小單元,可以存儲(chǔ)從內(nèi)存中讀取的數(shù)據(jù),也可以存儲(chǔ)準(zhǔn)備寫進(jìn)內(nèi)存的數(shù)據(jù);
Flag: 用于表示對應(yīng)緩存行的狀態(tài)信息
3、Java 線程調(diào)度機(jī)制
在任意時(shí)刻,CPU 只能執(zhí)行一條機(jī)器指令,每個(gè)線程只有獲取到 CPU 的使用權(quán)后,才可以執(zhí)行指令;
也就是在任意時(shí)刻,只有一個(gè)線程占用 CPU,處于運(yùn)行的狀態(tài);
多線程并發(fā)運(yùn)行實(shí)際上是指多個(gè)線程輪流獲取 CPU 使用權(quán),分別執(zhí)行各自的任務(wù);
線程的調(diào)度由 JVM 負(fù)責(zé),線程的調(diào)度是按照特定的機(jī)制為多個(gè)線程分配 CPU 的使用權(quán);
線程調(diào)度模型分為兩類:分時(shí)調(diào)度模型和搶占式調(diào)度模型;
①分時(shí)調(diào)度模型
分時(shí)調(diào)度模型是讓所有線程輪流獲取 CPU 使用權(quán),并且平均分配每個(gè)線程占用 CPU 的時(shí)間片;
②搶占式調(diào)度模型
- JVM 采用的是搶占式調(diào)度模型,也就是先讓優(yōu)先級(jí)高的線程占用 CPU,如果線程的優(yōu)先級(jí)都一樣,那就隨機(jī)選擇一個(gè)線程,并讓該線程占用 CPU;
- 也就是如果我們同時(shí)啟動(dòng)多個(gè)線程,并不能保證它們能輪流獲取到均等的時(shí)間片;
- 如果我們的程序想干預(yù)線程的調(diào)度過程,最簡單的辦法就是給每個(gè)線程設(shè)定一個(gè)優(yōu)先級(jí);
三、線程的安全性問題詳解
線程安全問題不是說線程不安全,而是多個(gè)線程之間交錯(cuò)操作有可能導(dǎo)致數(shù)據(jù)異常;
下面我們就來看下與線程安全相關(guān)的競態(tài)和實(shí)現(xiàn)線程安全要保證的三個(gè)點(diǎn):原子性、可見性和有序性;
①原子性
- 原子(Atomic)的字面意識(shí)是不可分割的,對于涉及共享變量訪問的操作,若該操作從其執(zhí)行線程以外的任意線程看來是不可分割的,那么該操作就是原子操作,相應(yīng)地稱該操作具有原子性(Atomicity);
- 所謂不可分割,就是訪問(讀/寫)某個(gè)共享變量的操作,從執(zhí)行線程以外的其他線程看來,該操作只有未開始和結(jié)束兩種狀態(tài),不會(huì)知道該操作的中間部分;
- 訪問同一組共享變量的原子操作是不能被交錯(cuò)的,這就排除了一個(gè)線程執(zhí)行一個(gè)操作的期間,另一個(gè)線程讀取或更新該操作鎖訪問的共享變量,導(dǎo)致臟數(shù)據(jù)和丟失更新;
②可見性
- 在多線程環(huán)境下,一個(gè)線程對某個(gè)共享變量進(jìn)行更新后,后續(xù)訪問該變量的線程可能無法立刻讀取到這個(gè)更新的結(jié)果,甚至永遠(yuǎn)也無法讀取到這個(gè)更新的結(jié)果,這就是線程安全問題的另一種表現(xiàn)形式:可見性;
- 可見性是指一個(gè)線程對共享變量的更新,對于其他讀取該變量的線程是否可見;
- 可見性問題與計(jì)算機(jī)的存儲(chǔ)系統(tǒng)有關(guān),程序中的變量可能會(huì)被分配到寄存器而不是主內(nèi)存中,每個(gè)處理器都有自己的寄存器,一個(gè)處理器無法讀取另一個(gè)處理器的寄存器上的內(nèi)容;
- 即使共享變量是分配到主內(nèi)存中存儲(chǔ)的,也不餓能保證可見性,因?yàn)樘幚砥鞑皇侵苯釉L問主內(nèi)存,而是通過高速緩存(Cache)進(jìn)行的;
- 一個(gè)處理器上運(yùn)行的線程對變量的更新,可能只是更新到該處理器的寫緩沖器(Store Buffer)中,還沒有到高速緩存中,更別說處理器了;
- 可見性描述的是一個(gè)線程對共享變量的更新,對于另一個(gè)線程是否可見,保證可見性意味著一個(gè)線程可以讀取到對應(yīng)共享變量的新值;
- 從保證線程安全的角度來看,光保證原子性還不夠,還要保證可見性,同時(shí)保證可見性和原子性才能確保一個(gè)線程能正確地看到其他線程對共享變量做的更新;
③ 有序性
- 有序性是指一個(gè)處理器在為一個(gè)線程執(zhí)行的內(nèi)存訪問操作,對于另一個(gè)處理器上運(yùn)行的線程來看是亂序的;
- 順序結(jié)構(gòu)是結(jié)構(gòu)化編程中的一種基本結(jié)構(gòu),它表示我們希望某個(gè)操作先于另外一個(gè)操作執(zhí)行;
- 但是在多核處理器的環(huán)境下,代碼的執(zhí)行順序是沒保障的,編譯器可能改變兩個(gè)操作的先后順序,處理器也可能不是按照程序代碼的順序執(zhí)行指令;
- 重排序(Reordering)處理器和編譯器是對代碼做的一種優(yōu)化,它可以在不影響單線程程序正確性的情況下提升程序的性能,但是它會(huì)對多線程程序的正確性產(chǎn)生影響,導(dǎo)致線程安全問題;
- 現(xiàn)代處理器為了提高指令的執(zhí)行效率,往往不是按程序順序注意執(zhí)行指令的,而是哪條指令就緒就先執(zhí)行哪條指令,這就是處理器的亂序執(zhí)行;
四、實(shí)現(xiàn)線程安全
要實(shí)現(xiàn)線程安全就要保證上面說到的原子性、可見性和有序性;
常見的實(shí)現(xiàn)線程安全的辦法是使用鎖和原子類型,而鎖可分為內(nèi)部鎖、顯式鎖、讀寫鎖、輕量級(jí)鎖(volatile)四種;
下面我們就來看看這四種鎖和原子類型的用法和特點(diǎn);
1、鎖
是鎖(Lock)的作用,讓多個(gè)線程更好地協(xié)作,避免多個(gè)線程的操作交錯(cuò)導(dǎo)致數(shù)據(jù)異常的問題;
鎖的五個(gè)特點(diǎn):
- 臨界區(qū):持有鎖的線程獲得鎖后和釋放鎖前執(zhí)行的代碼叫做臨界區(qū)(Critical Section);
- 排他性:鎖具有排他性,能夠保障一個(gè)共享變量在任一時(shí)刻只能被一個(gè)線程訪問,這就保證了臨界區(qū)代碼一次只能夠被一個(gè)線程執(zhí)行,臨界區(qū)的操作具有不可分割性,也就保證了原子性;
- 串行:鎖相當(dāng)于是把多個(gè)線程對共享變量的操作從并發(fā)改為串行;
- 三種保障:鎖能夠保護(hù)共享變量實(shí)現(xiàn)線程安全,它的作用包括保障原子性、可見性和有序性;
- 調(diào)度策略:鎖的調(diào)度策略分為公平策略和非公平策略,對應(yīng)的鎖就叫 公平鎖和非公平鎖;公平鎖會(huì)在加鎖前查看是否有排隊(duì)等待的線程,有的話會(huì)優(yōu)先處理排在前面的線程;公平鎖以增加上下文切換為代價(jià),保障了鎖調(diào)度的公平性,增加了線程暫停和喚醒的可能性;
- 公平鎖的開銷比非公平鎖大,所以 ReentrantLock 的默認(rèn)調(diào)度策略是非公平策略;
2、 volatile 關(guān)鍵字
volatile 關(guān)鍵字可用于修飾共享變量,對應(yīng)的變量就叫 volatile 變量,volatile 變量有下面幾個(gè)特點(diǎn);
- 易變化:volatile 的字面意思是“不穩(wěn)定的”,也就是 volatile 用于修飾容易發(fā)生變化的變量,不穩(wěn)定指的是對這種變量的讀寫操作要從高速緩存或主內(nèi)存中讀取,而不會(huì)分配到寄存器中;
- 比鎖低:volatile 的開銷比鎖低,volatile 變量的讀寫操作不會(huì)導(dǎo)致上下文切換,所以 volatile 關(guān)鍵字也叫輕量級(jí)鎖 ;
- 比普通變量高:volatile 變量讀操作的開銷比普通變量要高,這是因?yàn)?volatile 變量的值每次都要從高速緩存或主內(nèi)存中讀取,無法被暫存到寄存器中;
- 釋放/存儲(chǔ)屏障:對于 volatile 變量的寫操作,JVM 會(huì)在該操作前插入一個(gè)釋放屏障,并在該操作后插入一個(gè)存儲(chǔ)屏障;存儲(chǔ)屏障具有沖刷處理器緩存的作用,所以在 volatile 變量寫操作后插入一個(gè)存儲(chǔ)屏障,能讓該存儲(chǔ)屏障前的所有操作結(jié)果對其他處理器來說是同步的;
- 加載/獲取屏障:對于 volatile 變量的讀操作,JVM 會(huì)在該操作前插入一個(gè)加載屏障,并在操作后插入一個(gè)獲取屏障;加載屏障通過沖刷處理器緩存,使線程所在的處理器將其他處理器對該共享變量做的更新同步到該處理器的高速緩存中;
- 保證有序性:volatile 能禁止指令重排序,也就是使用 volatile 能保證操作的有序性;
- 保證可見性:讀線程執(zhí)行的加載屏障和寫線程執(zhí)行的存儲(chǔ)屏障配合在一起,能讓寫線程對 volatile 變量的寫操作對讀線程可見,從而保證了可見性;
- 原子性:在原子性方面,對于 long/double 型變量,volatile 能保證讀寫操作的原子型;對于非 long/double 型變量,volatile 只能保證寫操作的原子性;如果 volatile 變量寫操作前涉及共享變量,競態(tài)仍然可能發(fā)生,因?yàn)楣蚕碜兞抠x值給 volatile 變量時(shí),其他線程可能已經(jīng)更新了該共享變量的值;
3、原子類型
原子類型簡介:
在 JUC 下有一個(gè) atomic 包,這個(gè)包里面有一組原子類,使用原子類的方法,不需要加鎖也能保證線程安全,而原子類是通過 Unsafe 類中的 CAS 指令從硬件層面來實(shí)現(xiàn)線程安全的;
這個(gè)包里面有如 AtomicInteger、AtomicBoolean、AtomicReference、AtomicReferenceFIeldUpdater 等;
我們先來看一個(gè)使用原子整型 AtomicInteger 自增的例子;
// 初始值為 1
AtomicInteger integer = new AtomicInteger(1);
// 自增
int result = integer.incrementAndGet();
// 結(jié)果為 2
System.out.println(result);
AtomicReference 和 AtomicReferenceFIeldUpdater 可以讓我們自己的類具有原子性,它們的原理都是通過 Unsafe 的 CAS 操作實(shí)現(xiàn)的;
我們下面看下它們的用法和區(qū)別;
①、AtomicReference 基本用法
- class AtomicReferenceValueHolder {
- AtomicReference<String> atomicValue = new AtomicReference<>("HelloAtomic");
- }
- public void getAndUpdateFromReference() {
- AtomicReferenceValueHolder holder = new AtomicReferenceValueHolder();
- // 對比并設(shè)值
- // 如果值是 HelloAtomic,就把值換成 World
- holder.atomicValue.compareAndSet("HelloAtomic", "World");
- // World
- System.out.println(holder.atomicValue.get());
- // 修改并獲取修改后的值
- String value = holder.atomicValue.updateAndGet(new UnaryOperator<String>() {
- @Override
- public String apply(String s) {
- return "HelloWorld";
- }
- });
- // Hello World
- System.out.println(value);
- }
② AtomicReferenceFieldUpdater 基本用法
AtomicReferenceFieldUpdater 在用法上和 AtomicReference 有些不同,我們直接把 String 值暴露了出來,并且用 volatile 對這個(gè)值進(jìn)行了修飾;
并且將當(dāng)前類和值的類傳到 newUpdater ()方法中獲取 Updater,這種用法有點(diǎn)像反射,而且 AtomicReferenceFieldUpdater 通常是作為類的靜態(tài)成員使用;
- public class SimpleValueHolder {
- public static AtomicReferenceFieldUpdater<SimpleValueHolder, String> valueUpdater
- = AtomicReferenceFieldUpdater.newUpdater(
- SimpleValueHolder.class, String.class, "value");
- volatile String value = "HelloAtomic";
- }
- public void getAndUpdateFromUpdater() {
- SimpleValueHolder holder = new SimpleValueHolder();
- holder.valueUpdater.compareAndSet(holder, "HelloAtomic", "World");
- // World
- System.out.println(holder.valueUpdater.get(holder));
- String value = holder.valueUpdater.updateAndGet(holder, new UnaryOperator<String>() {
- @Override
- public String apply(String s) {
- return "HelloWorld";
- }
- });
- // HelloWorld
- System.out.println(value);
- }
③AtomicReference 與 AtomicReferenceFieldUpdater 的區(qū)別
AtomicReference 和 AtomicReferenceFieldUpdater 的作用是差不多的,在用法上 AtomicReference 比 AtomicReferenceFIeldUpdater 更簡單;
但是在內(nèi)部實(shí)現(xiàn)上,AtomicReference 內(nèi)部一樣是有一個(gè) volatile 變量;
使用 AtomicReference 和使用 AtomicReferenceFIeldUpdater 比起來,要多創(chuàng)建一個(gè)對象;
對于 32 位的機(jī)器,這個(gè)對象的頭占 12 個(gè)字節(jié),它的成員占 4 個(gè)字節(jié),也就是多出來 16 個(gè)字節(jié);
對于 64 位的機(jī)器,如果啟動(dòng)了指針壓縮,那這個(gè)對象占用的也是 16 個(gè)字節(jié);
對于 64 位的機(jī)器,如果沒啟動(dòng)指針壓縮,那么這個(gè)對象就會(huì)占 24 個(gè)字節(jié),其中對象頭占 16 個(gè)字節(jié),成員占 8 個(gè)字節(jié);
當(dāng)要使用 AtomicReference 創(chuàng)建成千上萬個(gè)對象時(shí),這個(gè)開銷就會(huì)變得很大;
這也就是為什么 BufferedInputStream 、Kotlin 協(xié)程 和 Kotlin 的 lazy 的實(shí)現(xiàn)會(huì)選擇 AtomicReferenceFieldUpdater 作為原子類型;
因?yàn)殚_銷的原因,所以一般只有在原子類型創(chuàng)建的實(shí)例確定了較少的情況下,比如說是單例,才會(huì)選擇 AtomicReference,否則都是用 AtomicReferenceFieldUpdater;
4、 鎖的使用技巧
- 使用鎖會(huì)帶來一定的開銷,而掌握鎖的使用技巧可以在一定程度上減少鎖帶來的開銷和潛在的問題,下面就是一些鎖的使用技巧;
- 長鎖不如短鎖:盡量只對必要的部分加鎖;
- 大鎖不如小鎖:進(jìn)可能對加鎖的對象拆分;
- 公鎖不如私鎖:進(jìn)可能把鎖的邏輯放到私有代碼中,如果讓外部調(diào)用者加鎖,可能會(huì)導(dǎo)致鎖不正當(dāng)使用導(dǎo)致死鎖;
- 嵌套鎖不如扁平鎖:在寫代碼時(shí)要避免鎖嵌套;
- 分離讀寫鎖:盡可能將讀鎖和寫鎖分離;
- 粗化高頻鎖:合并處理頻繁而且過短的鎖,因?yàn)槊恳话焰i都會(huì)帶來一定的開銷;
- 消除無用鎖:盡可能不加鎖,或者用 volatile 代替;
五、線程的四個(gè)活躍性問題
1、死鎖
死鎖是線程的一種常見多線程活躍性問題,如果兩個(gè)或更多的線程,因?yàn)橄嗷サ却龑Ψ蕉挥肋h(yuǎn)暫停,那么這就叫死鎖現(xiàn)象;
下面我們就來看看死鎖產(chǎn)生的四個(gè)條件和避免死鎖的三個(gè)方法;
2、死鎖產(chǎn)生的四個(gè)條件
當(dāng)多個(gè)線程發(fā)生了死鎖后,這些線程和相關(guān)共享變量就會(huì)滿足下面四個(gè)條件:
- 資源互斥:涉及的資源必須是獨(dú)占的,也就是資源每次只能被一個(gè)線程使用
- 資源不可搶奪:涉及的資源只能被持有該資源的線程主動(dòng)釋放,無法被其他線程搶奪(被動(dòng)釋放)
- 占用并等待資源:涉及的線程至少持有一個(gè)資源,還申請了其他資源,而其他資源剛好被其他線程持有,并且線程不釋放已持有資源
- 循環(huán)等待資源:涉及的線程必須等待別的線程持有的資源,而別的線程又反過來等待該線程持有的資源
只要產(chǎn)生了死鎖,上面的條件就一定成立,但是上面的條件都成立也不一定會(huì)產(chǎn)生死鎖;
3、 避免死鎖的三個(gè)方法
要想消除死鎖,只要破壞掉上面的其中一個(gè)條件即可;
由于鎖具有排他性,且無法被動(dòng)釋放,所以我們只能破壞掉第三個(gè)和第四個(gè)條件;
①、粗鎖法
- 使用粗粒度的鎖代替多個(gè)鎖,鎖的范圍變大了,訪問共享資源的多個(gè)線程都只需要申請一個(gè)鎖,因?yàn)槊總€(gè)線程只需要申請一個(gè)鎖就可以執(zhí)行自己的任務(wù),這樣“占用并等待資源”和“循環(huán)等待資源”這兩個(gè)條件就不成立了;
- 粗鎖法的缺點(diǎn)是會(huì)降低并發(fā)性,而且可能導(dǎo)致資源浪費(fèi),因?yàn)椴捎么宙i法時(shí),一次只能有一個(gè)線程訪問資源,這樣其他線程就只能擱置任務(wù)了;
②鎖排序法
鎖排序法指的是相關(guān)線程使用全局統(tǒng)一的順序申請鎖;
假如有多個(gè)線程需要申請鎖,我們只需要讓這些線程按照一個(gè)全局統(tǒng)一的順序去申請鎖,這樣就能破壞“循環(huán)等待資源”這個(gè)條件;
③tryLock
顯式鎖 ReentrantLock.tryLock(long timeUnit) 這個(gè)方法允許我們?yōu)樯暾堟i的操作設(shè)置超時(shí)時(shí)間,這樣就能破壞“占用并等待資源”這個(gè)條件;
④開放調(diào)用
開放調(diào)用(Open Call)就是一個(gè)方法在調(diào)用外部方法時(shí)不持有鎖,開放調(diào)用能破壞“占用并等待資源”這個(gè)條件;
六、線程之間怎么協(xié)作?
線程間的常見協(xié)作方式有兩種:等待和中斷;
當(dāng)一個(gè)線程中的操作需要等待另一個(gè)線程中的操作結(jié)束時(shí),就涉及到等待型線程協(xié)作方式;
常用的等待型線程協(xié)作方式有 join、wait/notify、await/signal、await/countDown 和 CyclicBarrier 五種,下面我們就來看看這五種線程協(xié)作方式的用法和區(qū)別;
1、join
- 使用 Thread.join() 方法,我們可以讓一個(gè)線程等待另一個(gè)線程執(zhí)行結(jié)束后再繼續(xù)執(zhí)行;
- join() 方法實(shí)現(xiàn)等待是通過 wait() 方法實(shí)現(xiàn)的,在 join() 方法中,會(huì)不斷判斷調(diào)用了 join() 方法的線程是否還存活,是的話則繼續(xù)等待;
下面是 join() 方法的簡單用法;
- public void tryJoin() {
- Thread threadA = new ThreadA();
- Thread threadB = new ThreadB(threadA);
- threadA.start();
- threadB.start();
- }
- public class ThreadA extends Thread {
- @Override
- public void run() {
- System.out.println("線程 A 開始執(zhí)行");
- ThreadUtils.sleep(1000);
- System.out.println("線程 A 執(zhí)行結(jié)束");
- }
- }
- public class ThreadB extends Thread {
- private final Thread threadA;
- public ThreadB(Thread thread) {
- threadA = thread;
- }
- @Override
- public void run() {
- try {
- System.out.println("線程 B 開始等待線程 A 執(zhí)行結(jié)束");
- threadA.join();
- System.out.println("線程 B 結(jié)束等待,開始做自己想做的事情");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
2、 wait/notify
- 一個(gè)線程因?yàn)閳?zhí)行操作(目標(biāo)動(dòng)作)所需的保護(hù)條件未滿足而被暫停的過程就叫等待(wait);
- 一個(gè)線程更新了共享變量,使得其他線程需要的保護(hù)條件成立,喚醒了被暫停的線程的過程就叫通知(notify);
- wait() 方法的執(zhí)行線程叫等待線程,notify() 方法執(zhí)行的線程叫通知線程;
下面是 wait/notify 使用的示例代碼;
- final Object lock = new Object();
- private volatile boolean conditionSatisfied;
- public void startWait() throws InterruptedException {
- synchronized (lock) {
- System.out.println("等待線程獲取了鎖");
- while(!conditionSatisfied) {
- System.out.println("保護(hù)條件不成立,等待線程進(jìn)入等待狀態(tài)");
- lock.wait();
- }
- System.out.println("等待線程被喚醒,開始執(zhí)行目標(biāo)動(dòng)作");
- }
- }
- public void startNotify() {
- synchronized (lock) {
- System.out.println("通知線程獲取了鎖");
- System.out.println("通知線程即將喚醒等待線程");
- conditionSatisfied = true;
- lock.notify();
- }
- }
3、 wait/notify 原理
- JVM 會(huì)給每個(gè)對象維護(hù)一個(gè)入口集(Entry Set)和等待集(Wait Set);
- 入口集用于存儲(chǔ)申請?jiān)搶ο髢?nèi)部鎖的線程,等待集用于存儲(chǔ)對象上的等待線程;
- wait() 方法會(huì)將當(dāng)前線程暫停,在釋放內(nèi)部鎖時(shí),會(huì)將當(dāng)前線程存入該方法所屬的對象等待集中;
- 調(diào)用對象的 notify() 方法,會(huì)讓該對象的等待集中的任意一個(gè)線程喚醒,被喚醒的線程會(huì)繼續(xù)留在對象的等待集中,直到該線程再次持有對應(yīng)的內(nèi)部鎖時(shí),wait() 方法就會(huì)把當(dāng)前線程從對象的等待集中移除;
- 添加當(dāng)前線程到等待集、暫停當(dāng)前線程、釋放鎖以及把喚醒后的線程從對象的等待集中移除,都是在 wait() 方法中實(shí)現(xiàn)的;
- 在 wait() 方法的 native 代碼中,會(huì)判斷線程是否持有當(dāng)前對象的內(nèi)部鎖,如果沒有的話,就會(huì)報(bào)非法監(jiān)視器狀態(tài)異常,這也就是為什么要在同步代碼塊中執(zhí)行 wait() 方法;
4、notify()/notifyAll()
notify() 可能導(dǎo)致信號(hào)丟失,而 notifyAll() 雖然會(huì)把不需要喚醒的等待線程也喚醒,但是在正確性方面有保障;
所以一般情況下優(yōu)先使用 notifyAll() 保障正確性;
一般情況下,只有在下面兩個(gè)條件都實(shí)現(xiàn)時(shí),才會(huì)選擇使用 notify() 實(shí)現(xiàn)通知;
①只需喚醒一個(gè)線程
當(dāng)一次通知只需要喚醒最多一個(gè)線程時(shí),我們可以考慮使用 notify() 實(shí)現(xiàn)通知,但是光滿足這個(gè)條件還不夠;
在不同的等待線程使用不同的保護(hù)條件時(shí),notify() 喚醒的一個(gè)任意線程可能不是我們需要喚醒的那個(gè)線程,所以需要條件 2 來排除;
②對象的等待集中只包含同質(zhì)等待線程
同質(zhì)等待線程指的是線程使用同一個(gè)保護(hù)條件并且 wait() 調(diào)用返回后的邏輯一致;
最典型的同質(zhì)線程是使用同一個(gè) Runnable 創(chuàng)建的不同線程,或者同一個(gè) Thread 子類 new 出來的多個(gè)實(shí)例;
5、await/signal
wait()/notify() 過于底層,而且還存在兩個(gè)問題,一是過早喚醒、二是無法區(qū)分 Object.wait(ms) 返回是由于等待超時(shí)還是被通知線程喚醒;
await/signal 基本用法
- private Lock lock = new ReentrantLock();
- private Condition condition = lock.newCondition();
- private volatile boolean conditionSatisfied = false;
- private void startWait() {
- lock.lock();
- System.out.println("等待線程獲取了鎖");
- try {
- while (!conditionSatisfied) {
- System.out.println("保護(hù)條件不成立,等待線程進(jìn)入等待狀態(tài)");
- condition.await();
- }
- System.out.println("等待線程被喚醒,開始執(zhí)行目標(biāo)動(dòng)作");
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock();
- System.out.println("等待線程釋放了鎖");
- }
- }
- public void startNotify() {
- lock.lock();
- System.out.println("通知線程獲取了鎖");
- try {
- conditionSatisfied = true;
- System.out.println("通知線程即將喚醒等待線程");
- condition.signal();
- } finally {
- System.out.println("通知線程釋放了鎖");
- lock.unlock();
- }
- }
- 當(dāng)我們在兩個(gè)線程中分別執(zhí)行了上面的兩個(gè)函數(shù)后,能得到下面的輸出;
- 等待線程獲取了鎖
- 保護(hù)條件不成立,等待線程進(jìn)入等待狀態(tài)
- 通知線程獲取了鎖
- 通知線程即將喚醒等待線程
- 等待線程被喚醒,開始執(zhí)行目標(biāo)動(dòng)作
6、 awaitUntil() 用法
awaitUntil(timeout, unit) 方法;
如果是由于超時(shí)導(dǎo)致等待結(jié)束,那么 awaitUntil() 會(huì)返回 false,否則會(huì)返回 true,表示等待是被喚醒的,下面我們就看看這個(gè)方法是怎么用的;
- private void startTimedWait() throws InterruptedException {
- lock.lock();
- System.out.println("等待線程獲取了鎖");
- // 3 秒后超時(shí)
- Date date = new Date(System.currentTimeMillis() + 3 * 1000);
- boolean isWakenUp = true;
- try {
- while (!conditionSatisfied) {
- if (!isWakenUp) {
- System.out.println("已超時(shí),結(jié)束等待任務(wù)");
- return;
- } else {
- System.out.println("保護(hù)條件不滿足,并且等待時(shí)間未到,等待進(jìn)入等待狀態(tài)");
- isWakenUp = condition.awaitUntil(date);
- }
- }
- System.out.println("等待線程被喚醒,開始執(zhí)行目標(biāo)動(dòng)作");
- } finally {
- lock.unlock();
- }
- }
- public void startDelayedNotify() {
- threadSleep(4 * 1000);
- startNotify();
- }
- 等待線程獲取了鎖
- 保護(hù)條件不滿足,并且等待時(shí)間未到,等待進(jìn)入等待狀態(tài)
- 已超時(shí),結(jié)束等待任務(wù)
- 通知線程獲取了鎖
- 通知線程即將喚醒等待線程
7、 await/countDown
使用 join() 實(shí)現(xiàn)的是一個(gè)線程等待另一個(gè)線程執(zhí)行結(jié)束,但是有的時(shí)候我們只是想要一個(gè)特定的操作執(zhí)行結(jié)束,不需要等待整個(gè)線程執(zhí)行結(jié)束,這時(shí)候就可以使用 CountDownLatch 來實(shí)現(xiàn);
await/countDown 基本用法
- public void tryAwaitCountDown() {
- startWaitThread();
- startCountDownThread();
- startCountDownThread();
- }
- final int prerequisiteOperationCount = 2;
- final CountDownLatch latch = new CountDownLatch(prerequisiteOperationCount);
- private void startWait() throws InterruptedException {
- System.out.println("等待線程進(jìn)入等待狀態(tài)");
- latch.await();
- System.out.println("等待線程結(jié)束等待");
- }
- private void startCountDown() {
- try {
- System.out.println("執(zhí)行先決操作");
- } finally {
- System.out.println("計(jì)數(shù)值減 1");
- latch.countDown();
- }
- }
8、 CyclicBarrier
有的時(shí)候多個(gè)線程需要互相等待對方代碼中的某個(gè)地方(集合點(diǎn)),這些線程才能繼續(xù)執(zhí)行,這時(shí)可以使用 CyclicBarrier(柵欄);
CyclicBarrier 基本用法
- final int parties = 3;
- final Runnable barrierAction = new Runnable() {
- @Override
- public void run() {
- System.out.println("人來齊了,開始爬山");
- }
- };
- final CyclicBarrier barrier = new CyclicBarrier(parties, barrierAction);
- public void tryCyclicBarrier() {
- firstDayClimb();
- secondDayClimb();
- }
- private void firstDayClimb() {
- new PartyThread("第一天爬山,老李先來").start();
- new PartyThread("老王到了,小張還沒到").start();
- new PartyThread("小張到了").start();
- }
- private void secondDayClimb() {
- new PartyThread("第二天爬山,老王先來").start();
- new PartyThread("小張到了,老李還沒到").start();
- new PartyThread("老李到了").start();
- }
- public class PartyThread extends Thread {
- private final String content;
- public PartyThread(String content) {
- this.content = content;
- }
- @Override
- public void run() {
- System.out.println(content);
- try {
- barrier.await();
- } catch (BrokenBarrierException e) {
- e.printStackTrace();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
Android 中常用的 7 種異步方式:Thread、HandlerThread、IntentService、AsyncTask、線程池、RxJava 和 Kotlin 協(xié)程;
總結(jié):
1、線程有很多優(yōu)勢:
- 提高多處理器的利用效率;
- 簡化業(yè)務(wù)功能設(shè)計(jì);
- 實(shí)現(xiàn)異步處理;
2、多線程的風(fēng)險(xiǎn):
- 共享數(shù)據(jù)的線程安全性;
- 多線程執(zhí)行時(shí)的活躍性問題;
- 多線程的所帶來的性能損失問題;
3、下次詳解下Android中常用異步方式,從實(shí)際出發(fā)。