面試問題
- synchronized的原理
- synchronized優(yōu)化后的鎖機制簡單介紹一下,包括自旋鎖晴股、偏向鎖羽峰、輕量級鎖、重量級鎖
- 談?wù)剬ynchronized關(guān)鍵字涉及到的類鎖园蝠,方法鎖渺蒿,重入鎖的理解
- wait、sleep的區(qū)別和notify運行過程
- synchronized關(guān)鍵字和Lock的區(qū)別彪薛,為什么Lock的性能好一些
- volatile原理
- synchronized 和 volatile 關(guān)鍵字的作用和區(qū)別
- ReentrantLock的內(nèi)部實現(xiàn)
- 多線程的使用場景
- 什么是線程池茂装,如何使用怠蹂,為什么要使用線程池
- Java中的線程池共有幾種
- 線程池的原理
- 線程池都有哪幾種工作隊列
- 怎么理解無界隊列和有界隊列
- 多線程中的安全隊列一般通過什么實現(xiàn)
synchronized的原理
synchronized 代碼塊是由一對兒 monitorenter/monitorexit 指令實現(xiàn)的,Monitor 對象是同步的基本實現(xiàn)少态,而 synchronized 同步方法使用了ACC_SYNCHRONIZED訪問標志來辨別一個方法是否聲明為同步方法城侧,從而執(zhí)行相應(yīng)的同步調(diào)用。
JVM 提供了三種不同的 Monitor 實現(xiàn)彼妻,也就是常說的三種不同的鎖:偏斜鎖(Biased Locking)嫌佑、輕量級鎖和重量級鎖,大大改進了其性能侨歉。
所謂鎖的升級屋摇、降級,就是 JVM 優(yōu)化 synchronized 運行的機制幽邓,當(dāng) JVM 檢測到不同的競爭狀況時炮温,會自動切換到適合的鎖實現(xiàn),這種切換就是鎖的升級颊艳、降級茅特。
當(dāng)沒有競爭出現(xiàn)時,默認會使用偏斜鎖棋枕。JVM 會利用 CAS 操作白修,在對象頭上的 Mark Word 部分設(shè)置線程 ID,以表示這個對象偏向于當(dāng)前線程重斑,所以并不涉及真正的互斥鎖兵睛。這樣做的假設(shè)是基于在很多應(yīng)用場景中,大部分對象生命周期中最多會被一個線程鎖定窥浪,使用偏斜鎖可以降低無競爭開銷祖很。
如果有另外的線程試圖鎖定某個已經(jīng)被偏斜過的對象,JVM 就需要撤銷(revoke)偏斜鎖漾脂,并切換到輕量級鎖實現(xiàn)假颇。輕量級鎖依賴 CAS 操作 Mark Word 來試圖獲取鎖,如果重試成功骨稿,就使用普通的輕量級鎖笨鸡;否則,進一步升級為重量級鎖(可能會先進行自旋鎖升級坦冠,如果失敗再嘗試重量級鎖升級)形耗。
synchronized優(yōu)化后的鎖機制簡單介紹一下,包括自旋鎖辙浑、偏向鎖激涤、輕量級鎖、重量級鎖
自旋鎖:
線程自旋說白了就是讓cpu在做無用功判呕,比如:可以執(zhí)行幾次for循環(huán)倦踢,可以執(zhí)行幾條空的匯編指令送滞,目的是占著CPU不放,等待獲取鎖的機會硼一。如果旋的時間過長會影響整體性能累澡,時間過短又達不到延遲阻塞的目的。
偏向鎖:
偏向鎖就是一旦線程第一次獲得了監(jiān)視對象般贼,之后讓監(jiān)視對象“偏向”這個線程,之后的多次調(diào)用則可以避免CAS操作奥吩,說白了就是置個變量哼蛆,如果發(fā)現(xiàn)為true則無需再走各種加鎖/解鎖流程。
輕量級鎖:
輕量級鎖是由偏向所升級來的霞赫,偏向鎖運行在一個線程進入同步塊的情況下腮介,當(dāng)?shù)诙€線程加入鎖競爭用的時候,偏向鎖就會升級為輕量級鎖端衰;
重量級鎖:
重量鎖在JVM中又叫對象監(jiān)視器(Monitor)叠洗,它很像C中的Mutex,除了具備Mutex(0|1)互斥的功能旅东,它還負責(zé)實現(xiàn)了Semaphore(信號量)的功能灭抑,也就是說它至少包含一個競爭鎖的隊列,和一個信號阻塞隊列(wait隊列)抵代,前者負責(zé)做互斥腾节,后一個用于做線程同步。
談?wù)剬ynchronized關(guān)鍵字涉及到的類鎖荤牍,方法鎖案腺,重入鎖的理解
synchronized修飾靜態(tài)方法獲取的是類鎖(類的字節(jié)碼文件對象)。
synchronized修飾普通方法或代碼塊獲取的是對象鎖康吵。這種機制確保了同一時刻對于每一個類實例劈榨,其所有聲明為 synchronized 的成員函數(shù)中至多只有一個處于可執(zhí)行狀態(tài),從而有效避免了類成員變量的訪問沖突晦嵌。
它倆是不沖突的同辣,也就是說:獲取了類鎖的線程和獲取了對象鎖的線程是不沖突的!
wait耍铜、sleep的區(qū)別和notify運行過程
wait邑闺、sleep的區(qū)別:
最大的不同是在等待時 wait 會釋放鎖,而 sleep 一直持有鎖棕兼。wait 通常被用于線程間交互陡舅,sleep 通常被用于暫停執(zhí)行。
- 首先伴挚,要記住這個差別靶衍,“sleep是Thread類的方法,wait是Object類中定義的方法”灾炭。盡管這兩個方法都會影響線程的執(zhí)行行為,但是本質(zhì)上是有區(qū)別的颅眶。
- Thread.sleep不會導(dǎo)致鎖行為的改變蜈出,如果當(dāng)前線程是擁有鎖的,那么Thread.sleep不會讓線程釋放鎖涛酗。如果能夠幫助你記憶的話铡原,可以簡單認為和鎖相關(guān)的方法都定義在Object類中,因此調(diào)用Thread.sleep是不會影響鎖的相關(guān)行為商叹。
- Thread.sleep和Object.wait都會暫停當(dāng)前的線程燕刻,對于CPU資源來說,不管是哪種方式暫停的線程剖笙,都表示它暫時不再需要CPU的執(zhí)行時間卵洗。OS會將執(zhí)行時間分配給其它線程。區(qū)別是弥咪,調(diào)用wait后过蹂,需要別的線程執(zhí)行notify/notifyAll才能夠重新獲得CPU執(zhí)行時間。
線程的狀態(tài)參考 Thread.State的定義聚至。新創(chuàng)建的但是沒有執(zhí)行(還沒有調(diào)用start())的線程處于“就緒”酷勺,或者說Thread.State.NEW狀態(tài)。 - Thread.State.BLOCKED(阻塞)表示線程正在獲取鎖時晚岭,因為鎖不能獲取到而被迫暫停執(zhí)行下面的指令鸥印,一直等到這個鎖被別的線程釋放。BLOCKED狀態(tài)下線程坦报,OS調(diào)度機制需要決定下一個能夠獲取鎖的線程是哪個库说,這種情況下,就是產(chǎn)生鎖的爭用片择,無論如何這都是很耗時的操作潜的。
notify運行過程:
當(dāng)線程A(消費者)調(diào)用wait()方法后,線程A讓出鎖字管,自己進入等待狀態(tài)啰挪,同時加入鎖對象的等待隊列。
線程B(生產(chǎn)者)獲取鎖后嘲叔,調(diào)用notify方法通知鎖對象的等待隊列亡呵,使得線程A從等待隊列進入阻塞隊列。
線程A進入阻塞隊列后硫戈,直至線程B釋放鎖后锰什,線程A競爭得到鎖繼續(xù)從wait()方法后執(zhí)行。
synchronized關(guān)鍵字和Lock的區(qū)別,為什么Lock的性能好一些
Lock(ReentrantLock)的底層實現(xiàn)主要是Volatile + CAS(樂觀鎖)汁胆,而Synchronized是一種悲觀鎖梭姓,比較耗性能。但是在JDK1.6以后對Synchronized的鎖機制進行了優(yōu)化嫩码,加入了偏向鎖誉尖、輕量級鎖、自旋鎖铸题、重量級鎖铡恕,在并發(fā)量不大的情況下,性能可能優(yōu)于Lock機制回挽。所以建議一般請求并發(fā)量不大的情況下使用synchronized關(guān)鍵字没咙。
volatile原理
而volatile關(guān)鍵字就是Java中提供的另一種解決可見性有序性問題的方案。對于原子性千劈,需要強調(diào)一點,也是大家容易誤解的一點:對volatile變量的單次讀/寫操作可保證原子性的牌捷,如long和double類型變量墙牌,但是并不能保證i++這種操作的原子性,因為本質(zhì)上i++是讀暗甥、寫兩次操作喜滨。
volatile也是互斥同步的一種實現(xiàn),不過它非常的輕量級撤防。
volatile可以防止CPU指令重排序虽风,保證被volatile修飾的變量對所有線程都是可見的。
被volatile修飾的變量在工作內(nèi)存修改后會被強制寫回主內(nèi)存寄月,其他線程在使用時也會強制從主內(nèi)存刷新辜膝,這樣就保證了一致性。
什么是指令重排序:
- 指令重排序是指指令亂序執(zhí)行漾肮,即在條件允許的情況下直接運行當(dāng)前有能力立即執(zhí)行的后續(xù)指令厂抖,避開為獲取一條指令所需數(shù)據(jù)而造成的等待,通過亂序執(zhí)行的技術(shù)提供執(zhí)行效率克懊。
- 指令重排序會在被volatile修飾的變量的賦值操作前忱辅,添加一個內(nèi)存屏障,指令重排序時不能把后面的指令重排序移到內(nèi)存屏障之前的位置褒繁。
synchronized 和 volatile 關(guān)鍵字的作用和區(qū)別
- volatile 僅能使用在變量級別翰蠢;synchronized則可以使用在變量匆绣、方法、和類級別的损搬。
- volatile 僅能實現(xiàn)變量的修改可見性,并不能保證原子性;synchronized 則可以保證變量的修改可見性和原子性场躯。
- volatile 不會造成線程的阻塞谈为;synchronized 可能會造成線程的阻塞。
- volatile 標記的變量不會被編譯器優(yōu)化踢关;synchronized標記的變量可以被編譯器優(yōu)化伞鲫。
ReentrantLock的內(nèi)部實現(xiàn)
ReentrantLock實現(xiàn)的前提就是AbstractQueuedSynchronizer,簡稱AQS签舞,是java.util.concurrent的核心秕脓,CountDownLatch、FutureTask儒搭、Semaphore吠架、ReentrantLock等都有一個內(nèi)部類是這個抽象類的子類。由于AQS是基于FIFO隊列的實現(xiàn)搂鲫,因此必然存在一個個節(jié)點傍药,Node就是一個節(jié)點,Node有兩種模式:共享模式和獨占模式魂仍。ReentrantLock是基于AQS的拐辽,AQS是Java并發(fā)包中眾多同步組件的構(gòu)建基礎(chǔ),它通過一個int類型的狀態(tài)變量state和一個FIFO隊列來完成共享資源的獲取擦酌,線程的排隊等待等俱诸。AQS是個底層框架,采用模板方法模式赊舶,它定義了通用的較為復(fù)雜的邏輯骨架睁搭,比如線程的排隊,阻塞笼平,喚醒等园骆,將這些復(fù)雜但實質(zhì)通用的部分抽取出來,這些都是需要構(gòu)建同步組件的使用者無需關(guān)心的出吹,使用者僅需重寫一些簡單的指定的方法即可(其實就是對于共享變量state的一些簡單的獲取釋放的操作)遇伞。AQS的子類一般只需要重寫tryAcquire(int arg)和tryRelease(int arg)兩個方法即可。
ReentrantLock的處理邏輯:
其內(nèi)部定義了三個重要的靜態(tài)內(nèi)部類捶牢,Sync鸠珠,NonFairSync,F(xiàn)airSync秋麸。Sync作為ReentrantLock中公用的同步組件渐排,繼承了AQS(要利用AQS復(fù)雜的頂層邏輯嘛,線程排隊灸蟆,阻塞驯耻,喚醒等等);NonFairSync和FairSync則都繼承Sync,調(diào)用Sync的公用邏輯可缚,然后再在各自內(nèi)部完成自己特定的邏輯(公平或非公平)霎迫。
NonFairSync(非公平可重入鎖):
- 先獲取state值,若為0帘靡,意味著此時沒有線程獲取到資源知给,CAS將其設(shè)置為1,設(shè)置成功則代表獲取到排他鎖了描姚;
- 若state大于0涩赢,肯定有線程已經(jīng)搶占到資源了,此時再去判斷是否就是自己搶占的轩勘,是的話筒扒,state累加,返回true绊寻,重入成功花墩,state的值即是線程重入的次數(shù);
- 其他情況澄步,則獲取鎖失敗观游。
FairSync(公平可重入鎖):
可以看到,公平鎖的大致邏輯與非公平鎖是一致的驮俗,不同的地方在于有了!hasQueuedPredecessors()這個判斷邏輯,即便state為0允跑,也不能貿(mào)然直接去獲取王凑,要先去看有沒有還在排隊的線程,若沒有聋丝,才能嘗試去獲取索烹,做后面的處理。反之弱睦,返回false百姓,獲取失敗。
ReentrantLock的tryRelease()方法實現(xiàn)原理:
若state值為0况木,表示當(dāng)前線程已完全釋放干凈垒拢,返回true,上層的AQS會意識到資源已空出火惊。若不為0求类,則表示線程還占有資源,只不過將此次重入的資源的釋放了而已屹耐,返回false尸疆。
ReentrantLock是一種可重入的,可實現(xiàn)公平性的互斥鎖,它的設(shè)計基于AQS框架寿弱,可重入和公平性的實現(xiàn)邏輯都不難理解犯眠,每重入一次,state就加1症革,當(dāng)然在釋放的時候筐咧,也得一層一層釋放。至于公平性地沮,在嘗試獲取鎖的時候多了一個判斷:是否有比自己申請早的線程在同步隊列中等待嗜浮,若有,去等待摩疑;若沒有危融,才允許去搶占。
多線程的使用場景
使用多線程就一定效率高嗎雷袋?有時候使用多線程并不是為了提高效率吉殃,而是使得CPU能同時處理多個事件。
- 為了不阻塞主線程,啟動其他線程來做事情,比如APP中的耗時操作都不在UI線程中做楷怒。
- 實現(xiàn)更快的應(yīng)用程序,即主線程專門監(jiān)聽用戶請求,子線程用來處理用戶請求,以獲得大的吞吐量.感覺這種情況蛋勺,多線程的效率未必高。這種情況下的多線程是為了不必等待鸠删,可以并行處理多條數(shù)據(jù)抱完。比如JavaWeb的就是主線程專門監(jiān)聽用戶的HTTP請求,然啟動子線程去處理用戶的HTTP請求刃泡。
- 某種雖然優(yōu)先級很低的服務(wù)巧娱,但是卻要不定時去做。比如Jvm的垃圾回收烘贴。
- 某種任務(wù)禁添,雖然耗時,但是不消耗CPU的操作時間桨踪,開啟個線程老翘,效率會有顯著提高。比如讀取文件锻离,然后處理铺峭。磁盤IO是個很耗費時間,但是不耗CPU計算的工作纳账。所以可以一個線程讀取數(shù)據(jù)逛薇,一個線程處理數(shù)據(jù)∈璩妫肯定比一個線程讀取數(shù)據(jù)永罚,然后處理效率高啤呼。因為兩個線程的時候充分利用了CPU等待磁盤IO的空閑時間。
什么是線程池呢袱,如何使用官扣,為什么要使用線程池
線程池就是事先將多個線程對象放到一個容器中,使用的時候就不用new線程而是直接去池中拿線程即可羞福,節(jié)省了開辟子線程的時間惕蹄,提高了代碼執(zhí)行效率。
Java中的線程池共有幾種
Java有四種線程池:
- newCachedThreadPool
不固定線程數(shù)量治专,且支持最大為Integer.MAX_VALUE的線程數(shù)量卖陵。
可緩存線程池:
1、線程數(shù)無限制张峰。 2泪蔫、有空閑線程則復(fù)用空閑線程,若無空閑線程則新建線程喘批。 3撩荣、一定程序減少頻繁創(chuàng)建/銷毀線程,減少系統(tǒng)開銷饶深。
public static ExecutorService newCachedThreadPool() {
// 這個線程池corePoolSize為0餐曹,maximumPoolSize為Integer.MAX_VALUE
// 意思也就是說來一個任務(wù)就創(chuàng)建一個woker,回收時間是60s
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- newFixedThreadPool
一個固定線程數(shù)量的線程池敌厘。
定長線程池:
1台猴、可控制線程最大并發(fā)數(shù)(同時執(zhí)行的線程數(shù))。 2俱两、超出的線程會在隊列中等待卿吐。
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
// corePoolSize跟maximumPoolSize值一樣,同時傳入一個無界阻塞隊列
// 該線程池的線程會維持在指定線程數(shù)锋华,不會進行回收
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
- newSingleThreadExecutor
可以理解為線程數(shù)量為1的FixedThreadPool。
單線程化的線程池:
1箭窜、有且僅有一個工作線程執(zhí)行任務(wù)毯焕。 2、所有任務(wù)按照指定順序執(zhí)行磺樱,即遵循隊列的入隊出隊規(guī)則纳猫。
public static ExecutorService newSingleThreadExecutor() {
// 線程池中只有一個線程進行任務(wù)執(zhí)行,其他的都放入阻塞隊列
// 外面包裝的FinalizableDelegatedExecutorService類實現(xiàn)了finalize方法竹捉,在JVM垃圾回收的時候會關(guān)閉線程池
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- newScheduledThreadPool
支持定時以指定周期循環(huán)執(zhí)行任務(wù)芜辕。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
注意:前三種線程池是ThreadPoolExecutor不同配置的實例,最后一種是ScheduledThreadPoolExecutor的實例块差。
線程池的原理
從數(shù)據(jù)結(jié)構(gòu)的角度來看侵续,線程池主要使用了阻塞隊列(BlockingQueue)和HashSet集合構(gòu)成倔丈。 從任務(wù)提交的流程角度來看,對于使用線程池的外部來說状蜗,線程池的機制是這樣的:
- 如果正在運行的線程數(shù) < coreSize需五,馬上創(chuàng)建核心線程執(zhí)行該task,不排隊等待轧坎;
- 如果正在運行的線程數(shù) >= coreSize宏邮,把該task放入阻塞隊列;
- 如果隊列已滿 && 正在運行的線程數(shù) < maximumPoolSize缸血,創(chuàng)建新的非核心線程執(zhí)行該task蜜氨;
- 如果隊列已滿 && 正在運行的線程數(shù) >= maximumPoolSize,線程池調(diào)用handler的reject方法拒絕本次提交捎泻。
線程池的線程復(fù)用:
這里就需要深入到源碼addWorker():它是創(chuàng)建新線程的關(guān)鍵飒炎,也是線程復(fù)用的關(guān)鍵入口。最終會執(zhí)行到runWoker族扰,它取任務(wù)有兩個方式:
- firstTask:這是指定的第一個runnable可執(zhí)行任務(wù)厌丑,它會在Woker這個工作線程中運行執(zhí)行任務(wù)run。并且置空表示這個任務(wù)已經(jīng)被執(zhí)行渔呵。
- getTask():這首先是一個死循環(huán)過程怒竿,工作線程循環(huán)直到能夠取出Runnable對象或超時返回,這里的取的目標就是任務(wù)隊列workQueue扩氢,對應(yīng)剛才入隊的操作,有入有出录豺。
其實就是任務(wù)在并不只執(zhí)行創(chuàng)建時指定的firstTask第一任務(wù)朦肘,還會從任務(wù)隊列的中通過getTask()方法自己主動去取任務(wù)執(zhí)行,而且是有/無時間限定的阻塞等待双饥,保證線程的存活媒抠。
信號量:
semaphore 可用于進程間同步也可用于同一個進程間的線程同步。
可以用來保證兩個或多個關(guān)鍵代碼段不被并發(fā)調(diào)用咏花。在進入一個關(guān)鍵代碼段之前趴生,線程必須獲取一個信號量;一旦該關(guān)鍵代碼段完成了昏翰,那么該線程必須釋放信號量苍匆。其它想進入該關(guān)鍵代碼段的線程必須等待直到第一個線程釋放信號量。
線程池都有哪幾種工作隊列
- ArrayBlockingQueue
是一個基于數(shù)組結(jié)構(gòu)的有界阻塞隊列棚菊,此隊列按 FIFO(先進先出)原則對元素進行排序浸踩。 - LinkedBlockingQueue
一個基于鏈表結(jié)構(gòu)的阻塞隊列,此隊列按FIFO (先進先出) 排序元素统求,吞吐量通常要高于ArrayBlockingQueue检碗。靜態(tài)工廠方法Executors.newFixedThreadPool()和Executors.newSingleThreadExecutor使用了這個隊列据块。 - SynchronousQueue
一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調(diào)用移除操作后裸,否則插入操作一直處于阻塞狀態(tài)瑰钮,吞吐量通常要高于LinkedBlockingQueue,靜態(tài)工廠方法Executors.newCachedThreadPool使用了這個隊列微驶。 - PriorityBlockingQueue
一個具有優(yōu)先級的無限阻塞隊列浪谴。
怎么理解無界隊列和有界隊列
有界隊列:
1.初始的poolSize < corePoolSize,提交的runnable任務(wù)因苹,會直接做為new一個Thread的參數(shù)苟耻,立馬執(zhí)行 。
2.當(dāng)提交的任務(wù)數(shù)超過了corePoolSize扶檐,會將當(dāng)前的runable提交到一個block queue中凶杖。
3.有界隊列滿了之后,如果poolSize < maximumPoolsize時款筑,會嘗試new 一個Thread的進行救急處理智蝠,立馬執(zhí)行對應(yīng)的runnable任務(wù)。
4.如果3中也無法處理了奈梳,就會走到第四步執(zhí)行reject操作杈湾。
無界隊列:
與有界隊列相比,除非系統(tǒng)資源耗盡攘须,否則無界的任務(wù)隊列不存在任務(wù)入隊失敗的情況漆撞。當(dāng)有新的任務(wù)到來,系統(tǒng)的線程數(shù)小于corePoolSize時于宙,則新建線程執(zhí)行任務(wù)浮驳。當(dāng)達到corePoolSize后,就不會繼續(xù)增加捞魁,若后續(xù)仍有新的任務(wù)加入至会,而沒有空閑的線程資源,則任務(wù)直接進入隊列等待谱俭。若任務(wù)創(chuàng)建和處理的速度差異很大奋献,無界隊列會保持快速增長,直到耗盡系統(tǒng)內(nèi)存旺上。
當(dāng)線程池的任務(wù)緩存隊列已滿并且線程池中的線程數(shù)目達到maximumPoolSize,如果還有任務(wù)到來就會采取任務(wù)拒絕策略糖埋。
多線程中的安全隊列一般通過什么實現(xiàn)
Java提供的線程安全的Queue可以分為阻塞隊列和非阻塞隊列宣吱,其中阻塞隊列的典型例子是BlockingQueue,非阻塞隊列的典型例子是ConcurrentLinkedQueue.
對于BlockingQueue瞳别,想要實現(xiàn)阻塞功能征候,需要調(diào)用put(e) take() 方法杭攻。而ConcurrentLinkedQueue是基于鏈接節(jié)點的、無界的疤坝、線程安全的非阻塞隊列兆解。