Java Thread 多線程
程序:是指令和數(shù)據(jù)的有序集合,本身沒有任何運(yùn)行的含義,是一個(gè)靜態(tài)的概念
進(jìn)程:是執(zhí)行程序的一次執(zhí)行過程,是一個(gè)動(dòng)態(tài)的概念沪么,是系統(tǒng)資源分配的單位
線程:一個(gè)進(jìn)程可以包含若干個(gè)線程,一個(gè)進(jìn)程至少有一個(gè)線程锌半,不然沒有存在的意義禽车,線程是CPU調(diào)度和執(zhí)行的單位。
線程創(chuàng)建
- 創(chuàng)建自定義線程類繼承Thread類
重寫run() 方法,編寫線程執(zhí)行體哭当。創(chuàng)建線程對(duì)象,調(diào)用start() 方法啟動(dòng)線程
主線程如果調(diào)用run() 方法冗澈,則會(huì)先執(zhí)行run() 方法
多條執(zhí)行路徑钦勘,主線程調(diào)用start() 方法,子線程就會(huì)調(diào)用run() 方法亚亲,主線程和子線程<u>交替</u>執(zhí)行(即同時(shí)進(jìn)行彻采,但在同一時(shí)間只能做一件事情)
注意:
線程開啟不一定立即執(zhí)行,由CPU調(diào)度執(zhí)行
<u>不建議使用:避免OOP單繼承局限性</u>
- 創(chuàng)建一個(gè)線程聲明實(shí)現(xiàn)Runnable接口
重寫run() 方法捌归,編寫線程執(zhí)行體肛响。創(chuàng)建線程對(duì)象,調(diào)用start() 方法啟動(dòng)線程
在主線程里面創(chuàng)建Runnable接口的實(shí)現(xiàn)類對(duì)象
TestThread testThread = new TestThread(); //然后丟入創(chuàng)建的Threa類對(duì)象里
/*
Thread thread = new Thread(testThread); //創(chuàng)建線程對(duì)象惜索,通過線程對(duì)象開啟線程
thread.start(); */
//注釋里兩行代碼相當(dāng)于下行代碼
new Thread(testThread).start();
<u>推薦使用:避免單繼承局限性特笋,靈活方便,方便同一個(gè)對(duì)象被多個(gè)對(duì)象使用巾兆。</u>
- 實(shí)現(xiàn)Callable 接口(了解)
實(shí)現(xiàn)Callable接口猎物,需要返回值類型,重寫call() 方法角塑,需要拋出異常蔫磨,創(chuàng)建目標(biāo)對(duì)象,<u>創(chuàng)建執(zhí)行服務(wù)</u> ExecutorService圃伶,通過服務(wù)去提交方法堤如,最后關(guān)閉服務(wù)
好處:可以定義返回值,可以拋出異常
補(bǔ)充
用start方法來啟動(dòng)線程窒朋,真正實(shí)現(xiàn)了多線程運(yùn)行搀罢,這時(shí)無需等待run方法體中的代碼執(zhí)行完畢而直接繼續(xù)執(zhí)行后續(xù)的代碼。通過調(diào)用Thread類的 start()方法來啟動(dòng)一個(gè)線程炼邀,這時(shí)此線程處于就緒(可運(yùn)行)狀態(tài)魄揉,并沒有運(yùn)行,一旦得到cpu時(shí)間片拭宁,就開始執(zhí)行run()方法洛退,這里的run()方法 稱為線程體,它包含了要執(zhí)行的這個(gè)線程的內(nèi)容杰标,Run方法運(yùn)行結(jié)束兵怯,此線程隨即終止。
靜態(tài)代理
靜態(tài)代理模式:
- 真是對(duì)象和代理對(duì)象都要實(shí)現(xiàn)同一個(gè)接口
- 代理對(duì)象要代理真實(shí)對(duì)象(實(shí)現(xiàn)真實(shí)對(duì)象做不了的事情腔剂,在代理對(duì)象里引入真實(shí)對(duì)象)
好處:
- 代理對(duì)象可以做很多真實(shí)對(duì)象做不了的事情
- 真是對(duì)象專注做自己的事情
Lambda表達(dá)式
new Thread( ()-> sout("nihao!") ).start();
- 避免匿名內(nèi)部類定義過多
- 讓代碼看起來更簡(jiǎn)潔
- 去掉一堆沒意義的代碼媒区,只留下核心的邏輯
函數(shù)式接口:
任何接口,如果只包含<u>唯一一個(gè)抽象方法</u>,那么他就是一個(gè)函數(shù)式接口
對(duì)于函數(shù)式接口袜漩,我們可以通過lambda表達(dá)式來創(chuàng)建該接口的對(duì)象
() -> sout() ; 語句中绪爸,前面的( ) 就是new 的接口和其包含的抽象方法
注意:
接口的實(shí)現(xiàn)類如果放在主類里,要加static 關(guān)鍵字宙攻,即作為 內(nèi)部實(shí)現(xiàn)類
放在方法里奠货,就是作為局部?jī)?nèi)部類
作為匿名內(nèi)部類的話,new的是接口座掘,而不是實(shí)現(xiàn)類
lambda表達(dá)式就是在匿名內(nèi)部類的基礎(chǔ)上省略了new 接口和重寫的抽象方法递惋,只需要留下參入的參數(shù) -> ...
lambda表達(dá)式只能有一行代碼的情況下才能簡(jiǎn)化為一行,如果有多行就必須用代碼塊包裹溢陪,即 -> 后加{ sout(); sout(); }; 前提是:必須是函數(shù)式接口
單個(gè)或多個(gè)參數(shù)也可以去掉參數(shù)類型萍虽,要去就都要去掉,多個(gè)參數(shù)就需要加括號(hào)包裹
多線程的優(yōu)勢(shì)和存儲(chǔ)的風(fēng)險(xiǎn)
多線程編程具備以下優(yōu)勢(shì):
提高系統(tǒng)的吞吐率(Throughout)形真,多線程編程可以使一個(gè)進(jìn)程有多個(gè)并發(fā)的操作
提高響應(yīng)性(Responsiveness)杉编,Web服務(wù)器會(huì)采用一些專門的線程負(fù)責(zé)用戶的請(qǐng)求處理,縮短用戶的等待時(shí)間
充分利用多核處理器資源没酣,通過多線程可以充分的利用CPU資源
多線程編程存在的風(fēng)險(xiǎn):
- 線程安全問題王财,多線程共享數(shù)據(jù)時(shí),如果沒有采取正確的并發(fā)訪問控制措施裕便,就可能會(huì)產(chǎn)生一些數(shù)據(jù)一致性問題绒净,如讀取臟數(shù)據(jù)(過期的數(shù)據(jù)),如丟失數(shù)據(jù)更新偿衰。
- 線程活性問題挂疆,由于程序自身的缺陷或者資源稀缺性導(dǎo)致線程一直處于非RUNNABLE狀態(tài),這就是線程活性問題下翎,常見的活性故障有以下幾種:
- 死鎖(DeadLock)
- 鎖死(Lockout)
- 活鎖(LiveLock)
- 饑餓(Starvation)
- 上下文切換(Context Switch)缤言,處理器從執(zhí)行一個(gè)線程切換到執(zhí)行另外一個(gè)線程
- 可靠性,可能會(huì)由一個(gè)線程導(dǎo)致JVM意外終止视事,其他線程也無法執(zhí)行
線程狀態(tài)
創(chuàng)建狀態(tài)胆萧、就緒狀態(tài)、阻塞狀態(tài)俐东、運(yùn)行狀態(tài)跌穗、死亡狀態(tài)
<u>創(chuàng)建狀態(tài)</u>( new ) -> 調(diào)用start( ) 方法進(jìn)入 <u>就緒狀態(tài)</u>,等待CPU的調(diào)度虏辫,調(diào)度完后 -> 進(jìn)入<u>運(yùn)行狀態(tài)</u>蚌吸,運(yùn)行狀態(tài)中調(diào)用sleep、wait方法等可以使線程進(jìn)入<u>阻塞狀態(tài)</u> -> 阻塞狀態(tài)解除后使線程又進(jìn)入就緒狀態(tài) -> 如果線程正常執(zhí)行完砌庄,就進(jìn)入了<u>死亡狀態(tài)</u>
Thread.getState();
//獲取線程狀態(tài)
Thread.State
- NEW :尚未啟動(dòng)的線程處于此狀態(tài)
- RUNNABLE :在Java虛擬機(jī)中執(zhí)行的線程處于此狀態(tài)
- BLOCKED :被阻塞等待監(jiān)視器鎖定的線程處于此狀態(tài)
- WAITING :正在等待另一個(gè)線程執(zhí)行待定動(dòng)作的線程處于此狀態(tài)
- TIMED_WAITING :正在等待另一個(gè)線程執(zhí)行動(dòng)作達(dá)到指定等待時(shí)間的線程處于此狀態(tài)
- TERMINATED :已退出的線程處于此狀態(tài)
一個(gè)線程可以在給定時(shí)間點(diǎn)處于一個(gè)狀態(tài)羹唠,這些狀態(tài)不反映任何操作系統(tǒng)線程狀態(tài)的虛擬機(jī)狀態(tài)奕枢。
thread.getState() 方法可以獲取線程當(dāng)前狀態(tài)
線程中斷或結(jié)束,一旦進(jìn)入死亡狀態(tài)佩微,就不能再次啟動(dòng)缝彬,線程只能啟動(dòng)一次
線程方法
方法 | 說明 |
---|---|
setPriority(int newPriority) | 更改線程優(yōu)先級(jí) |
static void sleep(long millis) | 讓當(dāng)前線程休眠指定毫秒數(shù) |
void join( ) | 等待該線程終止 |
static void yield( ) 禮讓線程 | 暫停當(dāng)前正在執(zhí)行的線程對(duì)象,并執(zhí)行其他線程 |
void interrupt( ) 不建議使用 | 中斷線程哺眯,別用這個(gè)方式 |
boolean isAlive( ) | 測(cè)試線程是否處于活躍狀態(tài) |
不推薦jdk推薦的停止線程的方法(stop跌造、destroy)
推薦線程自己停止下來,建議使用一個(gè)標(biāo)志位進(jìn)行終止變量族购,當(dāng)flag=false,則終止線程運(yùn)行
線程休眠:
- sleep 指定當(dāng)前線程阻塞的毫秒數(shù)陵珍;
- sleep 存在異常InterruptException寝杖;
- sleep 時(shí)間達(dá)到后線程進(jìn)入就緒狀態(tài);
- sleep 可以模擬網(wǎng)絡(luò)延時(shí)互纯,倒計(jì)時(shí)等瑟幕;
- 每一個(gè)對(duì)象都有一個(gè)鎖,sleep 不會(huì)釋放鎖留潦;
線程禮讓 yield:
- 禮讓線程只盹,讓當(dāng)前正在執(zhí)行的線程暫停,但不阻塞
- 將線程從運(yùn)行狀態(tài)轉(zhuǎn)為就緒狀態(tài)
- 讓CPU重新調(diào)度兔院,<u>禮讓不一定成功殖卑,取決于CPU</u>
合并線程 join:
- Join 合并線程,待此線程執(zhí)行完成后坊萝,再執(zhí)行其他線程孵稽,其他線程阻塞(插隊(duì))
線程優(yōu)先級(jí)
- Java 提供一個(gè)線程調(diào)度器來監(jiān)控程序中啟動(dòng)后進(jìn)入就緒狀態(tài)的所有線程,線程調(diào)度器按照優(yōu)先級(jí)決定應(yīng)該調(diào)度哪個(gè)線程來執(zhí)行
- 線程的優(yōu)先級(jí)用數(shù)字表示十偶,范圍從 1~10
- Thread.MIN_PRIOROTY = 1 最小優(yōu)先級(jí)
- Thread.MAX_PRIOROTY = 10 最大優(yōu)先級(jí)
- Thread.NORM_PRIOROTY = 5 默認(rèn)優(yōu)先級(jí)(main)
注意:線程優(yōu)先級(jí)大不一定先執(zhí)行菩鲜,真是權(quán)重更大了而已,獲得的資源更多惦积,還是看CPU的調(diào)度
- 使用以下方式該百年或獲取優(yōu)先級(jí)
- getPriority( )
- setPriority( int xxx )
守護(hù)線程
- 線程分為用戶線程和守護(hù)線程
- 虛擬機(jī)必須確保用戶線程執(zhí)行完畢
- 虛擬機(jī)不用等待守護(hù)線程執(zhí)行完畢
如:后臺(tái)記錄操作日志接校、監(jiān)控內(nèi)存、垃圾回收等待機(jī)制..
//設(shè)置守護(hù)線程
thread.setDaemon(true); //默認(rèn)是false表示是用戶線程狮崩,正常線程都是用戶線程
中斷線程
只是給線程打上一個(gè)中斷的標(biāo)志蛛勉,但是線程并不會(huì)中斷,需要判斷過后起標(biāo)志再去操作厉亏。
Thread.interrupt(); //中斷
t.isInterrupted(); //判斷是否中斷(是否打上標(biāo)志)
線程安全問題
非線程安全主要是指多個(gè)線程對(duì)同一個(gè)對(duì)象的實(shí)例變量進(jìn)行操作時(shí)董习,會(huì)出現(xiàn)值被更改,值不同步的情況爱只。
線程安全問題表現(xiàn)為三個(gè)方面:原子性皿淋、可見性和有序性招刹。
-
原子性
原子(Atomic)就是不可分割的意思,原子操作的不可分割有兩層含義:
- 訪問(讀/寫)某個(gè)共享變量的操作從其他線程來看窝趣,該操作要么已經(jīng)執(zhí)行完畢疯暑,要么尚未發(fā)生,即其他線程看不到當(dāng)前操作的中間結(jié)果
- 訪問同一組共享變量的原子操作是不能交錯(cuò)的
Java有兩種方式實(shí)現(xiàn)原子性:1)鎖哑舒;2)處理器的CAS指令
- 鎖具有排他性妇拯,保證共享變量在某一時(shí)刻只能被一個(gè)線程訪問。
- CAS指令直接在硬件(處理器和內(nèi)存)層次上實(shí)現(xiàn)洗鸵,看作硬件鎖
-
可見性
在多線程環(huán)境中越锈,一個(gè)線程對(duì)某個(gè)共享變量進(jìn)行更新后,后續(xù)的其他線程可能無法立即讀到這個(gè)更新的結(jié)果
-
有序性
有序性(Ordering)是指在什么情況下一個(gè)處理器上運(yùn)行的一個(gè)線程所執(zhí)行的內(nèi)存訪問操作在另外一個(gè)處理器運(yùn)行的其他線程看來是亂序的膘滨。
亂序是指內(nèi)存訪問操作的順序看起來發(fā)生了變化
在多核處理器的環(huán)境下甘凭,編寫的順序結(jié)構(gòu),這種操作執(zhí)行的順序可能是沒有保障的:
- 編譯器可能會(huì)改變兩個(gè)操作的先后順序火邓;
- 處理器也可能不會(huì)按照目標(biāo)代碼的順序執(zhí)行丹弱;
- 這種一個(gè)處理器上執(zhí)行的多個(gè)操作,在其他處理器來看它的順序與目標(biāo)代碼指定的順序可能不一樣铲咨,這種現(xiàn)象就叫重排序
- 重排序是對(duì)內(nèi)存訪問有序操作的一種優(yōu)化躲胳,可以不影響單線程程序正確的情況下提升程序的性能,但是可能對(duì)多線程程序的正確性產(chǎn)生影響纤勒,即可能導(dǎo)致線程安全問題
可以把重排序分為指令重排序和存儲(chǔ)子系統(tǒng)重排序兩種:
- 指令重排序主要是由JIT編譯器坯苹,處理器引起的,指程序順序和執(zhí)行順序不一樣
- 存儲(chǔ)子系統(tǒng)重排序是由高速緩存摇天,寫緩沖器引起的北滥,感知順序與執(zhí)行順序不一致
指令重排序
在源碼順序與程序順序不一致或者程序順序與執(zhí)行順序不一致的情況下,我們就說發(fā)生了指令重排序(Instruction Reorder)
指令重排是一種動(dòng)作闸翅,確實(shí)對(duì)指令的順序做出了調(diào)整再芋,重排序的對(duì)象指令
javac編譯器一般不會(huì)執(zhí)行指令重排序,而 JIT編譯器可能執(zhí)行指令重排序坚冀,處理器也可能執(zhí)行指令重排序济赎,使得執(zhí)行順序和程序順序不一致。
指令重排不會(huì)對(duì)單線程程序的結(jié)果正確性產(chǎn)生影響记某,可能導(dǎo)致對(duì)多線程程序出現(xiàn)非預(yù)期結(jié)果司训。
存儲(chǔ)子系統(tǒng)重排序
存儲(chǔ)子系統(tǒng)是指寫緩沖器與高速緩存。
高速緩存(Cache)是CPU中為了匹配與主內(nèi)存處理速度不匹配而設(shè)計(jì)的一個(gè)高速緩存液南。
寫緩沖器(Store buffer壳猜,Write buffer)用來提高寫高速緩存操作的效率
即使處理器嚴(yán)格按照程序順序執(zhí)行兩個(gè)內(nèi)存訪問操作,在存儲(chǔ)子系統(tǒng)的作用下滑凉,其他處理器對(duì)這兩個(gè)操作的感知順序與程序順序不一致统扳,即這兩個(gè)操作的執(zhí)行順序看起來像是發(fā)生了變化喘帚,這種現(xiàn)象稱為 存儲(chǔ)子系統(tǒng)重排序。
存儲(chǔ)子系統(tǒng)重排序并沒有真正的對(duì)指令執(zhí)行順序進(jìn)行調(diào)整咒钟,而是造成一種指令執(zhí)行順序被調(diào)整的假象吹由。
存儲(chǔ)子系統(tǒng)重排序?qū)ο笫莾?nèi)存操作的結(jié)果。
保證內(nèi)存訪問的順序性
實(shí)質(zhì)上就是怎么解決重排序?qū)е碌木€程安全問題朱嘴。
可以使用volatile關(guān)鍵字倾鲫,synchronized關(guān)鍵字實(shí)現(xiàn)有序性。
線程同步
線程同步機(jī)制是用于協(xié)調(diào)線程之間的數(shù)據(jù)訪問的機(jī)制萍嬉,該機(jī)制可以保障線程安全乌昔。
Java平臺(tái)提供的線程同步機(jī)制包括:鎖Lock,volatile關(guān)鍵字壤追,final關(guān)鍵字玫荣,static關(guān)鍵字,以及相關(guān)的API大诸,如Object.wait()/Object.notify()等
<u>同一個(gè)對(duì)象被多個(gè)線程同時(shí)操作,</u>這時(shí)候就需要線程同步贯卦,線程同步其實(shí)就是一種等待機(jī)制资柔,多個(gè)需要同時(shí)訪問此對(duì)象的線程進(jìn)入這個(gè)對(duì)象的等待池形成隊(duì)列,等待前面的線程使用完畢撵割,下一個(gè)線程再使用
鎖的概述
鎖具有排他性(Exclusive)贿堰,即一個(gè)鎖一次只能被一個(gè)線程持有,這種鎖稱為排他鎖或互斥鎖(Mutex)
**JVM把鎖分為內(nèi)部鎖和顯式鎖啡彬,內(nèi)部鎖通過synchronized關(guān)鍵字實(shí)現(xiàn)羹与;顯式鎖通過java.concurrent.locks.lock接口的實(shí)現(xiàn)類實(shí)現(xiàn)的。**
鎖的作用
鎖可以實(shí)現(xiàn)對(duì)共享數(shù)據(jù)的安全訪問庶灿,保障線程的原子性纵搁,可見性與有序性。鎖是通過互斥保障原子性往踢,一個(gè)鎖只能被一個(gè)線程持有腾誉,這就保證臨界區(qū)的代碼一次只能被一個(gè)線程執(zhí)行。使得臨界區(qū)代碼所執(zhí)行的操作自然而然的具有不可分割的特性(原子性)
可見性的保障是用過寫線程沖刷處理器的緩存和讀線程刷新處理器緩存這兩個(gè)動(dòng)作實(shí)現(xiàn)的峻呕。在java平臺(tái)中利职,鎖的獲得隱含著刷新處理器緩存的動(dòng)作,而鎖的釋放隱含著沖刷處理器緩存的動(dòng)作瘦癌。
鎖能夠保障有序性猪贪,寫線程在臨界區(qū)所執(zhí)行的操作,在讀線程所執(zhí)行的臨界區(qū)看來像是完全按照源碼順序執(zhí)行的讯私。
注意:
使用鎖保障線程的安全性热押,必須滿足以下條件:
- 這些線程在訪問共享數(shù)據(jù)時(shí)必須使用同一個(gè)鎖
- 即使是讀取共享數(shù)據(jù)的線程也需要使用同步鎖
鎖相關(guān)概念
-
可重入性(Reentrancy)
一個(gè)線程持有該鎖的時(shí)候能再次(多次)申請(qǐng)?jiān)撴i西傀。即如果一個(gè)線程持有的一個(gè)鎖的時(shí)候還能夠繼續(xù)成功申請(qǐng)?jiān)撴i,稱該鎖是可重入的楞黄,否則就稱該鎖不可重入的池凄。
-
鎖的爭(zhēng)用與調(diào)度
Java平臺(tái)中內(nèi)部鎖屬于非公平鎖,顯式鎖lock既支持公平鎖又支持非公平鎖
-
鎖的粒度
一個(gè)鎖可以保護(hù)的共享數(shù)據(jù)的數(shù)量大小稱為鎖的粒度鬼廓。
鎖保護(hù)的共享數(shù)據(jù)量大肿仑,稱該鎖的粒度粗,否則就稱該鎖粒度細(xì)
鎖的粒度過粗會(huì)導(dǎo)致線程在申請(qǐng)鎖時(shí)會(huì)進(jìn)行不必要的等待碎税,鎖的粒度過細(xì)會(huì)增加鎖調(diào)度的開銷尤慰。
內(nèi)部鎖:synchronized關(guān)鍵字
Java中的每個(gè)對(duì)象都有一個(gè)與之關(guān)聯(lián)的內(nèi)部鎖,這種鎖也稱為監(jiān)視器(Monitor)雷蹂,這種鎖是一種排他鎖伟端,可以保障原子性、可見性和有序性
內(nèi)部鎖是通過synchronized關(guān)鍵字實(shí)現(xiàn)的匪煌,synchronized關(guān)鍵字可以修飾代碼塊责蝠,修飾該方法。
線程同步實(shí)現(xiàn)條件:隊(duì)列+鎖
自我理解:公共廁所的例子萎庭!線程在線程池排隊(duì)訪問對(duì)象霜医,形成隊(duì)列,<u>
每個(gè)對(duì)象都有一個(gè)鎖
</u>一個(gè)線程訪問對(duì)象后獲得對(duì)象的排他鎖驳规,獨(dú)占資源肴敛,使其他線程必須等待,使用完后釋放鎖即可
存在的問題:
- 一個(gè)線程持有鎖會(huì)導(dǎo)致其他所有需要此鎖的線程掛起
- 在多線程的競(jìng)爭(zhēng)下吗购,加鎖會(huì)導(dǎo)致比較多的上下文切換 和 調(diào)度延時(shí)医男,引起性能問題
- 如果一個(gè)優(yōu)先級(jí)高的線程等待一個(gè)優(yōu)先級(jí)低的線程釋放鎖,引起優(yōu)先級(jí)倒置捻勉,引起性能問題
同步方法:
synchronized 方法
public synchronized void method(int args){} //同步方法
- synchronized 方法控制對(duì)“對(duì)象”的訪問镀梭,每個(gè)對(duì)象應(yīng)有一把鎖,每個(gè)synchronized方法都必須獲得調(diào)用該方法的對(duì)象的鎖才能執(zhí)行踱启,否則線程會(huì)阻塞丰辣,方法一旦執(zhí)行,就獨(dú)占該鎖禽捆,直到該方法返回才釋放鎖笙什,后面被阻塞的線程才能獲得這個(gè)鎖,繼續(xù)執(zhí)行
缺陷:若將一個(gè)大的方法申明為synchronized胚想,將會(huì)影響效率
注意:
- 方法里面需要修改內(nèi)容才需要鎖琐凭,鎖的太多浪費(fèi)資源,只讀內(nèi)容不需要加鎖
同步塊:
synchronized (Obj) {//同步代碼塊浊服,Obj:被鎖的對(duì)象统屈,方法丟在塊里
同步代碼塊胚吁,訪問共享數(shù)據(jù)
}
- Obj 稱之為 同步監(jiān)視器
- Obj可以是任何對(duì)象,但是推薦使用共享資源作為同步監(jiān)視器
- 同步方法中無需指定同步監(jiān)視器愁憔,因?yàn)橥椒椒ǖ耐奖O(jiān)視器就是this腕扶,就是這個(gè)對(duì)象本身,或者是class
- 同步監(jiān)視器的執(zhí)行過程
- 第一個(gè)線程訪問吨掌,鎖定同步監(jiān)視器半抱,執(zhí)行其中代碼
- 第二個(gè)線程訪問,發(fā)現(xiàn)同步監(jiān)視器被鎖定膜宋,無法訪問
- 第一個(gè)線程訪問完畢窿侈,解鎖同步監(jiān)視器
- 第二個(gè)線程訪問,發(fā)現(xiàn)沒鎖秋茫,然后鎖定訪問
注意:如果需要鎖的對(duì)象就是this史简,那就可以直接使用同步方法,就是在方法前加synchronized肛著,如果需要加鎖的對(duì)象不是this圆兵,而是其他對(duì)象,就寫同步塊枢贿,鎖定指定的對(duì)象殉农,然后將方法寫在塊中。(哪個(gè)對(duì)象的屬性進(jìn)行增刪改等修改了萨咕,就是需要鎖的對(duì)象)
同步方法鎖的粒度粗,并發(fā)效率低
同步代碼塊鎖的粒度細(xì)火本,并發(fā)效率高
臟讀
出現(xiàn)讀取屬性值出現(xiàn)一些意外危队,讀取的是中間值,而不是修改之后的值钙畔。
出現(xiàn)臟讀的原因是:對(duì)共享數(shù)據(jù)的修改 與對(duì)共享數(shù)據(jù)的讀取不同步
解決方法:對(duì)修改數(shù)據(jù)的代碼塊進(jìn)行同步茫陆,還要對(duì)讀取數(shù)據(jù)的代碼塊進(jìn)行同步
線程出現(xiàn)異常會(huì)自動(dòng)釋放鎖
死鎖:
多個(gè)線程各自占用一些共享資源,并且互相等待其他線程占有的資源才能運(yùn)行擎析,而導(dǎo)致兩個(gè)或多個(gè)線程都在等待對(duì)方釋放資源簿盅,都停止執(zhí)行的情形,某一個(gè)同步塊同時(shí)擁有“兩個(gè)以上對(duì)象的鎖”時(shí)揍魂,就可能發(fā)生<u>死鎖</u>問題
- 產(chǎn)生死鎖的四個(gè)必要條件:
- 互斥條件:一個(gè)資源每次只能被一個(gè)進(jìn)程使用
- 請(qǐng)求與保持條件:一個(gè)進(jìn)程因請(qǐng)求資源而阻塞時(shí)桨醋,對(duì)已獲得資源保持不放
- 不剝奪條件:進(jìn)程已獲得的資源,在未使用完之前现斋,不能強(qiáng)行剝奪
- 循環(huán)等待條件:若干進(jìn)程之間形成形成一種頭尾相接的循環(huán)等待資源關(guān)系
只需要破壞以上四個(gè)必要條件中的任意一個(gè)或多個(gè)喜最,就能避免死鎖發(fā)生。
輕量級(jí)同步機(jī)制:volatile關(guān)鍵字
volatile的作用
volatile關(guān)鍵的作用使變量在多個(gè)線程之間可見庄蹋∷材冢可以強(qiáng)制線程從公共內(nèi)存中讀取變量的值迷雪,而不是從工作內(nèi)存中讀取。
volatile與synchronized比較
- volatile關(guān)鍵字是線程同步的輕量級(jí)實(shí)現(xiàn)虫蝶,所以volatile性能肯定比synchronized要好章咧;volatile只能修飾變量,而synchronized還可以修飾方法能真,代碼塊赁严。
- 多線程訪問volatile變量不會(huì)發(fā)生阻塞,而synchronized可能會(huì)阻塞
- volatile能保證數(shù)據(jù)的可見性舟陆,但是不能保證原子性误澳;而synchronized可以保證原子性,也可以保證可見性
- 關(guān)鍵字volatile解決的是變量在多個(gè)線程之間的可見性秦躯;synchronized關(guān)鍵字解決多個(gè)線程之間訪問公共資源的同步性忆谓。
volatile非原子性
volatile關(guān)鍵字增加了實(shí)例變量在多個(gè)線程之間的可見性,但是它不具備原子性
常用的原子類進(jìn)行自增自減操作
i++操作不是原子操作踱承,所以不能保證線程安全倡缠,除了使用synchronized進(jìn)行同步外,也可以使用AtomicInteger/AtomicLong原子類進(jìn)行實(shí)現(xiàn)茎活。
CAS(Compare And Swap)
CAS是由硬件實(shí)現(xiàn)的昙沦。
CAS可以將read- modify - write這類的操作轉(zhuǎn)換為原子操作
CAS原理:
在把數(shù)據(jù)更新到主內(nèi)存時(shí),再次讀取主內(nèi)存變量的值载荔,如果現(xiàn)在變量的值與期望的值(操作起始時(shí)讀取的值)一樣就更新盾饮。
CAS實(shí)現(xiàn)原子操作背后有一個(gè)假設(shè):共享變量的當(dāng)前值與當(dāng)前線程提供的期望值相同,就認(rèn)為這個(gè)變量沒有被其他線程修改過懒熙。
但是丘损,實(shí)際上這種假設(shè)不一定總是成立。CAS會(huì)有ABA問題發(fā)生
如果想要規(guī)避ABA問題工扎,可以為共享變量引入一個(gè)修訂號(hào)(時(shí)間戳)徘钥,每次修改共享變量時(shí),相應(yīng)的修訂號(hào)就會(huì)增加1肢娘,每次對(duì)共享變量的修改都會(huì)導(dǎo)致修訂號(hào)的增加呈础,通過修訂號(hào)依然可以準(zhǔn)確判斷是否被其他線程修改過。AtomicStampedReference類就是基于這種思想產(chǎn)生的橱健。
原子變量類
原子變量類是基于CAS實(shí)現(xiàn)的而钞,當(dāng)對(duì)共享變量進(jìn)行read- modify - write更新操作時(shí),通過原子變量類可以保障操作的原子性和可見性拘荡。對(duì)變量的read- modify - write更新操作是指當(dāng)前操作不是一個(gè)簡(jiǎn)單的賦值笨忌,而是變量的新值依賴變量的舊值。由于volatile只能保障變量的可見性,無法保障原子性官疲,原子變量類內(nèi)部就是借助一個(gè)volatile變量袱结,并且保障了該變量的read- modify - write操作的原子性,有時(shí)把原子變量類看作增強(qiáng)的volatile變量途凫,原子變量類有12個(gè):
分組 | 原子變量類 |
---|---|
基礎(chǔ)數(shù)據(jù)型 | AtomicInteger垢夹,AtomicLong,AtomicBoolean |
數(shù)組型 | AtomicIntegerArray维费,AtomicLongArray果元,AtomicReferenceArray |
字段更新器 | AtomicIntegerFieldUpdater,AtomicLongFieldUpdater犀盟,AtomicReferenceFieldUpdater |
引用型 | AtomicReference而晒,AtomicStampedReference,AtomicMarkableReference |
ReentrantLock鎖
- 從JDK5.0開始阅畴,Java提供了強(qiáng)大的線程同步機(jī)制--通過顯示定義同步鎖對(duì)象老師先同步倡怎。同步鎖使用Lock對(duì)象充當(dāng)
- 鎖提供了對(duì)共享資源的獨(dú)占訪問,每次只能有一個(gè)線程對(duì)Lock對(duì)象加鎖贱枣,線程開始訪問共享資源之前應(yīng)先獲得Lock對(duì)象
- ReentrantLock 類實(shí)現(xiàn)了Lock监署,它擁有與synchronized 相同的并發(fā)性和內(nèi)存語義,在實(shí)現(xiàn)線程安全的控制中纽哥,比較常用的是ReentrantLock钠乏,可以顯式加鎖、釋放鎖春塌。(ReentrantLock 可重入鎖晓避,它的功能比synchronized多)
鎖的可重入性
鎖的可重入性是指,當(dāng)一個(gè)線程獲得一個(gè)對(duì)象鎖后只壳,再次請(qǐng)求該對(duì)象鎖時(shí)是可以獲得該對(duì)象鎖的俏拱。
//定義lock鎖
ReentrantLock lock = new ReentrantLock();
lock.lock() //加鎖
lock.unlock() //解鎖
lockInterruptibly()
lockInterruptibly()方法的作用:如果當(dāng)前線程未被中斷則獲得鎖,如果當(dāng)前線程被中斷則出現(xiàn)異常吕世。<u>可以解決死鎖問題</u>
lock.lock(); //獲得鎖定彰触,即使調(diào)用了線程的interrupt()方法梯投,線程也不會(huì)真正中斷
lock.lockInterruptibly(); //如果線程中斷了,不會(huì)獲得鎖,會(huì)發(fā)生異常
tryLock()方法
tryLock(long time,TimeUnit unit)的作用在給定等待時(shí)長(zhǎng)內(nèi)鎖沒有被另外的線程持有导犹,并且當(dāng)前線程也沒有中斷裹赴,則獲得該鎖,通過該方法可以實(shí)現(xiàn)鎖對(duì)象的限時(shí)等待么鹤。
tryLock()無參方法僅在調(diào)用時(shí)鎖定未被其他線程持有的鎖终娃,如果調(diào)用方法時(shí),鎖對(duì)象被其他線程持有蒸甜,則放棄棠耕。
<u>使用tryLock()可以避免死鎖問題</u>
newCondition()方法
關(guān)鍵字synchronized與wait()/notify()這兩個(gè)方法一起使用可以實(shí)現(xiàn)等待/通知模式余佛,Lock鎖的newCondition()方法返回Condition對(duì)象,Condition類也可以實(shí)現(xiàn)等待/通知模式窍荧。
使用notify()通知時(shí)辉巡,JVM會(huì)隨機(jī)喚醒某個(gè)等待的線程,使用Condition類則可以<u>進(jìn)行選擇性通知</u>蕊退。
Condition比較常用的兩個(gè)方法:
- await()會(huì)使當(dāng)前線程等待郊楣,同時(shí)會(huì)釋放鎖。 當(dāng)其他線程調(diào)用signal()時(shí)瓤荔,線程會(huì)重新獲得鎖并繼續(xù)執(zhí)行净蚤。
- signal()用于喚醒一個(gè)等待線程。
注意:
在調(diào)用Condition的await()/signal()方法前输硝,也需要線程持有相關(guān)的Lock鎖今瀑。調(diào)用await()方法后線程會(huì)釋放這個(gè)鎖,調(diào)用signal()方法后會(huì)從當(dāng)前的Condition對(duì)象的等待隊(duì)列中腔丧,喚醒一個(gè)線程放椰,喚醒的線程嘗試獲得鎖,一旦獲得鎖成功后就繼續(xù)執(zhí)行愉粤。
公平鎖和非公平鎖
大多數(shù)情況下砾医,鎖的申請(qǐng)都是非公平的,系統(tǒng)只會(huì)從阻塞隊(duì)列中隨機(jī)選擇一個(gè)線程衣厘,無法保證公平性如蚜。
公平的鎖會(huì)按照時(shí)間的先后順序,保證先到先得影暴,公平鎖這一特點(diǎn)不會(huì)出現(xiàn)線程饑餓的現(xiàn)象错邦。多個(gè)線程不會(huì)發(fā)生同一個(gè)線程連續(xù)多次獲得鎖的可能,保證鎖的公平性型宙。公平鎖看起來公平撬呢,但是要實(shí)現(xiàn)公平鎖必須要求系統(tǒng)維護(hù)一個(gè)有序隊(duì)列,所以公平鎖的實(shí)現(xiàn)成本較高妆兑,性能也較低魂拦,因此默認(rèn)情況下鎖是非公平的。
- synchronized內(nèi)部鎖就是非公平的搁嗓,ReentrantLock重入鎖提供了一個(gè)構(gòu)造方法:ReentrantLock(boolean fair)芯勘,當(dāng)在創(chuàng)建鎖對(duì)象時(shí)實(shí)參傳遞true就可以把該鎖設(shè)置為公平鎖
ReentrantLock常用方法
- int getHoldCount():返回當(dāng)前線程調(diào)用lock()方法的次數(shù)
- int getQueueLength():返回正等待獲得鎖的線程預(yù)估數(shù)
- int getWaitQueueLength(Condition condition):返回與Condition條件相關(guān)的等待的線程的預(yù)估數(shù)
- boolean hasQueuedThread(Thread thread):查詢參數(shù)指定的線程是否在等待獲得鎖
- boolean hasQueuedThreads():查詢是否還有線程在等待獲得該鎖
- boolean hasWaiters(Condition condition):查詢是否還有線程正在等待指定的Condition條件
- boolean isFair():判斷是否為公平鎖
- boolean isHeldByCurrentThread():判斷當(dāng)前線程是否持有該鎖
synchronized與Lock的對(duì)比
- Lock是顯式鎖(手動(dòng)開啟和關(guān)閉鎖,別忘記關(guān)閉鎖)腺逛,synchronized是隱式鎖荷愕,出了作用域自動(dòng)釋放
- Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
- 使用Lock鎖,JVM將花費(fèi)較少的時(shí)間來調(diào)度線程安疗,性能更好抛杨。并且具有更好的擴(kuò)展性(提供更多的子類)
- 使用優(yōu)先順序
- Lock > 同步代碼塊(已經(jīng)進(jìn)入了方法體,分配了相應(yīng)資源)> 同步方法(方法體外)
對(duì)于synchronized內(nèi)部鎖來說荐类,如果一個(gè)線程在等待鎖蝶桶,只有兩種結(jié)果:要么該線程獲得鎖繼續(xù)執(zhí)行,要么就保持等待
對(duì)于ReentrantLock可重入鎖來說掉冶,提供另外一種可能真竖,在等待鎖的過程中,程序可以根據(jù)需要取消對(duì)鎖的請(qǐng)求厌小。
ReentrantReadWriteLock讀寫鎖
synchronized內(nèi)部鎖和ReentrantLock鎖都是獨(dú)占鎖(排他鎖)恢共,同一時(shí)間只允許一個(gè)線程執(zhí)行同步代碼塊,可以保證線程的安全性璧亚,但是執(zhí)行效率低讨韭。
ReentrantReadWriteLock讀寫鎖是一種改進(jìn)的排他鎖,也可以稱作共享/排他鎖癣蟋。允許多個(gè)線程同時(shí)讀取共享數(shù)據(jù)透硝,但是一次只允許一個(gè)線程對(duì)共享數(shù)據(jù)進(jìn)行更新。
讀寫鎖通過讀鎖和寫鎖來完成讀寫操作疯搅。線程在讀取共享數(shù)據(jù)前必須先持有讀鎖濒生,該讀鎖可以同時(shí)被多個(gè)線程持有,即它是共享的幔欧。寫鎖是排他的罪治,線程在更新共享數(shù)據(jù)前必須先持有寫鎖, 一個(gè)線程持有寫鎖時(shí)其他線程線程無法獲得相應(yīng)的鎖礁蔗。
讀鎖只是在讀線程之間共享觉义,任何一個(gè)線程持有讀鎖時(shí),其他線程都無法持有寫鎖浴井,保證線程在讀取數(shù)據(jù)期間沒有其他線程對(duì)數(shù)據(jù)進(jìn)行更新晒骇,使得讀線程能夠讀取數(shù)據(jù)的最新值,保證讀數(shù)據(jù)期間共享變量不被修改
//定義讀寫鎖
ReadWriteLock rwlock = new ReentrantReadWriteLock();
//獲得讀鎖
Lock readLock = rwlock.readLock();
//獲得寫鎖
Lock writeLock = rwlock.writeLock();
//讀線程方法
readLock.lock();
try{
讀取數(shù)據(jù);
}finally{
readLock.unlock();
}
//寫線程方法
writeLock.lock();
try{
更新數(shù)據(jù)磺浙;
}finally{
writeLock.unlock();
}
等待/通知機(jī)制
Object類中的wait()方法可以使執(zhí)行當(dāng)前代碼的線程等待洪囤,暫停執(zhí)行,直到接到通知或被中斷為止屠缭。(會(huì)釋放鎖)
注意:
wait()方法只能在同步代碼塊中由鎖對(duì)象調(diào)用
-
調(diào)用wait()方法箍鼓,當(dāng)前線程會(huì)釋放鎖
Object類中的notify()可以喚醒線程崭参,該方法也必須在同步代碼塊中由鎖對(duì)象調(diào)用呵曹,沒有使用鎖對(duì)象調(diào)用wait()/notify()方法會(huì)拋出llegalMonitorStateException異常,如果有多個(gè)等待的線程,notify()方法只能喚醒其中一個(gè)奄喂。在同步代碼塊中調(diào)用notify()方法后铐殃,并不會(huì)立即釋放鎖對(duì)象,需要等當(dāng)前同步代碼塊執(zhí)行完后才會(huì)釋放鎖對(duì)象跨新,一般將notify()放在同步代碼塊的最后富腊。
interrupt()方法會(huì)中斷wait()等待
interrupt方法會(huì)中斷wait方法,會(huì)釋放鎖域帐。
wait(long)的使用
wait(long)帶有l(wèi)ong類型參數(shù)的wait()等待赘被,如果在參數(shù)指定的時(shí)間內(nèi)沒有被喚醒,超時(shí)后會(huì)自動(dòng)喚醒
通知過早問題
線程wait()等待后肖揣,可以調(diào)用notify()喚醒線程民假,如果notify()喚醒的過早,在等待之前就調(diào)用了龙优,notify()可能會(huì)打亂程序正常的運(yùn)行邏輯(就是喚醒線程在等待線程之前先執(zhí)行了羊异,導(dǎo)致等待線程無法被喚醒,可以定義一個(gè)靜態(tài)變量static boolean flag = true;
作為線程運(yùn)行的標(biāo)志彤断,在等待線程里加一個(gè)條件while(flag)
判斷線程狀態(tài)野舶,在喚醒線程中,將flag
改為false
宰衙,這樣如果先執(zhí)行喚醒線程平道,那等待線程也不會(huì)執(zhí)行)
wait等待條件發(fā)生了變化
在使用wait/notify模式時(shí),如果wait條件發(fā)生了變化供炼,也可能會(huì)造成邏輯的混亂
線程協(xié)作
- 在生產(chǎn)者消費(fèi)者問題中巢掺,僅有synchronized是不夠的
- synchronized 可阻止并發(fā)更新同一個(gè)共享資源,實(shí)現(xiàn)了同步
- synchronized 不能用來實(shí)現(xiàn)不同線程之間的消息傳遞(通信)
- java提供了幾個(gè)方法解決線程之間的通信問題
方法名 | 作用 |
---|---|
wait( ) | 表示線程會(huì)一直等待劲蜻,直到其他線程通知陆淀,與sleep不同,會(huì)釋放鎖 |
wait(long timeout) | 指定等待的毫秒數(shù) |
notify( ) | 喚醒一個(gè)處于扽等該狀態(tài)的線程 |
notifyAll( ) | 喚醒同一個(gè)對(duì)象上所有調(diào)用wait( )方法的線程先嬉,優(yōu)先級(jí)別高的線程優(yōu)先調(diào)度 |
注意:
均是Object類的方法轧苫,都只能在同步方法或者同步代碼塊中使用,否則會(huì)拋出異常IIIegaMonitorStateException
解決方式1
并發(fā)協(xié)作模式“生產(chǎn)者/消費(fèi)者”--> <u>管程法</u>
- 生產(chǎn)者:負(fù)責(zé)生成數(shù)據(jù)的模塊
- 消費(fèi)者:負(fù)責(zé)處理數(shù)據(jù)的模塊
- 緩沖區(qū):消費(fèi)者不能直接使用生產(chǎn)者的數(shù)據(jù)疫蔓,他們之間有個(gè)“緩沖區(qū)”
生產(chǎn)者將生產(chǎn)好的數(shù)據(jù)放入緩沖區(qū)中含懊,消費(fèi)者從緩沖區(qū)拿出數(shù)據(jù)
解決方式2
并發(fā)協(xié)作模式“生產(chǎn)者/消費(fèi)者模式”--> 信號(hào)燈法
生產(chǎn)者消費(fèi)者模式
在java中,負(fù)責(zé)產(chǎn)生數(shù)據(jù)的模塊是生產(chǎn)者衅胀,負(fù)責(zé)使用數(shù)據(jù)的模塊是消費(fèi)者岔乔,生產(chǎn)者消費(fèi)者解決數(shù)據(jù)的平衡問題,即先有數(shù)據(jù)然后才能使用滚躯,沒有數(shù)據(jù)時(shí)消費(fèi)者需要等待雏门。
- 生產(chǎn)-消費(fèi):操作數(shù)據(jù)
- 多生產(chǎn)-多消費(fèi):notify()不能保證是生產(chǎn)者喚醒消費(fèi)者嘿歌,如果生產(chǎn)者喚醒的還是生產(chǎn)者可能會(huì)出現(xiàn)假死的情況(所以喚醒操作要用notifyAll)
- 操作棧
通過管道實(shí)現(xiàn)線程間的通信
在java.io包中的PipeStream管道流用于在線程之間傳送數(shù)據(jù),一個(gè)線程發(fā)送數(shù)據(jù)到輸出管道茁影,另外一個(gè)線程從輸入管道中讀取數(shù)據(jù)宙帝。相關(guān)的類包括:PipedInputStream和PipedOutputStream,PipedReader和PipedWriter字符流募闲。
ThreadLocal的使用
除了控制資源的訪問外步脓,還可以通過增加資源來保證線程安全。ThreadLocal主要解決為每個(gè)線程綁定自己的值
ThreadLocal提供了線程內(nèi)存儲(chǔ)變量的能力浩螺,這些變量不同之處在于每一個(gè)線程讀取的變量是對(duì)應(yīng)的互相獨(dú)立的靴患。通過get和set方法就可以得到當(dāng)前線程對(duì)應(yīng)的值。
線程管理
線程組
Thread類有幾個(gè)構(gòu)造方法允許在創(chuàng)建線程時(shí)指定線程組要出,如果在創(chuàng)建線程時(shí)沒有制定線程組蚁廓,則該線程屬于父線程所在的線程組。JVM在創(chuàng)建main線程時(shí)會(huì)為他指定一個(gè)線程組厨幻,因此每個(gè)Java線程都有一個(gè)線程組與之關(guān)聯(lián)相嵌,可以調(diào)用線程的getThreadGroup()方法返回線程組。
捕獲線程的執(zhí)行異常
在線程的run方法中况脆,如果有受檢異常必須進(jìn)行捕獲處理饭宾,如果想要獲得run()方法中出現(xiàn)的運(yùn)行時(shí)異常信息,可以通過回調(diào)UncaughtExceptionhandler接口獲得哪個(gè)線程出現(xiàn)了運(yùn)行時(shí)異常格了。在Thread類中有關(guān)處理運(yùn)行時(shí)異常的方法有:
- getDefaultUncaughtExceptionhandler():獲得全局的(默認(rèn)的)UncaughtExceptionhandler
- getUncaughtExceptionhandler():獲得當(dāng)前線程的UncaughtExceptionhandler
- set
設(shè)置線程異晨疵回調(diào)接口
注入Hook勾子線程
很多軟件包括mysql、zookeeper盛末、Kafka等都存在Hook線程的校驗(yàn)機(jī)制弹惦,目的是校驗(yàn)進(jìn)程是否已啟動(dòng),防止重復(fù)啟動(dòng)程序悄但。
當(dāng)JVM退出時(shí)會(huì)執(zhí)行Hook線程棠隐,經(jīng)常在程序啟動(dòng)時(shí)創(chuàng)建一個(gè).lock文件,用.lock文件校驗(yàn)程序是否啟動(dòng)檐嚣,在程序退出(JVM退出)時(shí)刪除該.lock文件助泽,在Hook線程中除了防止重新啟動(dòng)進(jìn)程外,還可以做資源釋放嚎京,盡量避免在Hook線程中進(jìn)行復(fù)雜的操作嗡贺。
線程池
思路:提前創(chuàng)建好多個(gè)線程,放入線程池中鞍帝,使用時(shí)直接獲取诫睬,使用完放回池中,可以避免頻繁創(chuàng)建銷毀帕涌,實(shí)現(xiàn)重復(fù)利用摄凡。
-
好處:
- 提供響應(yīng)速度(減少了創(chuàng)建新線程的時(shí)間)
- 降低資源消耗(重復(fù)利用線程池中線程续徽,不需要每次都創(chuàng)建)
- 便于線程管理
- corePoolSize:核心池的大小
- maximumPoolsize:最大線程數(shù)
- keepAliveTime:線程沒有任務(wù)時(shí)最多保持多長(zhǎng)時(shí)間后會(huì)終止
//創(chuàng)建服務(wù),創(chuàng)建線程池
ExecutorService service = Executor.newFixedThreadPool(x); //x:線程數(shù)
//通過線程池執(zhí)行線程也可以架谎,通過start也可以
service.execute(new MyThread());
//關(guān)閉鏈接
service.shutdown();