线程中这么调用类_这些线程知识总结是真的到位!java开发两年的我看的目瞪口呆
前言
什么是線程:程序中負(fù)責(zé)執(zhí)行的那個(gè)東東就叫做線程(執(zhí)行路線,進(jìn)程內(nèi)部的執(zhí)行序列),或著說是進(jìn)程的子任務(wù)。
Java中實(shí)現(xiàn)多線程有幾種方法
繼承Thread類;
實(shí)現(xiàn)Runnable接口;
實(shí)現(xiàn)Callable接口通過FutureTask包裝器來創(chuàng)建Thread線程;
使用ExecutorService、Callable、Future實(shí)現(xiàn)有返回結(jié)果的多線程(也就是使用了ExecutorService來管理前面的三種方式)。
如何停止一個(gè)正在運(yùn)行的線程
- 使用退出標(biāo)志,使線程正常退出,也就是當(dāng)run方法完成后線程終止。
- 使用stop方法強(qiáng)行終止,但是不推薦這個(gè)方法,因?yàn)閟top和suspend及resume一樣都是過期作廢的方法。
- 使用interrupt方法中斷線程。
notify()和notifyAll()有什么區(qū)別?
notify可能會(huì)導(dǎo)致死鎖,而notifyAll則不會(huì)
任何時(shí)候只有一個(gè)線程可以獲得鎖,也就是說只有一個(gè)線程可以運(yùn)行synchronized 中的代碼
使用notifyall,可以喚醒
所有處于wait狀態(tài)的線程,使其重新進(jìn)入鎖的爭(zhēng)奪隊(duì)列中,而notify只能喚醒一個(gè)。
wait() 應(yīng)配合while循環(huán)使用,不應(yīng)使用if,務(wù)必在wait()調(diào)用前后都檢查條件,如果不滿足,必須調(diào)用notify()喚醒另外的線程來處理,自己繼續(xù)wait()直至條件滿足再往下執(zhí)行。
notify() 是對(duì)notifyAll()的一個(gè)優(yōu)化,但它有很精確的應(yīng)用場(chǎng)景,并且要求正確使用。不然可能導(dǎo)致死鎖。正確的場(chǎng)景應(yīng)該是 WaitSet中等待的是相同的條件,喚醒任一個(gè)都能正確處理接下來的事項(xiàng),如果喚醒的線程無法正確處理,務(wù)必確保繼續(xù)notify()下一個(gè)線程,并且自身需要重新回到WaitSet中.
sleep()和wait() 有什么區(qū)別?
對(duì)于sleep()方法,我們首先要知道該方法是屬于Thread類中的。而wait()方法,則是屬于Object類中的。
sleep()方法導(dǎo)致了程序暫停執(zhí)行指定的時(shí)間,讓出cpu該其他線程,但是他的監(jiān)控狀態(tài)依然保持者,當(dāng)指定的時(shí)間到了又會(huì)自動(dòng)恢復(fù)運(yùn)行狀態(tài)。在調(diào)用sleep()方法的過程中,線程不會(huì)釋放對(duì)象鎖。
當(dāng)調(diào)用wait()方法的時(shí)候,線程會(huì)放棄對(duì)象鎖,進(jìn)入等待此對(duì)象的等待鎖定池,只有針對(duì)此對(duì)象調(diào)用notify()方法后本線程才進(jìn)入對(duì)象鎖定池準(zhǔn)備,獲取對(duì)象鎖進(jìn)入運(yùn)行狀態(tài)。
volatile 是什么?可以保證有序性嗎?
一旦一個(gè)共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:
1)保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見性,即一個(gè)線程修改了某個(gè)變量的值,這新值對(duì)其他線程來說是立即可見的,volatile關(guān)鍵字會(huì)強(qiáng)制將修改的值立即寫入主存。
2)禁止進(jìn)行指令重排序。
volatile 不是原子性操作
什么叫保證部分有序性?
當(dāng)程序執(zhí)行到volatile變量的讀操作或者寫操作時(shí),在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結(jié)果已經(jīng)對(duì)后面的操作可見;在其后面的操作肯定還沒有進(jìn)行;
x = 2; //語句1y = 0; //語句2flag = true; //語句3x = 4; //語句4y = -1; //語句5由于flag變量為volatile變量,那么在進(jìn)行指令重排序的過程的時(shí)候,不會(huì)將語句3放到語句1、語句2前面,也不會(huì)將語句3放到語句4、語句5后面。但是要注意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的。
使用 Volatile 一般用于 狀態(tài)標(biāo)記量 和 單例模式的雙檢鎖
Thread 類中的start() 和 run() 方法有什么區(qū)別?
start()方法被用來啟動(dòng)新創(chuàng)建的線程,而且start()內(nèi)部調(diào)用了run()方法,這和直接調(diào)用run()方法的效果不一樣。當(dāng)你調(diào)用run()方法的時(shí)候,只會(huì)是在原來的線程中調(diào)用,沒有新的線程啟動(dòng),start()方法才會(huì)啟動(dòng)新線程。
為什么wait, notify 和 notifyAll這些方法不在thread類里面?
明顯的原因是JAVA提供的鎖是對(duì)象級(jí)的而不是線程級(jí)的,每個(gè)對(duì)象都有鎖,通過線程獲得。如果線程需要等待某些鎖那么調(diào)用對(duì)象中的wait()方法就有意義了。如果wait()方法定義在Thread類中,線程正在等待的是哪個(gè)鎖就不明顯了。簡(jiǎn)單的說,由于wait,notify和notifyAll都是鎖級(jí)別的操作,所以把他們定義在Object類中因?yàn)殒i屬于對(duì)象。
為什么wait和notify方法要在同步塊中調(diào)用?
wait()方法強(qiáng)制當(dāng)前線程釋放對(duì)象鎖。這意味著在調(diào)用某對(duì)象的wait()方法之前,當(dāng)前線程必須已經(jīng)獲得該對(duì)象的鎖。因此,線程必須在某個(gè)對(duì)象的同步方法或同步代碼塊中才能調(diào)用該對(duì)象的wait()方法。
在調(diào)用對(duì)象的notify()和notifyAll()方法之前,調(diào)用線程必須已經(jīng)得到該對(duì)象的鎖。因此,必須在某個(gè)對(duì)象的同步方法或同步代碼塊中才能調(diào)用該對(duì)象的notify()或notifyAll()方法。
調(diào)用wait()方法的原因通常是,調(diào)用線程希望某個(gè)特殊的狀態(tài)(或變量)被設(shè)置之后再繼續(xù)執(zhí)行。調(diào)用notify()或notifyAll()方法的原因通常是,調(diào)用線程希望告訴其他等待中的線程:“特殊狀態(tài)已經(jīng)被設(shè)置”。這個(gè)狀態(tài)作為線程間通信的通道,它必須是一個(gè)可變的共享狀態(tài)(或變量)。
Java中interrupted 和 isInterruptedd方法的區(qū)別?
interrupted() 和 isInterrupted()的主要區(qū)別是前者會(huì)將中斷狀態(tài)清除而后者不會(huì)。Java多線程的中斷機(jī)制是用內(nèi)部標(biāo)識(shí)來實(shí)現(xiàn)的,調(diào)用Thread.interrupt()來中斷一個(gè)線程就會(huì)設(shè)置中斷標(biāo)識(shí)為true。當(dāng)中斷線程調(diào)用靜態(tài)方法Thread.interrupted()來檢查中斷狀態(tài)時(shí),中斷狀態(tài)會(huì)被清零。而非靜態(tài)方法isInterrupted()用來查詢其它線程的中斷狀態(tài)且不會(huì)改變中斷狀態(tài)標(biāo)識(shí)。簡(jiǎn)單的說就是任何拋出InterruptedException異常的方法都會(huì)將中斷狀態(tài)清零。無論如何,一個(gè)線程的中斷狀態(tài)有有可能被其它線程調(diào)用中斷來改變。
Java中synchronized 和 ReentrantLock 有什么不同?
相似點(diǎn):
這兩種同步方式有很多相似之處,它們都是加鎖方式同步,而且都是阻塞式的同步,也就是說當(dāng)如果一個(gè)線程獲得了對(duì)象鎖,進(jìn)入了同步塊,其他訪問該同步塊的線程都必須阻塞在同步塊外面等待,而進(jìn)行線程阻塞和喚醒的代價(jià)是比較高的.
區(qū)別:
這兩種方式最大區(qū)別就是對(duì)于Synchronized來說,它是java語言的關(guān)鍵字,是原生語法層面的互斥,需要jvm實(shí)現(xiàn)。而ReentrantLock它是JDK 1.5之后提供的API層面的互斥鎖,需要lock()和unlock()方法配合try/finally語句塊來完成。
Synchronized進(jìn)過編譯,會(huì)在同步塊的前后分別形成monitorenter和monitorexit這個(gè)兩個(gè)字節(jié)碼指令。在執(zhí)行monitorenter指令時(shí),首先要嘗試獲取對(duì)象鎖。如果這個(gè)對(duì)象沒被鎖定,或者當(dāng)前線程已經(jīng)擁有了那個(gè)對(duì)象鎖,把鎖的計(jì)算器加1,相應(yīng)的,在執(zhí)行monitorexit指令時(shí)會(huì)將鎖計(jì)算器就減1,當(dāng)計(jì)算器為0時(shí),鎖就被釋放了。如果獲取對(duì)象鎖失敗,那當(dāng)前線程就要阻塞,直到對(duì)象鎖被另一個(gè)線程釋放為止。
由于ReentrantLock是java.util.concurrent包下提供的一套互斥鎖,相比Synchronized,ReentrantLock類提供了一些高級(jí)功能,主要有以下3項(xiàng):
1.等待可中斷,持有鎖的線程長(zhǎng)期不釋放的時(shí)候,正在等待的線程可以選擇放棄等待,這相當(dāng)于Synchronized來說可以避免出現(xiàn)死鎖的情況。
2.公平鎖,多個(gè)線程等待同一個(gè)鎖時(shí),必須按照申請(qǐng)鎖的時(shí)間順序獲得鎖,Synchronized鎖非公平鎖,ReentrantLock默認(rèn)的構(gòu)造函數(shù)是創(chuàng)建的非公平鎖,可以通過參數(shù)true設(shè)為公平鎖,但公平鎖表現(xiàn)的性能不是很好。
3.鎖綁定多個(gè)條件,一個(gè)ReentrantLock對(duì)象可以同時(shí)綁定對(duì)個(gè)對(duì)象。
有三個(gè)線程T1,T2,T3,如何保證順序執(zhí)行?
在多線程中有多種方法讓線程按特定順序執(zhí)行,你可以用線程類的join()方法在一個(gè)線程中啟動(dòng)另一個(gè)線程,另外一個(gè)線程完成該線程繼續(xù)執(zhí)行。為了確保三個(gè)線程的順序你應(yīng)該先啟動(dòng)最后一個(gè)(T3調(diào)用T2,T2調(diào)用T1),這樣T1就會(huì)先完成而T3最后完成。
實(shí)際上先啟動(dòng)三個(gè)線程中哪一個(gè)都行,
因?yàn)樵诿總€(gè)線程的run方法中用join方法限定了三個(gè)線程的執(zhí)行順序。
SynchronizedMap和ConcurrentHashMap有什么區(qū)別?**
SynchronizedMap()和Hashtable一樣,實(shí)現(xiàn)上在調(diào)用map所有方法時(shí),都對(duì)整個(gè)map進(jìn)行同步。而ConcurrentHashMap的實(shí)現(xiàn)卻更加精細(xì),它對(duì)map中的所有桶加了鎖。所以,只要有一個(gè)線程訪問map,其他線程就無法進(jìn)入map,而如果一個(gè)線程在訪問ConcurrentHashMap某個(gè)同時(shí),其他線程,仍然可以對(duì)map執(zhí)行某些操作。
所以,ConcurrentHashMap在性能以及安全性方面,明顯比Collections.synchronizedMap()更加有優(yōu)勢(shì)。同時(shí),同步操作精確控制到桶,這樣,即使在遍歷map時(shí),如果其他線程試圖對(duì)map進(jìn)行數(shù)據(jù)修改,也不會(huì)拋出ConcurrentModificationException。
什么是線程安全
線程安全就是說多線程訪問同一代碼,不會(huì)產(chǎn)生不確定的結(jié)果。
在多線程環(huán)境中,當(dāng)各線程不共享數(shù)據(jù)的時(shí)候,即都是私有(private)成員,那么一定是線程安全的。但這種情況并不多見,在多數(shù)情況下需要共享數(shù)據(jù),這時(shí)就需要進(jìn)行適當(dāng)?shù)耐娇刂屏恕?/p>
線程安全一般都涉及到synchronized, 就是一段代碼同時(shí)只能有一個(gè)線程來操作 不然中間過程可能會(huì)產(chǎn)生不可預(yù)制的結(jié)果。
如果你的代碼所在的進(jìn)程中有多個(gè)線程在同時(shí)運(yùn)行,而這些線程可能會(huì)同時(shí)運(yùn)行這段代碼。如果每次運(yùn)行結(jié)果和單線程運(yùn)行的結(jié)果是一樣的,而且其他的變量的值也和預(yù)期的是一樣的,就是線程安全的。
Thread類中的yield方法有什么作用?
Yield方法可以暫停當(dāng)前正在執(zhí)行的線程對(duì)象,讓其它有相同優(yōu)先級(jí)的線程執(zhí)行。它是一個(gè)靜態(tài)方法而且只保證當(dāng)前線程放棄CPU占用而不能保證使其它線程一定能占用CPU,執(zhí)行yield()的線程有可能在進(jìn)入到暫停狀態(tài)后馬上又被執(zhí)行。
Java線程池中submit() 和 execute()方法有什么區(qū)別?
兩個(gè)方法都可以向線程池提交任務(wù),execute()方法的返回類型是void,它定義在Executor接口中, 而submit()方法可以返回持有計(jì)算結(jié)果的Future對(duì)象,它定義在ExecutorService接口中,它擴(kuò)展了Executor接口,其它線程池類像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有這些方法。
說一說自己對(duì)于 synchronized 關(guān)鍵字的了解
synchronized關(guān)鍵字解決的是多個(gè)線程之間訪問資源的同步性,synchronized關(guān)鍵字可以保證被它修飾的方法或者代碼塊在任意時(shí)刻只能有一個(gè)線程執(zhí)行。
另外,在 Java 早期版本中,synchronized屬于重量級(jí)鎖,效率低下,因?yàn)楸O(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的 Mutex Lock 來實(shí)現(xiàn)的,Java 的線程是映射到操作系統(tǒng)的原生線程之上的。如果要掛起或者喚醒一個(gè)線程,都需要操作系統(tǒng)幫忙完成,而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,時(shí)間成本相對(duì)較高,這也是為什么早期的 synchronized 效率低的原因。慶幸的是在 Java 6 之后 Java 官方對(duì)從 JVM 層面對(duì)synchronized 較大優(yōu)化,所以現(xiàn)在的 synchronized 鎖效率也優(yōu)化得很不錯(cuò)了。JDK1.6對(duì)鎖的實(shí)現(xiàn)引入了大量的優(yōu)化,如自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級(jí)鎖等技術(shù)來減少鎖操作的開銷。
說說自己是怎么使用 synchronized 關(guān)鍵字,在項(xiàng)目中用到了嗎synchronized關(guān)鍵字最主要的三種使用方式:
- 修飾實(shí)例方法: 作用于當(dāng)前對(duì)象實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前對(duì)象實(shí)例的鎖
- 修飾靜態(tài)方法: 也就是給當(dāng)前類加鎖,會(huì)作用于類的所有對(duì)象實(shí)例,因?yàn)殪o態(tài)成員不屬于任何一個(gè)實(shí)例對(duì)象,是類成員( static 表明這是該類的一個(gè)靜態(tài)資源,不管new了多少個(gè)對(duì)象,只有一份)。所以如果一個(gè)線程A調(diào)用一個(gè)實(shí)例對(duì)象的非靜態(tài) synchronized 方法,而線程B需要調(diào)用這個(gè)實(shí)例對(duì)象所屬類的靜態(tài) synchronized 方法,是允許的,不會(huì)發(fā)生互斥現(xiàn)象,因?yàn)樵L問靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的鎖,而訪問非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實(shí)例對(duì)象鎖。
- 修飾代碼塊: 指定加鎖對(duì)象,對(duì)給定對(duì)象加鎖,進(jìn)入同步代碼庫前要獲得給定對(duì)象的鎖。
- 總結(jié): synchronized 關(guān)鍵字加到 static 靜態(tài)方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖。synchronized 關(guān)鍵字加到實(shí)例方法上是給對(duì)象實(shí)例上鎖。盡量不要使用 synchronized(String a) 因?yàn)镴VM中,字符串常量池具有緩存功能!
什么是線程安全?Vector是一個(gè)線程安全類嗎?
如果你的代碼所在的進(jìn)程中有多個(gè)線程在同時(shí)運(yùn)行,而這些線程可能會(huì)同時(shí)運(yùn)行這段代碼。如果每次運(yùn)行結(jié)果和單線程運(yùn)行的結(jié)果是一樣的,而且其他的變量 的值也和預(yù)期的是一樣的,就是線程安全的。一個(gè)線程安全的計(jì)數(shù)器類的同一個(gè)實(shí)例對(duì)象在被多個(gè)線程使用的情況下也不會(huì)出現(xiàn)計(jì)算失誤。很顯然你可以將集合類分 成兩組,線程安全和非線程安全的。Vector 是用同步方法來實(shí)現(xiàn)線程安全的, 而和它相似的ArrayList不是線程安全的。
volatile關(guān)鍵字的作用?
一旦一個(gè)共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:
- 保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見性,即一個(gè)線程修改了某個(gè)變量的值,這新值對(duì)其他線程來說是立即可見的。
- 禁止進(jìn)行指令重排序。
- volatile本質(zhì)是在告訴jvm當(dāng)前變量在寄存器(工作內(nèi)存)中的值是不確定的,需要從主存中讀取;synchronized則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問該變量,其他線程被阻塞住。
- volatile僅能使用在變量級(jí)別;synchronized則可以使用在變量、方法、和類級(jí)別的。
- volatile僅能實(shí)現(xiàn)變量的修改可見性,并不能保證原子性;synchronized則可以保證變量的修改可見性和原子性。
- volatile不會(huì)造成線程的阻塞;synchronized可能會(huì)造成線程的阻塞。
volatile標(biāo)記的變量不會(huì)被編譯器優(yōu)化;synchronized標(biāo)記的變量可以被編譯器優(yōu)化。
常用的線程池有哪些?
- newSingleThreadExecutor:創(chuàng)建一個(gè)單線程的線程池,此線程池保證所有任務(wù)的執(zhí)行順序按照任務(wù)的提交順序執(zhí)行。
- newFixedThreadPool:創(chuàng)建固定大小的線程池,每次提交一個(gè)任務(wù)就創(chuàng)建一個(gè)線程,直到線程達(dá)到線程池的最大大小。
- newCachedThreadPool:創(chuàng)建一個(gè)可緩存的線程池,此線程池不會(huì)對(duì)線程池大小做限制,線程池大小完全依賴于操作系統(tǒng)(或者說JVM)能夠創(chuàng)建的最大線程大小。
- newScheduledThreadPool:創(chuàng)建一個(gè)大小無限的線程池,此線程池支持定時(shí)以及周期性執(zhí)行任務(wù)的需求。
- newSingleThreadExecutor:創(chuàng)建一個(gè)單線程的線程池。此線程池支持定時(shí)以及周期性執(zhí)行任務(wù)的需求。
簡(jiǎn)述一下你對(duì)線程池的理解
(如果問到了這樣的問題,可以展開的說一下線程池如何用、線程池的好處、線程池的啟動(dòng)策略)合理利用線程池能夠帶來三個(gè)好處。
第一:降低資源消耗。通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗。
第二:提高響應(yīng)速度。當(dāng)任務(wù)到達(dá)時(shí),任務(wù)可以不需要等到線程創(chuàng)建就能立即執(zhí)行。
第三:提高線程的可管理性。線程是稀缺資源,如果無限制的創(chuàng)建,不僅會(huì)消耗系統(tǒng)資源,還會(huì)降低系統(tǒng)的穩(wěn)定性,使用線程池可以進(jìn)行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控。
Java程序是如何執(zhí)行的
我們?nèi)粘5墓ぷ髦卸际褂瞄_發(fā)工具(IntelliJ IDEA 或 Eclipse 等)可以很方便的調(diào)試程序,或者是通過打包工具把項(xiàng)目打包成 jar 包或者 war 包,放入 Tomcat 等 Web 容器中就可以正常運(yùn)行了,但你有沒有想過 Java 程序內(nèi)部是如何執(zhí)行的?其實(shí)不論是在開發(fā)工具中運(yùn)行還是在 Tomcat 中運(yùn)行,Java 程序的執(zhí)行流程基本都是相同的,它的執(zhí)行流程如下:
- 先把 Java 代碼編譯成字節(jié)碼,也就是把 .java 類型的文件編譯成 .class 類型的文件。這個(gè)過程的大致執(zhí)行流程:Java 源代碼 -> 詞法分析器 -> 語法分析器 -> 語義分析器 -> 字符碼生成器 -> 最終生成字節(jié)碼,其中任何一個(gè)節(jié)點(diǎn)執(zhí)行失敗就會(huì)造成編譯失敗;
- 把 class 文件放置到 Java 虛擬機(jī),這個(gè)虛擬機(jī)通常指的是 Oracle 官方自帶的 Hotspot JVM;
- Java 虛擬機(jī)使用類加載器(Class Loader)裝載 class 文件;
- 類加載完成之后,會(huì)進(jìn)行字節(jié)碼校驗(yàn),字節(jié)碼校驗(yàn)通過之后 JVM 解釋器會(huì)把字節(jié)碼翻譯成機(jī)器碼交由操作系統(tǒng)執(zhí)行。但不是所有代碼都是解釋執(zhí)行的,JVM 對(duì)此做了優(yōu)化,比如,以 Hotspot 虛擬機(jī)來說,它本身提供了 JIT(Just In Time)也就是我們通常所說的動(dòng)態(tài)編譯器,它能夠在運(yùn)行時(shí)將熱點(diǎn)代碼編譯為機(jī)器碼,這個(gè)時(shí)候字節(jié)碼就變成了編譯執(zhí)行。
說一說自己對(duì)于 synchronized 關(guān)鍵字的了解
synchronized關(guān)鍵字解決的是多個(gè)線程之間訪問資源的同步性,synchronized關(guān)鍵字可以保證被它修飾的方法或者代碼塊在任意時(shí)刻只能有一個(gè)線程執(zhí)行。
另外,在 Java 早期版本中,synchronized屬于重量級(jí)鎖,效率低下,因?yàn)楸O(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的 Mutex Lock 來實(shí)現(xiàn)的,Java 的線程是映射到操作系統(tǒng)的原生線程之上的。如果要掛起或者喚醒一個(gè)線程,都需要操作系統(tǒng)幫忙完成,而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,時(shí)間成本相對(duì)較高,這也是為什么早期的 synchronized 效率低的原因。慶幸的是在 Java 6 之后 Java 官方對(duì)從 JVM 層面對(duì)synchronized 較大優(yōu)化,所以現(xiàn)在的 synchronized 鎖效率也優(yōu)化得很不錯(cuò)了。JDK1.6對(duì)鎖的實(shí)現(xiàn)引入了大量的優(yōu)化,如自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級(jí)鎖等技術(shù)來減少鎖操作的開銷。
說說自己是怎么使用 synchronized 關(guān)鍵字,在項(xiàng)目中用到了嗎
synchronized關(guān)鍵字最主要的三種使用方式:
- 修飾實(shí)例方法,作用于當(dāng)前對(duì)象實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前對(duì)象實(shí)例的鎖
- 修飾靜態(tài)方法,作用于當(dāng)前類對(duì)象加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類對(duì)象的鎖 。也就是給當(dāng)前類加鎖,會(huì)作用于類的所有對(duì)象實(shí)例,因?yàn)殪o態(tài)成員不屬于任何一個(gè)實(shí)例對(duì)象,是類成員( static 表明這是該類的一個(gè)靜態(tài)資源,不管new了多少個(gè)對(duì)象,只有一份,所以對(duì)該類的所有對(duì)象都加了鎖)。所以如果一個(gè)線程A調(diào)用一個(gè)實(shí)例對(duì)象的非靜態(tài) synchronized 方法,而線程B需要調(diào)用這個(gè)實(shí)例對(duì)象所屬類的靜態(tài) synchronized 方法,是允許的,不會(huì)發(fā)生互斥現(xiàn)象,因?yàn)樵L問靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的鎖,而訪問非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實(shí)例對(duì)象鎖。
- 修飾代碼塊,指定加鎖對(duì)象,對(duì)給定對(duì)象加鎖,進(jìn)入同步代碼庫前要獲得給定對(duì)象的鎖。 和 synchronized 方法一樣,synchronized(this)代碼塊也是鎖定當(dāng)前對(duì)象的。synchronized 關(guān)鍵字加到 static 靜態(tài)方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖。這里再提一下:synchronized關(guān)鍵字加到非 static 靜態(tài)方法上是給對(duì)象實(shí)例上鎖。另外需要注意的是:盡量不要使用 synchronized(String a) 因?yàn)镴VM中,字符串常量池具有緩沖功能!
下面我已一個(gè)常見的面試題為例講解一下 synchronized 關(guān)鍵字的具體使用。
面試中面試官經(jīng)常會(huì)說:“單例模式了解嗎?來給我手寫一下!給我解釋一下雙重檢驗(yàn)鎖方式實(shí)現(xiàn)單利模式的原理唄!”
雙重校驗(yàn)鎖實(shí)現(xiàn)對(duì)象單例(線程安全)
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { //先判斷對(duì)象是否已經(jīng)實(shí)例過,沒有實(shí)例化過才進(jìn)入加鎖代碼 if (uniqueInstance == null) { //類對(duì)象加鎖 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; }}另外,需要注意 uniqueInstance 采用 volatile 關(guān)鍵字修飾也是很有必要。
uniqueInstance 采用 volatile 關(guān)鍵字修飾也是很有必要的, uniqueInstance = new Singleton(); 這段代碼其實(shí)是分為三步執(zhí)行:
但是由于 JVM 具有指令重排的特性,執(zhí)行順序有可能變成 1->3->2。指令重排在單線程環(huán)境下不會(huì)出現(xiàn)問題,但是在多線程環(huán)境下會(huì)導(dǎo)致一個(gè)線程獲得還沒有初始化的實(shí)例。例如,線程 T1 執(zhí)行了 1 和 3,此時(shí) T2 調(diào)用 getUniqueInstance() 后發(fā)現(xiàn) uniqueInstance 不為空,因此返回 uniqueInstance,但此時(shí) uniqueInstance 還未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保證在多線程環(huán)境下也能正常運(yùn)行。
講一下 synchronized 關(guān)鍵字的底層原理
synchronized 關(guān)鍵字底層原理屬于 JVM 層面。
① synchronized 同步語句塊的情況
public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("synchronized 代碼塊"); } }}復(fù)制代碼通過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關(guān)字節(jié)碼信息:首先切換到類的對(duì)應(yīng)目錄執(zhí)行 javac SynchronizedDemo.java 命令生成編譯后的 .class 文件,然后執(zhí)行javap -c -s -v -l SynchronizedDemo.class。
從上面我們可以看出:
synchronized 同步語句塊的實(shí)現(xiàn)使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結(jié)束位置。 當(dāng)執(zhí)行 monitorenter 指令時(shí),線程試圖獲取鎖也就是獲取 monitor(monitor對(duì)象存在于每個(gè)Java對(duì)象的對(duì)象頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對(duì)象可以作為鎖的原因) 的持有權(quán).當(dāng)計(jì)數(shù)器為0則可以成功獲取,獲取后將鎖計(jì)數(shù)器設(shè)為1也就是加1。相應(yīng)的在執(zhí)行 monitorexit 指令后,將鎖計(jì)數(shù)器設(shè)為0,表明鎖被釋放。如果獲取對(duì)象鎖失敗,那當(dāng)前線程就要阻塞等待,直到鎖被另外一個(gè)線程釋放為止。
② synchronized 修飾方法的的情況
public class SynchronizedDemo2 { public synchronized void method() { System.out.println("synchronized 方法"); }}synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取而代之的確實(shí)是 ACC_SYNCHRONIZED 標(biāo)識(shí),該標(biāo)識(shí)指明了該方法是一個(gè)同步方法,JVM 通過該 ACC_SYNCHRONIZED 訪問標(biāo)志來辨別一個(gè)方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用。
為什么要用線程池?
線程池提供了一種限制和管理資源(包括執(zhí)行一個(gè)任務(wù))。 每個(gè)線程池還維護(hù)一些基本統(tǒng)計(jì)信息,例如已完成任務(wù)的數(shù)量。
這里借用《Java并發(fā)編程的藝術(shù)》提到的來說一下使用線程池的好處:
- 降低資源消耗。 通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗。
- 提高響應(yīng)速度。 當(dāng)任務(wù)到達(dá)時(shí),任務(wù)可以不需要的等到線程創(chuàng)建就能立即執(zhí)行。
- 提高線程的可管理性。 線程是稀缺資源,如果無限制的創(chuàng)建,不僅會(huì)消耗系統(tǒng)資源,還會(huì)降低系統(tǒng)的穩(wěn)定性,使用線程池可以進(jìn)行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控。
實(shí)現(xiàn)Runnable接口和Callable接口的區(qū)別
如果想讓線程池執(zhí)行任務(wù)的話需要實(shí)現(xiàn)的Runnable接口或Callable接口。 Runnable接口或Callable接口實(shí)現(xiàn)類都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執(zhí)行。兩者的區(qū)別在于 Runnable 接口不會(huì)返回結(jié)果但是 Callable 接口可以返回結(jié)果。
備注: 工具類Executors可以實(shí)現(xiàn)Runnable對(duì)象和Callable對(duì)象之間的相互轉(zhuǎn)換。(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。
執(zhí)行execute()方法和submit()方法的區(qū)別是什么呢?
1)execute() 方法用于提交不需要返回值的任務(wù),所以無法判斷任務(wù)是否被線程池執(zhí)行成功與否;
2)submit()方法用于提交需要返回值的任務(wù)。線程池會(huì)返回一個(gè)future類型的對(duì)象,通過這個(gè)future對(duì)象可以判斷任務(wù)是否執(zhí)行成功,并且可以通過future的get()方法來獲取返回值,get()方法會(huì)阻塞當(dāng)前線程直到任務(wù)完成,而使用 get(long timeout,TimeUnit unit)方法則會(huì)阻塞當(dāng)前線程一段時(shí)間后立即返回,這時(shí)候有可能任務(wù)沒有執(zhí)行完。
如何創(chuàng)建線程池
《阿里巴巴Java開發(fā)手冊(cè)》中強(qiáng)制線程池不允許使用 Executors 去創(chuàng)建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學(xué)更加明確線程池的運(yùn)行規(guī)則,規(guī)避資源耗盡的風(fēng)險(xiǎn)**
Executors 返回線程池對(duì)象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor : 允許請(qǐng)求的隊(duì)列長(zhǎng)度為 Integer.MAX_VALUE,可能堆積大量的請(qǐng)求,從而導(dǎo)致OOM。CachedThreadPool 和 ScheduledThreadPool : 允許創(chuàng)建的線程數(shù)量為 Integer.MAX_VALUE ,可能會(huì)創(chuàng)建大量線程,從而導(dǎo)致OOM。
方式一:通過構(gòu)造方法實(shí)現(xiàn)
方式二:通過Executor 框架的工具類Executors來實(shí)現(xiàn) 我們可以創(chuàng)建三種類型的ThreadPoolExecutor:
- FixedThreadPool : 該方法返回一個(gè)固定線程數(shù)量的線程池。該線程池中的線程數(shù)量始終不變。當(dāng)有一個(gè)新的任務(wù)提交時(shí),線程池中若有空閑線程,則立即執(zhí)行。若沒有,則新的任務(wù)會(huì)被暫存在一個(gè)任務(wù)隊(duì)列中,待有線程空閑時(shí),便處理在任務(wù)隊(duì)列中的任務(wù)。
- SingleThreadExecutor: 方法返回一個(gè)只有一個(gè)線程的線程池。若多余一個(gè)任務(wù)被提交到該線程池,任務(wù)會(huì)被保存在一個(gè)任務(wù)隊(duì)列中,待線程空閑,按先入先出的順序執(zhí)行隊(duì)列中的任務(wù)。
- CachedThreadPool: 該方法返回一個(gè)可根據(jù)實(shí)際情況調(diào)整線程數(shù)量的線程池。線程池的線程數(shù)量不確定,但若有空閑線程可以復(fù)用,則會(huì)優(yōu)先使用可復(fù)用的線程。若所有線程均在工作,又有新的任務(wù)提交,則會(huì)創(chuàng)建新的線程處理任務(wù)。所有線程在當(dāng)前任務(wù)執(zhí)行完畢后,將返回線程池進(jìn)行復(fù)用。
對(duì)應(yīng)Executors工具類中的方法如圖所示:
最后
感謝你看到這里,文章有什么不足還請(qǐng)指正,覺得文章對(duì)你有幫助的話記得給我點(diǎn)個(gè)贊,每天都會(huì)分享java相關(guān)技術(shù)文章或行業(yè)資訊,歡迎大家關(guān)注和轉(zhuǎn)發(fā)文章!
總結(jié)
以上是生活随笔為你收集整理的线程中这么调用类_这些线程知识总结是真的到位!java开发两年的我看的目瞪口呆的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 内存之争:Kingston vs Cor
- 下一篇: kali 更新源_kali安装避坑