01 前言
本章節(jié)主要分享下菊碟,多線程并發(fā)在電商系統(tǒng)下的應(yīng)用。主要從以下幾個(gè)方面深入:線程相關(guān)的基礎(chǔ)理論和工具、多線程程序下的性能調(diào)優(yōu)和電商場景下多線程的使用手蝎。
02 多線程
2.1 JU·C線程池
(1)概念
回顧線程創(chuàng)建的方式
- 繼承Thread
- 實(shí)現(xiàn)Runnable
- 使用FutureTask
線程狀態(tài)
NEW:剛剛創(chuàng)建,沒做任何操作
RUNNABLE:調(diào)用run蝉娜,可以執(zhí)行丙者,但不代表一定在執(zhí)行(RUNNING,READY)
WATING:使用了waite(),join()等方法
TIMED_WATING:使用了sleep(long),wait(long),join(long)等方法
BLOCKED:搶不到鎖
TERMINATED:終止
線程池基本概念
根據(jù)上面的狀態(tài),普通線程執(zhí)行完悬秉,就會(huì)進(jìn)入TERMINA TED銷毀掉榛斯,而線程池就是創(chuàng)建一個(gè)緩沖池存放線程,執(zhí)行結(jié)束以后搂捧,該線程并不會(huì)死亡驮俗,而是再次返回線程池中成為空閑狀態(tài),等候下次任務(wù)來臨允跑,這使得線程池比手動(dòng)創(chuàng)建線程有著更多的優(yōu)勢:
- 降低系統(tǒng)資源消耗王凑,通過重用已存在的線程,降低線程創(chuàng)建和銷毀造成的消耗聋丝;
- 提高系統(tǒng)響應(yīng)速度索烹,當(dāng)有任務(wù)到達(dá)時(shí),通過復(fù)用已存在的線程弱睦,無需等待新線程的創(chuàng)建便能立即執(zhí)行百姓;
- 方便線程并發(fā)數(shù)的管控。因?yàn)榫€程若是無限制的創(chuàng)建况木,可能會(huì)導(dǎo)致內(nèi)存占用過多而產(chǎn)生OOM
- 節(jié)省cpu切換線程的時(shí)間成本(需要保持當(dāng)前執(zhí)行線程的現(xiàn)場垒拢,并恢復(fù)要執(zhí)行線程的現(xiàn)場)。
- 提供更強(qiáng)大的功能火惊,延時(shí)定時(shí)線程池求类。(Timer vs ScheduledThreadPoolExecutor)
常用線程池類結(jié)構(gòu)
說明:
- 最常用的是ThreadPoolExecutor
- 調(diào)度用的ScheduledThreadPoolExecutor
- Executors是工具類,協(xié)助創(chuàng)建線程池
(2)工作機(jī)制
在線程池的編程模式下,任務(wù)是提交給整個(gè)線程池屹耐,而不是直接提交給某個(gè)線程尸疆,線程池在拿到任務(wù)后,就在內(nèi)部尋找是否有空閑的線程,如果有寿弱,則將任務(wù)交給某個(gè)空閑的線程犯眠。一個(gè)線程同時(shí)只能執(zhí)行一個(gè)任務(wù),但可以同時(shí)向一個(gè)線程池提交多個(gè)任務(wù)症革。
線程池狀態(tài)
- RUNNING:初始化狀態(tài)是RUNNING阔逼。線程池被一旦被創(chuàng)建,就處于RUNNING狀態(tài)地沮,并且線程池中的任務(wù)數(shù)為0嗜浮。RUNNING狀態(tài)下,能夠接收新任務(wù)摩疑,以及對(duì)已添加的任務(wù)進(jìn)行處理危融。
- SHUTDOWN:SHUTDOWN狀態(tài)時(shí),不接收新任務(wù)雷袋,但能處理已添加的任務(wù)吉殃。調(diào)用線程池的shutdown()接口時(shí),線程池由RUNNING -> SHUTDOWN楷怒。
//shutdown后不接受新任務(wù)蛋勺,但是task1,仍然可以執(zhí)行完成
ExecutorService poolExecutor = Executors.newFixedThreadPool(5);
poolExecutor.execute(new Runnable() {
public void run() {
try {
Thread.sleep(1000);
System.out.println("finish task 1");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
poolExecutor.shutdown();
poolExecutor.execute(new Runnable() {
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println("ok");
- STOP:不接收新任務(wù)鸠删,不處理已添加的任務(wù)抱完,并且會(huì)中斷正在處理的任務(wù)。調(diào)用線程池的shutdownNow()接口時(shí)刃泡,線程池由(RUNNING 或SHUTDOWN ) -> STOP
//改為shutdownNow后巧娱,任務(wù)立馬終止,sleep被打斷烘贴,新任務(wù)無法提交禁添,task1停止
poolExecutor.shutdownNow();
- TIDYING:所有的任務(wù)已終止,ctl記錄的”任務(wù)數(shù)量”為0桨踪,線程池會(huì)變?yōu)門IDYING老翘。線程池變?yōu)門IDYING狀態(tài)時(shí),會(huì)執(zhí)行鉤子函數(shù)terminated()锻离,可以通過重載terminated()函數(shù)來實(shí)現(xiàn)自定義行為
//自定義類铺峭,重寫terminated方法
public class MyExecutorService extends ThreadPoolExecutor {
public MyExecutorService(int corePoolSize, int maximumPoolSize, long
keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit,
workQueue);
}
@Override
protected void terminated() {
super.terminated();
System.out.println("treminated");
}
//調(diào)用 shutdownNow, ternimated方法被調(diào)用打印
public static void main(String[] args) throws InterruptedException {
MyExecutorService service = new
MyExecutorService(1,2,10000,TimeUnit.SECONDS,new
LinkedBlockingQueue<Runnable>(5));
service.shutdownNow();
}
}
- TERMINA TED:線程池處在TIDYING狀態(tài)時(shí)纳账,執(zhí)行完terminated()之后逛薇,就會(huì)由TIDYING ->TERMINA TED
(3)結(jié)構(gòu)說明
整理:最強(qiáng)“高并發(fā)”系統(tǒng)設(shè)計(jì) 46 連問,分分鐘秒殺一眾面試者
(4)任務(wù)提交流程
- 添加任務(wù)疏虫,如果線程池中的線程數(shù)沒有達(dá)到coreSize,會(huì)創(chuàng)建線程執(zhí)行任務(wù)
- 當(dāng)達(dá)到coreSize,把任務(wù)放workQueue中
- 當(dāng)queue滿了,未達(dá)maxsize創(chuàng)建心線程
- 線程數(shù)也達(dá)到maxsize,再添加任務(wù)會(huì)執(zhí)行reject策略
- 任務(wù)執(zhí)行完畢,超過keepactivetime,釋放超時(shí)的非核心線程,最終恢復(fù)到coresize大小
(5)源碼剖析
execute方法
//任務(wù)提交階段
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//判斷當(dāng)前workers中的線程數(shù)量有沒有超過核心線程數(shù)
if (workerCountOf(c) < corePoolSize) {
//如果沒有則創(chuàng)建核心線程數(shù)(參數(shù)true指的就是核心線程)
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果超過核心線程數(shù)了 先校驗(yàn)線程池是否正常運(yùn)行后向阻塞隊(duì)列workQueue末尾添加任務(wù)
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//再次檢查線程池運(yùn)行狀態(tài),若不在運(yùn)行則移除該任務(wù)并且執(zhí)行拒絕策略
if (! isRunning(recheck) && remove(command))
reject(command);
//若果沒有線程在執(zhí)行
else if (workerCountOf(recheck) == 0)
//則創(chuàng)建一個(gè)空的worker 該worker從隊(duì)列中獲取任務(wù)執(zhí)行
addWorker(null, false);
}
//否則直接添加非核心線程執(zhí)行任務(wù) 若非核心線程也添加失敗 則執(zhí)行拒絕策略
else if (!addWorker(command, false))
reject(command);
}
線程創(chuàng)建:addWorker()方法
//addWorker通過cas保證了并發(fā)安全性
private boolean addWorker(Runnable firstTask, boolean core) {
//第一部分 計(jì)數(shù)判斷,不符合返回false
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
//判斷線程數(shù),最大29位(CAPACITY=29位二進(jìn)制),所以設(shè)置線程池的線程數(shù)不是任意大的
if (wc >= CAPACITY ||
//判斷工作中的核心線程是否大于設(shè)置的核心線程或者設(shè)置的最大線程數(shù)
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//通過cas新增 若添加失敗會(huì)一直重試 若成功則跳過結(jié)束retry
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
//再次判斷運(yùn)行狀態(tài) 若運(yùn)行狀態(tài)改變則繼續(xù)重試
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
//第二部分:創(chuàng)建新的work放入works(一個(gè)hashSet)
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//將task任務(wù)封裝在新建的work中
w = new Worker(firstTask);
//獲取正在執(zhí)行該任務(wù)的線程
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
//將work加入到workers中
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
//上述work添加成功了,就開始執(zhí)行任務(wù)操作了
t.start();
workerStarted = true;
}
}
} finally {
//如果上述添加任務(wù)失敗了,會(huì)執(zhí)行移除該任務(wù)操作
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
獲取任務(wù)getTask()方法
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
//這里判斷是否要做超時(shí)處理,這里決定了當(dāng)前線程是否要被釋放
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//檢查當(dāng)前worker中線程數(shù)量是否超過max 并且上次循環(huán)poll等待超時(shí)了,則將隊(duì)列數(shù)量進(jìn)行原子性減
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
//線程可以被釋放,那就是poll,釋放時(shí)間就是keepAliveTime
//否則,線程不會(huì)被釋放,take一直阻塞在這里,直至新任務(wù)繼續(xù)工作
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
//到這里說明可被釋放的線程等待超時(shí)卧秘,已經(jīng)銷毀呢袱,設(shè)置該標(biāo)記,下次循環(huán)將線程數(shù)減少
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
(6)注意點(diǎn)
線程池是如何保證不被銷毀的
當(dāng)隊(duì)列中沒有任務(wù)時(shí),核心線程會(huì)一直阻塞獲取任務(wù)的方法,直至獲取到任務(wù)再次執(zhí)行
線程池中的線程會(huì)處于什么狀態(tài)
WAITING , TIMED_WAITING ,RUNNABLE
核心線程與非核心線程有本質(zhì)區(qū)別嗎翅敌?
答案:沒有羞福。被銷毀的線程和創(chuàng)建的先后無關(guān)。即便是第一個(gè)被創(chuàng)建的核心線程蚯涮,仍然有可能被銷毀
驗(yàn)證:看源碼治专,每個(gè)works在runWork的時(shí)候去getTask,在getTask內(nèi)部遭顶,并沒有針對(duì)性的區(qū)分當(dāng)前work是否是核心線程或者類似的標(biāo)記张峰。只要判斷works數(shù)量超出core,就會(huì)調(diào)用poll()棒旗,否則take()
2.2 鎖
(1)鎖的分類
1)樂觀鎖/悲觀鎖
樂觀鎖顧名思義喘批,很樂觀地認(rèn)為每次讀取數(shù)據(jù)的時(shí)候總是認(rèn)為沒人動(dòng)過,所以不去加鎖铣揉。但是在更新的時(shí)候回去對(duì)比一下原來的值饶深,看有沒有被別人更改過。適用于讀多寫少的場景逛拱。mysql中類比version號(hào)更新java中的atomic包屬于樂觀鎖實(shí)現(xiàn)敌厘,即CAS(下節(jié)會(huì)詳細(xì)介紹)
悲觀鎖在每次讀取數(shù)據(jù)的時(shí)候都認(rèn)為其他人會(huì)修改數(shù)據(jù),所以讀取數(shù)據(jù)的時(shí)候也加鎖朽合,這樣別人想拿的時(shí)候就會(huì)阻塞额湘,直到這個(gè)線程釋放鎖,這就影響了并發(fā)性能旁舰。適合寫操作比較多的場景锋华。mysql中類比for update。synchronized實(shí)現(xiàn)就是悲觀鎖(1.6之后優(yōu)化為鎖升級(jí)機(jī)制)箭窜,悲觀鎖書寫不當(dāng)很容易影響性能毯焕。
2)獨(dú)享鎖/共享鎖
很好理解,獨(dú)享鎖是指該鎖一次只能被一個(gè)線程所持有磺樱,而共享鎖是指該鎖可被多個(gè)線程所持有纳猫。
案例一:ReentrantLock,獨(dú)享鎖
public class PrivateLock {
Lock lock = new ReentrantLock();
long start = System.currentTimeMillis();
void read() {
lock.lock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
System.out.println("read time = "+(System.currentTimeMillis() - start));
}
public static void main(String[] args) {
final PrivateLock lock = new PrivateLock();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
lock.read();
}
}).start();
}
}
}
結(jié)果分析:每個(gè)線程結(jié)束的時(shí)間點(diǎn)逐個(gè)上升竹捉,鎖被獨(dú)享芜辕,一個(gè)用完下一個(gè),依次獲取鎖
案例二:ReadWriteLock块差,read共享侵续,write獨(dú)享
public class SharedLock {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock lock = readWriteLock.readLock();
long start = System.currentTimeMillis();
void read() {
lock.lock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
System.out.println("end time = "+(System.currentTimeMillis() - start));
}
public static void main(String[] args) {
final SharedLock lock = new SharedLock();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
lock.read();
}
}).start();
}
}
}
結(jié)果分析:每個(gè)線程獨(dú)自跑倔丈,各在100ms左右,證明是共享的
案例三:同樣是上例状蜗,換成writeLock
Lock lock = readWriteLock.writeLock();
結(jié)果分析:恢復(fù)到了1s時(shí)長需五,變?yōu)楠?dú)享
小結(jié):
- 讀鎖的共享鎖可保證并發(fā)讀是非常高效的,讀寫轧坎,寫讀 宏邮,寫寫的過程是互斥的。
- 獨(dú)享鎖與共享鎖也是通過AQS來實(shí)現(xiàn)的蜜氨,通過實(shí)現(xiàn)不同的方法,來實(shí)現(xiàn)獨(dú)享或者共享捎泻。
3)分段鎖
從Map一家子說起....
HashMap是線程不安全的飒炎,在多線程環(huán)境下,使用HashMap進(jìn)行put操作時(shí)族扰,可能會(huì)引起死循環(huán)厌丑,導(dǎo)致CPU利用率接近100%,所以在并發(fā)情況下不能使用HashMap渔呵。
于是有了HashT able怒竿,HashT able是線程安全的。但是HashT able線程安全的策略實(shí)在不怎么高明扩氢,將get/put所有相關(guān)操作都整成了synchronized的耕驰。
那有沒有辦法做到線程安全,又不這么粗暴呢录豺?基于分段鎖的ConcurrentHashMap誕生...
ConcurrentHashMap使用Segment(分段鎖)技術(shù)朦肘,將數(shù)據(jù)分成一段一段的存儲(chǔ),Segment數(shù)組的意義就是將一個(gè)大的table分割成多個(gè)小的table來進(jìn)行加鎖双饥,Segment數(shù)組中每一個(gè)元素一把鎖媒抠,每一個(gè)Segment元素存儲(chǔ)的是HashEntry數(shù)組+鏈表,這個(gè)和HashMap的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)一樣咏花。當(dāng)訪問其中一個(gè)段數(shù)據(jù)被某個(gè)線程加鎖的時(shí)候趴生,其他段的數(shù)據(jù)也能被其他線程訪問,這就使得ConcurrentHashMap不僅保證了線程安全昏翰,而且提高了性能苍匆。
但是這也引來一個(gè)負(fù)面影響:ConcurrentHashMap 定位一個(gè)元素的過程需要進(jìn)行兩次Hash操作,第一次Hash 定位到Segment棚菊,第二次Hash 定位到元素所在的鏈表浸踩。所以Hash 的過程比普通的HashMap 要長。
備注:JDK1.8ConcurrentHashMap中拋棄了原有的Segment 分段鎖统求,而采用了 CAS + synchronized來保證并發(fā)安全性检碗。
4)可重入鎖
可重入鎖指地獲取到鎖后据块,如果同步塊內(nèi)需要再次獲取同一把鎖的時(shí)候,直接放行后裸,而不是等待瑰钮。其意義在于防止死鎖冒滩。前面使用的synchronized 和ReentrantLock 都是可重入鎖微驶。
實(shí)現(xiàn)原理實(shí)現(xiàn)是通過為每個(gè)鎖關(guān)聯(lián)一個(gè)請(qǐng)求計(jì)數(shù)器和一個(gè)占有它的線程。如果同一個(gè)線程再次請(qǐng)求這個(gè)鎖开睡,計(jì)數(shù)器將遞增因苹,線程退出同步塊,計(jì)數(shù)器值將遞減篇恒。直到計(jì)數(shù)器為0鎖被釋放扶檐。
場景見于父類和子類的鎖的重入(調(diào)super方法),以及多個(gè)加鎖方法的嵌套調(diào)用胁艰。
案例一:父子可重入
public class ParentLock {
byte[] lock = new byte[0];
public void f1(){
synchronized (lock){
System.out.println("f1 from parent");
}
}
}
public class SonLock extends ParentLock {
public void f1() {
synchronized (super.lock){
super.f1();
System.out.println("f1 from son");
}
}
public static void main(String[] args) {
SonLock lock = new SonLock();
lock.f1();
}
}
案例二:內(nèi)嵌方法可重入
public class NestedLock {
public synchronized void f1(){
System.out.println("f1");
}
public synchronized void f2(){
f1();
System.out.println("f2");
}
public static void main(String[] args) {
NestedLock lock = new NestedLock();
//可以正常打印 f1,f2
lock.f2();
}
}
5)公平鎖/非公平鎖
基本概念:
公平鎖就是在并發(fā)環(huán)境中款筑,每個(gè)線程在獲取鎖時(shí)會(huì)先查看此鎖維護(hù)的等待隊(duì)列,如果為空腾么,或者當(dāng)前線程是等待隊(duì)列的第一個(gè)奈梳,就占有鎖,否則就會(huì)加入到等待隊(duì)列中解虱,直到按照FIFO的規(guī)則從隊(duì)列中取到自己攘须。
非公平鎖與公平鎖基本類似,只是在放入隊(duì)列前先判斷當(dāng)前鎖是否被線程持有殴泰。如果鎖空閑于宙,那么他可以直接搶占,而不需要判斷當(dāng)前隊(duì)列中是否有等待線程悍汛。只有鎖被占用的話捞魁,才會(huì)進(jìn)入排隊(duì)。在現(xiàn)實(shí)中想象一下游樂場旋轉(zhuǎn)木馬插隊(duì)現(xiàn)象......
優(yōu)缺點(diǎn):
公平鎖的優(yōu)點(diǎn)是等待鎖的線程不會(huì)餓死离咐,進(jìn)入隊(duì)列規(guī)規(guī)矩矩的排隊(duì)谱俭,遲早會(huì)輪到。缺點(diǎn)是整體吞吐效率相對(duì)非公平鎖要低健霹,等待隊(duì)列中除第一個(gè)線程以外的所有線程都會(huì)阻塞旺上,CPU喚醒阻塞線程的開銷比非公平鎖大。
非公平鎖的性能要高于公平鎖糖埋,因?yàn)榫€程有幾率不阻塞直接獲得鎖宣吱。ReentrantLock默認(rèn)使用非公平鎖就是基于性能考量。但是非公平鎖的缺點(diǎn)是可能引發(fā)隊(duì)列中的線程始終拿不到鎖瞳别,一直排隊(duì)被餓死征候。
編碼方式:
很簡單杭攻,ReentrantLock支持創(chuàng)建公平鎖和非公平鎖(默認(rèn)),想要實(shí)現(xiàn)公平鎖疤坝,使用new ReentrantLock(true)兆解。
背后原理:
AQS,后面還會(huì)詳細(xì)講到跑揉。AQS中有一個(gè)state標(biāo)識(shí)鎖的占用情況锅睛,一個(gè)隊(duì)列存儲(chǔ)等待線程。
state=0表示鎖空閑历谍。如果是公平鎖现拒,那就看看隊(duì)列有沒有線程在等,有的話不參與競爭乖乖追加到尾部望侈。如果是非公平鎖印蔬,那就直接參與競爭,不管隊(duì)列有沒有等待者脱衙。
state>0表示有線程占著鎖侥猬,這時(shí)候無論公平與非公平,都直接去排隊(duì)(想搶也沒有)
備注:
因?yàn)镽eentrantLock是可重入鎖捐韩,數(shù)量表示重入的次數(shù)退唠。所以是>0而不是簡單的0和1而synchronized只能是非公平鎖
6)鎖升級(jí)
java中每個(gè)對(duì)象都可作為鎖,鎖有四種級(jí)別奥帘,按照量級(jí)從輕到重分為:無鎖铜邮、偏向鎖、輕量級(jí)鎖寨蹋、重量級(jí)鎖松蒜。
如何理解呢?A占了鎖已旧,B就要阻塞等秸苗。但是,在操作系統(tǒng)中运褪,阻塞就要存儲(chǔ)當(dāng)前線程狀態(tài)惊楼,喚醒就要再恢復(fù),這個(gè)過程是要消耗時(shí)間的...
如果A使用鎖的時(shí)間遠(yuǎn)遠(yuǎn)小于B被阻塞和掛起的執(zhí)行時(shí)間秸讹,那么我們將B掛起阻塞就相當(dāng)?shù)牟缓纤恪?/p>
于是出現(xiàn)自旋:自旋指的是鎖已經(jīng)被其他線程占用時(shí)檀咙,當(dāng)前線程不會(huì)被掛起,而是在不停的試圖獲取鎖(可以理解為不停的循環(huán))璃诀,每循環(huán)一次表示一次自旋過程弧可。顯然這種操作會(huì)消耗CPU時(shí)間,但是相比線程下文切換時(shí)間要少的時(shí)候劣欢,自旋劃算棕诵。
而偏向鎖裁良、輕量鎖、重量鎖就是圍繞如何使得cpu的占用更劃算而展開的校套。
舉個(gè)生活的例子价脾,假設(shè)公司只有一個(gè)會(huì)議室(共享資源)
偏向鎖:
前期公司只有1個(gè)團(tuán)隊(duì),那么什么時(shí)候開會(huì)都能滿足笛匙,就不需要詢問和查看會(huì)議室的占用情況侨把,直接進(jìn)入使用
狀態(tài)。會(huì)議室門口掛了個(gè)牌子寫著A使用膳算,A默認(rèn)不需要預(yù)約(ThreadID=A)
輕量級(jí)鎖:
隨著業(yè)務(wù)發(fā)展座硕,擴(kuò)充為2個(gè)團(tuán)隊(duì)弛作,B團(tuán)隊(duì)肯定不會(huì)同意A無法無天涕蜂,于是當(dāng)AB同時(shí)需要開會(huì)時(shí),兩者競爭映琳,誰搶
到誰算誰的机隙。偏向鎖升級(jí)為輕量級(jí)鎖,但是未搶到者在門口會(huì)不停敲門詢問(自旋萨西,循環(huán))有鹿,開完沒有?開完
沒有谎脯?
重量級(jí)鎖:
后來發(fā)現(xiàn)葱跋,這種不停敲門的方式很煩,A可能不理不睬源梭,但是B要不停的鬧騰娱俺。于是鎖再次升級(jí)。
如果會(huì)議室被A占用废麻,那么B團(tuán)隊(duì)直接閉嘴荠卷,在門口安靜的等待(wait進(jìn)入阻塞),直到A用完后會(huì)通知
B(notify)烛愧。
注意點(diǎn):
- 上面幾種鎖都是JVM自己內(nèi)部實(shí)現(xiàn)油宜,我們不需要干預(yù),但是可以配置jvm參數(shù)開啟/關(guān)閉自旋鎖怜姿、偏
向鎖慎冤。 - 鎖可以升級(jí),但是不能反向降級(jí):偏向鎖→輕量級(jí)鎖→重量級(jí)鎖
- 無鎖爭用的時(shí)候使用偏向鎖沧卢,第二個(gè)線程到了升級(jí)為輕量級(jí)鎖進(jìn)行競爭蚁堤,更多線程時(shí),進(jìn)入重量級(jí)鎖阻塞
7) 互斥鎖/讀寫鎖
典型的互斥鎖:synchronized搏恤,ReentrantLock违寿,讀寫鎖:ReadWriteLock 前面都用過了互斥鎖屬于獨(dú)享鎖湃交,讀寫鎖里的寫鎖屬于獨(dú)享鎖,而讀鎖屬于共享鎖
案例:互斥鎖用不好可能會(huì)失效藤巢,看一個(gè)典型的鎖不住現(xiàn)象搞莺!
public class ObjectLock {
public static Integer i=0;
public void inc(){
synchronized (this){
int j=i;
try {
Thread.sleep(100);
j++;
} catch (InterruptedException e) {
e.printStackTrace();
}
i=j;
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
//重點(diǎn)!
new ObjectLock().inc();
}
}).start();
}
Thread.sleep(3000);
//理論上10才對(duì)掂咒〔挪祝可是....
System.out.println(ObjectLock.i);
}
}
結(jié)果分析:每個(gè)線程內(nèi)都是new對(duì)象,所以this不是同一把鎖绍刮,結(jié)果鎖不住温圆,輸出1
1.this,換成static的i 變量試試孩革?
2.換成ObjectLock.class 試試岁歉?
3.換成String.class
4.去掉synchronized塊,外部方法上加static synchronized
2.3 原子操作(atomic)
(1)概念
原子(atom)本意是“不能被進(jìn)一步分割的最小粒子”膝蜈,而原子操作(atomic operation)意為"不可被中斷的一個(gè)或一系列操作" 锅移。類比于數(shù)據(jù)庫事務(wù),redis的multi饱搏。
(2)CAS
Compare And Set(或Compare And Swap)非剃,翻譯過來就是比較并替換,CAS操作包含三個(gè)操作數(shù)——內(nèi)存位置(V)推沸、預(yù)期原值(A)备绽、新值(B)。從第一視角來看鬓催,理解為:我認(rèn)為位置V 應(yīng)該是A肺素,如果是A,則將B 放到這個(gè)位置深浮;否則压怠,不要更改,只告訴我這個(gè)位置現(xiàn)在的值即可飞苇。
計(jì)數(shù)器問題發(fā)生歸根結(jié)底是取值和運(yùn)算后的賦值中間菌瘫,發(fā)生了插隊(duì)現(xiàn)象,他們不是原子的操作布卡。前面的計(jì)數(shù)器使用加鎖方式實(shí)現(xiàn)了正確計(jì)數(shù)雨让,下面,基于CAS的原子類上場....
public class AtomicCounter {
private static AtomicInteger i = new AtomicInteger(0);
public int get(){
return i.get();
}
public void inc(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
i.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
final AtomicCounter counter = new AtomicCounter();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
counter.inc();
}
}).start();
}
Thread.sleep(3000);
//同樣可以正確輸出10
System.out.println(counter.i.get());
}
}
(3)atomic
上面展示了AtomicInteger忿等,關(guān)于atomic包栖忠,還有很多其他類型:
(4)基本類型
AtomicBoolean:以原子更新的方式更新boolean;
AtomicInteger:以原子更新的方式更新Integer;
AtomicLong:以原子更新的方式更新Long;
(5)引用類型
AtomicReference : 原子更新引用類型
AtomicReferenceFieldUpdater :原子更新引用類型的字段
AtomicMarkableReference : 原子更新帶有標(biāo)志位的引用類型
(6)數(shù)組
AtomicIntegerArray:原子更新整型數(shù)組里的元素庵寞。
AtomicLongArray:原子更新長整型數(shù)組里的元素狸相。
AtomicReferenceArray:原子更新引用類型數(shù)組里的元素。
(7)字段
AtomicIntegerFieldUpdater:原子更新整型的字段的更新器捐川。
AtomicLongFieldUpdater:原子更新長整型字段的更新器脓鹃。
AtomicStampedReference:原子更新帶有版本號(hào)的引用類型。
(8)注意
使用atomic要注意原子性的邊界古沥,把握不好會(huì)起不到應(yīng)有的效果瘸右,原子性被破壞。
public class BadAtomic {
AtomicInteger i = new AtomicInteger(0);
static int j=0;
public void badInc(){
int k = i.incrementAndGet();
try {
Thread.currentThread().sleep(new Random().nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
j=k;
}
public static void main(String[] args) throws InterruptedException {
BadAtomic atomic = new BadAtomic();
for (int i = 0; i < 10; i++) {
new Thread(()->{
atomic.badInc();
}).start();
}
Thread.sleep(3000);
System.out.println(atomic.j);
}
}
結(jié)果分析:
每次都不一樣岩齿,總之不是10
在badInc上加synchronized太颤,問題解決
這章節(jié)目前就介紹這么多,后續(xù)將擴(kuò)展更多的多線程相關(guān)的類,以及從項(xiàng)目中解讀多線程的應(yīng)用。