Java多線程

1.1 并行和并發(fā)有什么區(qū)別?

并發(fā)(concurrency)和并行(parallellism)是:

  • 并行是指兩個(gè)或者多個(gè)事件在同一時(shí)刻發(fā)生;而并發(fā)是指兩個(gè)或多個(gè)事件在同一時(shí)間間隔發(fā)生。
  • 并行是在不同實(shí)體上的多個(gè)事件,并發(fā)是在同一實(shí)體上的多個(gè)事件。比如在單核CPU系統(tǒng)上,只可能存在并發(fā)而不可能存在并行。
  • 并行是在一臺(tái)處理器上“同時(shí)”處理多個(gè)任務(wù),在多臺(tái)處理器上同時(shí)處理多個(gè)任務(wù)。如hadoop分布式集群

所以并發(fā)編程的目標(biāo)是充分的利用處理器的每一個(gè)核,以達(dá)到最高的處理性能。那為什么并發(fā)就能充分利用cpu的執(zhí)行能力

首先執(zhí)行多個(gè)任務(wù)如果是串行執(zhí)行那么cpu一定會(huì)存在等待一個(gè)任務(wù)執(zhí)行完再去執(zhí)行下一個(gè)任務(wù),但是如果是并發(fā)開(kāi)啟多個(gè)線程去分別執(zhí)行不同的任務(wù)的時(shí)候,這個(gè)時(shí)候便可以充分的利用cpu,多個(gè)線程進(jìn)行切換去搶占cpu,cpu的空閑時(shí)間就會(huì)減少。

1.2 多線程 VS 高并發(fā)

多線程是完成任務(wù)的一種方法,高并發(fā)是系統(tǒng)運(yùn)行的一種狀態(tài),通過(guò)多線程有助于系統(tǒng)承受高并發(fā)的狀態(tài)的實(shí)現(xiàn)。

高并發(fā)是系統(tǒng)運(yùn)行過(guò)程中遇到的一種“短時(shí)間內(nèi)遇到大量的操作請(qǐng)求” 的情況,主要發(fā)生在web系統(tǒng)集中大量訪問(wèn)或者socket端口集中行收到大量請(qǐng)求(例如12306搶票;天貓雙十一活動(dòng))。該情況會(huì)導(dǎo)致系統(tǒng)在這段時(shí)間內(nèi)大量操作,例如對(duì)資源的請(qǐng)求,對(duì)數(shù)據(jù)庫(kù)的集中操作等。如果并發(fā)處理不好,不僅降低了客戶體驗(yàn)度(請(qǐng)求時(shí)間過(guò)長(zhǎng)) ,同時(shí)可能導(dǎo)致宕機(jī),系統(tǒng)停止工作等。如果想要系統(tǒng)適應(yīng)高并發(fā)的狀態(tài),則需要從,硬件,軟件,網(wǎng)絡(luò),系統(tǒng)架構(gòu),開(kāi)發(fā)語(yǔ)言的選取,數(shù)據(jù)結(jié)構(gòu)的運(yùn)用,算法優(yōu)化,數(shù)據(jù)庫(kù)優(yōu)化等。。。而多線程只是解決方案其中之一。

2. 線程和進(jìn)程的區(qū)別?

進(jìn)程:是執(zhí)行中一段程序,即一旦程序被載入到內(nèi)存中并準(zhǔn)備執(zhí)行,它就是一個(gè)進(jìn)程。進(jìn)程是表示資源分配的的基本概念,又是調(diào)度運(yùn)行的基本單位,是系統(tǒng)中的并發(fā)執(zhí)行的單位。

線程:?jiǎn)蝹€(gè)進(jìn)程中執(zhí)行中每個(gè)任務(wù)就是一個(gè)線程。線程是進(jìn)程中執(zhí)行運(yùn)算的最小單位。
一個(gè)線程只能屬于一個(gè)進(jìn)程,但是一個(gè)進(jìn)程可以擁有多個(gè)線程。多線程處理就是允許一個(gè)進(jìn)程中在同一時(shí)刻執(zhí)行多個(gè)任務(wù)。

進(jìn)程和線程的主要差別在于它們是不同的操作系統(tǒng)資源管理方式。進(jìn)程有獨(dú)立的地址空間,一個(gè)進(jìn)程崩潰后,在保護(hù)模式下不會(huì)對(duì)其它進(jìn)程產(chǎn)生影響,而線程只是一個(gè)進(jìn)程中的不同執(zhí)行路徑。線程有自己的堆棧和局部變量,但線程之間沒(méi)有單獨(dú)的地址空間,一個(gè)線程死掉就等于整個(gè)進(jìn)程死掉,所以多進(jìn)程的程序要比多線程的程序健壯,但在進(jìn)程切換時(shí),耗費(fèi)資源較大,效率要差一些。但對(duì)于一些要求同時(shí)進(jìn)行并且又要共享某些變量的并發(fā)操作,只能用線程,不能用進(jìn)程。

    1. 簡(jiǎn)而言之,一個(gè)程序至少有一個(gè)進(jìn)程,一個(gè)進(jìn)程至少有一個(gè)線程.**
    1. 線程的劃分尺度小于進(jìn)程,使得多線程程序的并發(fā)性高。
    1. 另外,進(jìn)程在執(zhí)行過(guò)程中擁有獨(dú)立的內(nèi)存單元,而多個(gè)線程共享內(nèi)存,從而極大地提高了程序的運(yùn)行效率。
    1. 線程在執(zhí)行過(guò)程中與進(jìn)程還是有區(qū)別的。每個(gè)獨(dú)立的線程有一個(gè)程序運(yùn)行的入口、順序執(zhí)行序列和程序的出口。但是線程不能夠獨(dú)立執(zhí)行,必須依存在應(yīng)用程序中,由應(yīng)用程序提供多個(gè)線程執(zhí)行控制。
    1. 從邏輯角度來(lái)看,多線程的意義在于一個(gè)應(yīng)用程序中,有多個(gè)執(zhí)行部分可以同時(shí)執(zhí)行。但操作系統(tǒng)并沒(méi)有將多個(gè)線程看做多個(gè)獨(dú)立的應(yīng)用,來(lái)實(shí)現(xiàn)進(jìn)程的調(diào)度和管理以及資源分配。這就是進(jìn)程和線程的重要區(qū)別。
    1. 線程和進(jìn)程在使用上各有優(yōu)缺點(diǎn):
      線程執(zhí)行開(kāi)銷小,但不利于資源的管理和保護(hù);
      而進(jìn)程正相反。
      同時(shí),線程適合于在SMP機(jī)器上運(yùn)行,而進(jìn)程則可以跨機(jī)器遷移。

3. 守護(hù)線程是什么?

守護(hù)線程(即daemon thread),是個(gè)服務(wù)線程,準(zhǔn)確地來(lái)說(shuō)就是服務(wù)其他的線程,這是它的作用——而其他的線程只有一種,那就是用戶線程。所以java里線程分2種,
1、守護(hù)線程,比如垃圾回收線程,就是最典型的守護(hù)線程。
2、用戶線程,就是應(yīng)用程序里的自定義線程。

將線程轉(zhuǎn)換為守護(hù)線程可以通過(guò)調(diào)用Thread對(duì)象的setDaemon(true)方法來(lái)實(shí)現(xiàn)。在使用守護(hù)線程時(shí)需要注意一下幾點(diǎn):

(1) thread.setDaemon(true)必須在thread.start()之前設(shè)置,否則會(huì)跑出一個(gè)IllegalThreadStateException異常。你不能把正在運(yùn)行的常規(guī)線程設(shè)置為守護(hù)線程。

(2) 在Daemon線程中產(chǎn)生的新線程也是Daemon的。

(3) 守護(hù)線程應(yīng)該永遠(yuǎn)不去訪問(wèn)固有資源,如文件、數(shù)據(jù)庫(kù),因?yàn)樗鼤?huì)在任何時(shí)候甚至在一個(gè)操作的中間發(fā)生中斷

守護(hù)線程和用戶線程的沒(méi)啥本質(zhì)的區(qū)別:唯一的不同之處就在于虛擬機(jī)的離開(kāi):如果用戶線程已經(jīng)全部退出運(yùn)行了,只剩下守護(hù)線程存在了,虛擬機(jī)也就退出了。 因?yàn)闆](méi)有了被守護(hù)者,守護(hù)線程也就沒(méi)有工作可做了,也就沒(méi)有繼續(xù)運(yùn)行程序的必要了。

4. 創(chuàng)建線程有哪幾種方式?

一、繼承Thread類創(chuàng)建線程類
二、通過(guò)Runnable接口創(chuàng)建線程類
三、通過(guò)Callable和Future創(chuàng)建線程

采用實(shí)現(xiàn)Runnable、Callable接口的方式創(chuàng)見(jiàn)多線程時(shí),優(yōu)勢(shì)是:
線程類只是實(shí)現(xiàn)了Runnable接口或Callable接口,還可以繼承其他類。
在這種方式下,多個(gè)線程可以共享同一個(gè)target對(duì)象,所以非常適合多個(gè)相同線程來(lái)處理同一份資源的情況,從而可以將CPU、代碼和數(shù)據(jù)分開(kāi),形成清晰的模型,較好地體現(xiàn)了面向?qū)ο蟮乃枷搿?/p>

劣勢(shì)是:

編程稍微復(fù)雜,如果要訪問(wèn)當(dāng)前線程,則必須使用Thread.currentThread()方法。

使用繼承Thread類的方式創(chuàng)建多線程時(shí)優(yōu)勢(shì)是:

編寫簡(jiǎn)單,如果需要訪問(wèn)當(dāng)前線程,則無(wú)需使用Thread.currentThread()方法,直接使用this即可獲得當(dāng)前線程。

劣勢(shì)是:

線程類已經(jīng)繼承了Thread類,所以不能再繼承其他父類。

5. 說(shuō)一下 runnable 和 callable 有什么區(qū)別?

相同點(diǎn)
  • 都是接口
  • 都可以編寫多線程程序
  • 都采用Thread.start()啟動(dòng)線程
不同點(diǎn)
  • Runnable沒(méi)有返回值;Callable可以返回執(zhí)行結(jié)果,是個(gè)泛型,和Future、FutureTask配合可以用來(lái)獲取異步執(zhí)行的結(jié)果
  • Callable接口的call()方法允許拋出異常;Runnable的run()方法異常只能在內(nèi)部消化,不能往上繼續(xù)拋

:Callalbe接口支持返回執(zhí)行結(jié)果,需要調(diào)用FutureTask.get()得到,此方法會(huì)阻塞主進(jìn)程的繼續(xù)往下執(zhí)行,如果不調(diào)用不會(huì)阻塞。

Thread(抽象類) 和 runnable(接口)區(qū)別

  • 1、由于Java不允許多繼承,因此實(shí)現(xiàn)了Runnable接口可以再繼承其他類,但是Thread明顯不可以;
  • 2、Runnable可以實(shí)現(xiàn)多個(gè)相同的程序代碼的線程去共享同一個(gè)資源,而Thread并不是不可以,而是相比于Runnable來(lái)說(shuō),不太適合。
    比說(shuō),在實(shí)現(xiàn)runnable接口的類A中,定義一個(gè)變量B,然后使用new Thread(A).start()啟動(dòng)線程,通過(guò)多線程來(lái)共享A類中的變量B;但是通過(guò)Thread就不能這樣做,每次new Thread類C,C中的變量D都是新的,不共享的。如果需要共享,則和繼承runnable一樣,在繼承thread的類A中,定義一個(gè)變量B,然后然后使用new Thread(A).start()啟動(dòng)線程。
    其實(shí)我們從Thread源碼中也可以看到,當(dāng)以Thread方式去實(shí)現(xiàn)資源共享時(shí),實(shí)際上源碼內(nèi)部是將thread向下轉(zhuǎn)型為了Runnable,實(shí)際上內(nèi)部依然是以Runnable形式去實(shí)現(xiàn)的資源共享

在程序開(kāi)發(fā)中只要是多線程肯定永遠(yuǎn)以實(shí)現(xiàn)Runnable接口為主。

6. 線程有哪些狀態(tài)?

Java中的線程的生命周期大體可分為5種狀態(tài)。

image.png
    1. 新建(NEW):新創(chuàng)建了一個(gè)線程對(duì)象。
    1. 可運(yùn)行(RUNNABLE):線程對(duì)象創(chuàng)建后,其他線程(比如main線程)調(diào)用了該對(duì)象的start()方法。該狀態(tài)的線程位于可運(yùn)行線程池中,等待被線程調(diào)度選中,獲取cpu 的使用權(quán) 。
    1. 運(yùn)行(RUNNING):可運(yùn)行狀態(tài)(runnable)的線程獲得了cpu 時(shí)間片(timeslice) ,執(zhí)行程序代碼。
    1. 阻塞(BLOCKED):阻塞狀態(tài)是指線程因?yàn)槟撤N原因放棄了cpu 使用權(quán),也即讓出了cpu timeslice,暫時(shí)停止運(yùn)行。直到線程進(jìn)入可運(yùn)行(runnable)狀態(tài),才有機(jī)會(huì)再次獲得cpu timeslice 轉(zhuǎn)到運(yùn)行(running)狀態(tài)。阻塞的情況分三種:
    • (一). 等待阻塞:運(yùn)行(running)的線程執(zhí)行o.wait()方法,JVM會(huì)把該線程放入等待隊(duì)列(waitting queue)中。
    • (二). 同步阻塞:運(yùn)行(running)的線程在獲取對(duì)象的同步鎖時(shí),若該同步鎖被別的線程占用,則JVM會(huì)把該線程放入鎖池(lock pool)中。
    • (三). 其他阻塞:運(yùn)行(running)的線程執(zhí)行Thread.sleep(long ms)或t.join()方法,或者發(fā)出了I/O請(qǐng)求時(shí),JVM會(huì)把該線程置為阻塞狀態(tài)。當(dāng)sleep()狀態(tài)超時(shí)、join()等待線程終止或者超時(shí)、或者I/O處理完畢時(shí),線程重新轉(zhuǎn)入可運(yùn)行(runnable)狀態(tài)。
    1. 死亡(DEAD):線程run()、main() 方法執(zhí)行結(jié)束,或者因異常退出了run()方法,則該線程結(jié)束生命周期。死亡的線程不可再次復(fù)生。

7. sleep() 和 wait() 有什么區(qū)別?

  • 1、這兩個(gè)方法來(lái)自不同的類分別是,sleep來(lái)自Thread類,和wait來(lái)自O(shè)bject類。

  • 2、sleep() 和 wait() 的區(qū)別就是 調(diào)用sleep方法的線程不會(huì)釋放對(duì)象鎖,而調(diào)用wait() 方法會(huì)釋放對(duì)象鎖

sleep是Thread的靜態(tài)類方法,誰(shuí)調(diào)用的誰(shuí)去睡覺(jué),即使在a線程里調(diào)用了b的sleep方法,實(shí)際上還是a去睡覺(jué),要讓b線程睡覺(jué)要在b的代碼中調(diào)用sleep。

wait()是Object類的方法,當(dāng)一個(gè)線程執(zhí)行到wait方法時(shí),它就進(jìn)入到一個(gè)和該對(duì)象相關(guān)的等待池,同時(shí)釋放對(duì)象的機(jī)鎖,使得其他線程能夠訪問(wèn),可以通過(guò)notify,notifyAll方法來(lái)喚醒等待的線程

sleep不出讓系統(tǒng)資源;wait是進(jìn)入線程等待池等待,出讓系統(tǒng)資源,其他線程可以占用CPU。一般wait不會(huì)加時(shí)間限制,因?yàn)槿绻鹷ait線程的運(yùn)行資源不夠,再出來(lái)也沒(méi)用,要等待其他線程調(diào)用notify/notifyAll喚醒等待池中的所有線程,才會(huì)進(jìn)入就緒隊(duì)列等待OS分配系統(tǒng)資源。sleep(milliseconds)可以用時(shí)間指定使它自動(dòng)喚醒過(guò)來(lái),如果時(shí)間不到只能調(diào)用interrupt()強(qiáng)行打斷。

Thread.Sleep(0)的作用是“觸發(fā)操作系統(tǒng)立刻重新進(jìn)行一次CPU競(jìng)爭(zhēng)”。

  • 3、使用范圍:wait,notify和notifyAll只能在同步控制方法或者同步控制塊里面使用,而sleep可以在任何地方使用
    synchronized(x){
    x.notify()
    //或者wait()
    }
  • 4、sleep必須捕獲異常,而wait,notify和notifyAll不需要捕獲異常

8. notify()和 notifyAll()有什么區(qū)別?

如果線程調(diào)用了對(duì)象的 wait()方法,那么線程便會(huì)處于該對(duì)象的等待池中,等待池中的線程不會(huì)去競(jìng)爭(zhēng)該對(duì)象的鎖。
當(dāng)有線程調(diào)用了對(duì)象的 notifyAll()方法(喚醒所有 wait 線程)或 notify()方法(只隨機(jī)喚醒一個(gè) wait 線程),被喚醒的的線程便會(huì)進(jìn)入該對(duì)象的鎖池中,鎖池中的線程會(huì)去競(jìng)爭(zhēng)該對(duì)象鎖。也就是說(shuō),調(diào)用了notify后只要一個(gè)線程會(huì)由等待池進(jìn)入鎖池,而notifyAll會(huì)將該對(duì)象等待池內(nèi)的所有線程移動(dòng)到鎖池中,等待鎖競(jìng)爭(zhēng)
優(yōu)先級(jí)高的線程競(jìng)爭(zhēng)到對(duì)象鎖的概率大,假若某線程沒(méi)有競(jìng)爭(zhēng)到該對(duì)象鎖,它還會(huì)留在鎖池中,唯有線程再次調(diào)用 wait()方法,它才會(huì)重新回到等待池中。而競(jìng)爭(zhēng)到對(duì)象鎖的線程則繼續(xù)往下執(zhí)行,直到執(zhí)行完了 synchronized 代碼塊,它會(huì)釋放掉該對(duì)象鎖,這時(shí)鎖池中的線程會(huì)繼續(xù)競(jìng)爭(zhēng)該對(duì)象鎖。
綜上,所謂喚醒線程,另一種解釋可以說(shuō)是將線程由等待池移動(dòng)到鎖池,notifyAll調(diào)用后,會(huì)將全部線程由等待池移到鎖池,然后參與鎖的競(jìng)爭(zhēng),競(jìng)爭(zhēng)成功則繼續(xù)執(zhí)行,如果不成功則留在鎖池等待鎖被釋放后再次參與競(jìng)爭(zhēng)。而notify只會(huì)喚醒一個(gè)線程。

notify可能會(huì)導(dǎo)致死鎖,而notifyAll則不會(huì)

9. 線程的 run()和 start()有什么區(qū)別?

每個(gè)線程都有要執(zhí)行的任務(wù)。線程的任務(wù)處理邏輯可以在Tread類的run實(shí)例方法中直接實(shí)現(xiàn)或通過(guò)該方法進(jìn)行調(diào)用,因此

  • run()相當(dāng)于線程的任務(wù)處理邏輯的入口方法,它由Java虛擬機(jī)在運(yùn)行相應(yīng)線程時(shí)直接調(diào)用,而不是由應(yīng)用代碼進(jìn)行調(diào)用。

  • start()的作用是啟動(dòng)相應(yīng)的線程。啟動(dòng)一個(gè)線程實(shí)際是請(qǐng)求Java虛擬機(jī)運(yùn)行相應(yīng)的線程,而這個(gè)線程何時(shí)能夠運(yùn)行是由線程調(diào)度器決定的。start()調(diào)用結(jié)束并不表示相應(yīng)線程已經(jīng)開(kāi)始運(yùn)行,這個(gè)線程可能稍后運(yùn)行,也可能永遠(yuǎn)也不會(huì)運(yùn)行。

10.創(chuàng)建線程池有哪幾種方式?

Java通過(guò)Executors提供四種線程池,分別為:

  • newCachedThreadPool:創(chuàng)建一個(gè)可緩存線程池,如果線程池長(zhǎng)度超過(guò)處理需要,可靈活回收空閑線程,若無(wú)可回收,則新建線程,隊(duì)列是SynchronousQueue<Runnable>;
  • newFixedThreadPool 創(chuàng)建一個(gè)定長(zhǎng)線程池,可控制線程最大并發(fā)數(shù),超出的線程會(huì)在隊(duì)列中等待,隊(duì)列是LinkedBlockingQueue<Runnable>;
  • newScheduledThreadPool 創(chuàng)建一個(gè)定長(zhǎng)線程池,支持定時(shí)及周期性任務(wù)執(zhí)行,隊(duì)列是DelayedWorkQueue();
  • newSingleThreadExecutor 創(chuàng)建一個(gè)單線程化的線程池,它只會(huì)用唯一的工作線程來(lái)執(zhí)行任務(wù),保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級(jí))執(zhí)行,隊(duì)列是LinkedBlockingQueue<Runnable>;

阿里的 Java開(kāi)發(fā)手冊(cè),上面有線程池的一個(gè)建議:線程池不允許使用 Executors 去創(chuàng)建,而是通過(guò) ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學(xué)更加明確線程池的運(yùn)行規(guī)則,規(guī)避資源耗盡的風(fēng)險(xiǎn)。Executors利用工廠模式向我們提供了4種線程池實(shí)現(xiàn)方式,但是并不推薦使用,原因是使用Executors創(chuàng)建線程池不會(huì)傳入拒絕策略這個(gè)參數(shù)而使用默認(rèn)值所以我們常常忽略這一參數(shù),而且默認(rèn)使用的參數(shù)會(huì)導(dǎo)致資源浪費(fèi),不可取。

說(shuō)明:Executors 各個(gè)方法的弊端:
1)newFixedThreadPool 和 newSingleThreadExecutor:主要問(wèn)題是堆積的請(qǐng)求處理隊(duì)列可能會(huì)耗費(fèi)非常大的內(nèi)存,甚至 OOM,因?yàn)殛?duì)列是無(wú)界阻塞隊(duì)列LinkedBlockingQueue;

2)newCachedThreadPool 和 newScheduledThreadPool:主要問(wèn)題是線程數(shù)最大數(shù)是 Integer.MAX_VALUE,可能會(huì)創(chuàng)建數(shù)量非常多的線程,甚至 OOM。因?yàn)镾ynchronousQueue是一個(gè)內(nèi)部只能包含一個(gè)元素的隊(duì)列;

線程池源碼解析見(jiàn)Java線程池
隊(duì)列的介紹見(jiàn)Java容器

11. 線程池中 submit()和 execute()方法有什么區(qū)別?

    1. 接收的參數(shù)不一樣;submit callable,execute 是runnable
    1. submit()有返回值,而execute()沒(méi)有;
      例如,有個(gè)validation的task,希望該task執(zhí)行完后告訴我它的執(zhí)行結(jié)果,是成功還是失敗,然后繼續(xù)下面的操作。
    1. submit()可以進(jìn)行Exception處理;
      例如,如果task里會(huì)拋出checked或者unchecked exception,而你又希望外面的調(diào)用者能夠感知這些exception并做出及時(shí)的處理,那么就需要用到submit,通過(guò)對(duì)Future.get()進(jìn)行拋出異常的捕獲,然后對(duì)其進(jìn)行處理。

12.1 在 java 程序中怎么保證多線程的運(yùn)行安全?

一、線程安全在三個(gè)方面體現(xiàn)

1.原子性:提供互斥訪問(wèn),同一時(shí)刻只能有一個(gè)線程對(duì)數(shù)據(jù)進(jìn)行操作,(atomic,synchronized);

2.可見(jiàn)性:一個(gè)線程對(duì)主內(nèi)存的修改可以及時(shí)地被其他線程看到,(synchronized,volatile);

3.有序性:一個(gè)線程觀察其他線程中的指令執(zhí)行順序,由于指令重排序,該觀察結(jié)果一般雜亂無(wú)序,(happens-before原則)。

當(dāng)然由于synchronized和Lock保證每個(gè)時(shí)刻只有一個(gè)線程執(zhí)行同步代碼,所以是線程安全的,也可以實(shí)現(xiàn)這一功能,但是由于線程是同步執(zhí)行的,所以會(huì)影響效率。

原子性:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(huì)被任何因素打斷,要么就都不執(zhí)行。JDK里面提供了很多atomic類,AtomicInteger,AtomicLong,AtomicBoolean等等。它們是通過(guò)CAS完成原子性

可見(jiàn)性:指當(dāng)多個(gè)線程訪問(wèn)同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。

當(dāng)一個(gè)共享變量被volatile修飾時(shí),它會(huì)保證修改的值會(huì)立即被更新到主存,當(dāng)有其他線程需要讀取共享變量時(shí),它會(huì)去內(nèi)存中讀取新值。

普通的共享變量不能保證可見(jiàn)性,因?yàn)槠胀ü蚕碜兞勘恍薷暮?,什么時(shí)候被寫入主存是不確定的,當(dāng)其他線程去讀取時(shí),此時(shí)內(nèi)存中可能還是原來(lái)的舊值,因此無(wú)法保證可見(jiàn)性。

更新主存的步驟:當(dāng)前線程將其他線程的工作內(nèi)存中的緩存變量的緩存行設(shè)置為無(wú)效,然后當(dāng)前線程將變量的值跟新到主存,更新成功后將其他線程的緩存行更新為新的主存地址

其他線程讀取變量時(shí),發(fā)現(xiàn)自己的緩存行無(wú)效,它會(huì)等待緩存行對(duì)應(yīng)的主存地址被更新之后,然后去對(duì)應(yīng)的主存讀取最新的值。

有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
在Java內(nèi)存模型中,允許編譯器和處理器對(duì)指令進(jìn)行重排序,但是重排序過(guò)程不會(huì)影響到單線程程序的執(zhí)行,卻會(huì)影響到多線程并發(fā)執(zhí)行的正確性。
可以通過(guò)volatile關(guān)鍵字來(lái)保證一定的“有序性”。

12.2 在 java 程序中如何保證多個(gè)線程的執(zhí)行順序?

方法一:創(chuàng)建一個(gè)單線程線程池
//創(chuàng)建只有一根線程的線程池,保證所有任務(wù)按照指定順序執(zhí)行
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.submit(new A());
        executorService.submit(new B());
        executorService.submit(new C());
        executorService.shutdown();
方法二:使用join()方法,等前一個(gè)線程執(zhí)行完畢,下一個(gè)線程才能執(zhí)行

當(dāng)調(diào)用了t.join(),就必須要等待線程t執(zhí)行完畢后,才能繼續(xù)執(zhí)行其他線程。這里其實(shí)是運(yùn)用了Java中最頂級(jí)對(duì)象Object提供的方法wait()。wait()方法用于線程間通信,它的含義是通知一個(gè)線程等待一下,讓出CPU資源,注意這里是會(huì)放棄已經(jīng)占有的資源的。直到t線程執(zhí)行完畢,再調(diào)用notify()喚醒當(dāng)前正在運(yùn)行的線程。

public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new A());
        thread1.start();
        thread1.join();
        Thread thread2 = new Thread(new B());
        thread2.start();
        thread2.join();
        Thread thread3 = new Thread(new C());
        thread3.start();
    }

12.3 辨別線程安全與線程不安全

我們?cè)贘ava中常常會(huì)有某個(gè)對(duì)象是線程安全的,另一個(gè)對(duì)象是線程不安全的,那么怎么判斷這些對(duì)象是線程安全不安全的呢?是什么決定的線程安全問(wèn)題呢?

線程安全問(wèn)題都是由全局變量及靜態(tài)變量引起的。
若每個(gè)線程中對(duì)全局變量、靜態(tài)變量只有讀操作,而無(wú)寫操作,一般來(lái)說(shuō),這個(gè)全局變量是線程安全的;若有多個(gè)線程同時(shí)對(duì)全局變量(靜態(tài)變量)執(zhí)行寫操作——并發(fā)訪問(wèn)資源,一般都需要考慮線程同步,否則的話就可能影響線程安全。

那么怎么解決多線程并發(fā)訪問(wèn)資源的安全問(wèn)題呢?
通常有三種方式:同步代碼塊synchronized 、同步方法synchronized和鎖機(jī)制(Lock)

所以,一個(gè)對(duì)象是不是線程安全的,直接看其有沒(méi)有對(duì)全局變量做寫操作就可以,如果有,則繼續(xù)看其寫操作時(shí)有沒(méi)有加鎖Lock(或者同步),如果有的話,就是線程安全的,如果沒(méi)有的話,就是線程不安全的。

13. 多線程鎖的升級(jí)原理是什么?

在Java中,鎖共有4種狀態(tài),級(jí)別從低到高依次為:無(wú)狀態(tài)鎖,偏向鎖,輕量級(jí)鎖和重量級(jí)鎖狀態(tài),這幾個(gè)狀態(tài)會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí)。鎖可以升級(jí)但不能降級(jí)。

偏向鎖
偏向鎖的核心思想就是鎖會(huì)偏向第一個(gè)獲取它的線程,在接下來(lái)的執(zhí)行過(guò)程中該鎖沒(méi)有其他的線程獲取,則持有偏向鎖的線程永遠(yuǎn)不需要再進(jìn)行同步。

當(dāng)一個(gè)線程訪問(wèn)同步塊并獲取鎖的時(shí)候,會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)偏向的線程 ID,以后該線程在進(jìn)入和退出同步塊時(shí)不需要進(jìn)行 CAS 操作來(lái)加鎖和解鎖,只需要檢查當(dāng)前 Mark Word 中存儲(chǔ)的線程是否為當(dāng)前線程,如果是,則表示已經(jīng)獲得對(duì)象鎖;否則,需要測(cè)試 Mark Word 中偏向鎖的標(biāo)志是否為1,如果沒(méi)有則使用 CAS 操作競(jìng)爭(zhēng)鎖,如果設(shè)置了,則嘗試使用 CAS 將對(duì)象頭的偏向鎖指向當(dāng)前線程。

需要注意的是,偏向鎖使用一種等待競(jìng)爭(zhēng)出現(xiàn)才釋放鎖的機(jī)制,所以當(dāng)有其他線程嘗試獲得鎖時(shí),才會(huì)釋放鎖。偏向鎖的撤銷,需要等到安全點(diǎn)。它首先會(huì)暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著,如果不處于活動(dòng)狀態(tài),則將對(duì)象頭設(shè)置為無(wú)鎖狀態(tài);如果依然活動(dòng),擁有偏向鎖的棧會(huì)被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄,棧中的鎖記錄和對(duì)象頭的Mark Word要么重新偏向其他線程,要么恢復(fù)到無(wú)鎖或者標(biāo)記對(duì)象不合適作為偏向鎖(膨脹為輕量級(jí)鎖),最后喚醒暫停的線程。

輕量級(jí)鎖
線程在執(zhí)行同步塊之前,JVM會(huì)現(xiàn)在當(dāng)前線程的棧幀中創(chuàng)建用于儲(chǔ)存鎖記錄的空間(LockRecord),并將對(duì)象頭的Mark Word信息復(fù)制到鎖記錄中。然后線程嘗試使用 CAS 將對(duì)象頭的MarkWord替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,并且對(duì)象的鎖標(biāo)志位轉(zhuǎn)變?yōu)椤?0”,如果失敗,表示其他線程競(jìng)爭(zhēng)鎖,當(dāng)前線程便會(huì)嘗試自旋獲取鎖。如果有兩條以上的線程競(jìng)爭(zhēng)同一個(gè)鎖,那么輕量級(jí)鎖就不再有效,要膨脹為重量級(jí)鎖,鎖標(biāo)志的狀態(tài)變?yōu)椤?0”,MarkWord中儲(chǔ)存的就是指向重量級(jí)鎖(互斥量)的指針,后面等待的線程也要進(jìn)入阻塞狀態(tài)。

輕量級(jí)鎖解鎖時(shí),同樣通過(guò)CAS操作將對(duì)象頭換回來(lái)。如果成功,則表示沒(méi)有競(jìng)爭(zhēng)發(fā)生。如果失敗,說(shuō)明有其他線程嘗試過(guò)獲取該鎖,鎖同樣會(huì)膨脹為重量級(jí)鎖。在釋放鎖的同時(shí),喚醒被掛起的線程。

重量級(jí)鎖
重量級(jí)鎖(Heavyweight Lock)是將程序運(yùn)行交出控制權(quán),將線程掛起,由操作系統(tǒng)來(lái)負(fù)責(zé)線程間的調(diào)度,負(fù)責(zé)線程的阻塞和執(zhí)行。這樣會(huì)出現(xiàn)頻繁地對(duì)線程運(yùn)行狀態(tài)的切換,線程的掛起和喚醒,消耗大量的系統(tǒng)資源,導(dǎo)致性能低下。

最后看一下,鎖升級(jí)的圖示過(guò)程:

image.png

14. 什么是死鎖?

死鎖可以這樣理解,就是互相不讓步不放棄,同時(shí)需要對(duì)方的資源。造成互相不滿足資源需求,也不放棄自身已有資源。死鎖就這樣了。

死鎖是指多個(gè)進(jìn)程因競(jìng)爭(zhēng)資源而造成的一種僵局(互相等待),若無(wú)外力作用,這些進(jìn)程都將無(wú)法向前推進(jìn)。

死鎖是指兩個(gè)或兩個(gè)以上的進(jìn)程在執(zhí)行過(guò)程中,因爭(zhēng)奪資源而造成的一種互相等待的現(xiàn)象,若無(wú)外力作用,它們都將無(wú)法推進(jìn)下去,如果系統(tǒng)資源充足,進(jìn)程的資源請(qǐng)求都能夠得到滿足,死鎖出現(xiàn)的可能性就很低,否則就會(huì)因爭(zhēng)奪有限的資源而陷入死鎖。
產(chǎn)生死鎖的原因主要是:
  (1) 因?yàn)橄到y(tǒng)資源不足。
 ?。?) 進(jìn)程運(yùn)行推進(jìn)的順序不合適。
 ?。?) 資源分配不當(dāng)?shù)取?br> 產(chǎn)生死鎖的四個(gè)必要條件:
 ?。?) 互斥條件:一個(gè)資源每次只能被一個(gè)進(jìn)程使用。
  (2) 請(qǐng)求與保持條件:一個(gè)進(jìn)程因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不放。
 ?。?) 不剝奪條件:進(jìn)程已獲得的資源,在末使用完之前,不能強(qiáng)行剝奪。
 ?。?) 循環(huán)等待條件:若干進(jìn)程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系。
  這四個(gè)條件是死鎖的必要條件,只要系統(tǒng)發(fā)生死鎖,這些條件必然成立,而只要上述條件之
  一不滿足,就不會(huì)發(fā)生死鎖。

15. 怎么防止死鎖?

理解了死鎖的原因,尤其是產(chǎn)生死鎖的四個(gè)必要條件,就可以最大可能地避免、預(yù)防和解除死鎖。所以,在系統(tǒng)設(shè)計(jì)、進(jìn)程調(diào)度等方面注意如何不讓這四個(gè)必要條件成立,如何確定資源的合理分配算法,避免進(jìn)程永久占據(jù)系統(tǒng)資源。此外,也要防止進(jìn)程在處于等待狀態(tài)
的情況下占用資源。因此,對(duì)資源的分配要給予合理的規(guī)劃。

預(yù)防死鎖,預(yù)先破壞產(chǎn)生死鎖的四個(gè)條件。互斥不可能破壞,所以有如下三種方法:
1、破壞請(qǐng)求和保持條件,
進(jìn)程必須等所有要請(qǐng)求的資源都空閑時(shí)才能申請(qǐng)資源,這種方法會(huì)使資源浪費(fèi)嚴(yán)重(有些資源可能僅在運(yùn)行初期或結(jié)束時(shí)才使用,甚至根本不使用)。
允許進(jìn)程獲取初期所需資源后,便開(kāi)始運(yùn)行,運(yùn)行過(guò)程中再逐步釋放自己占有的資源,比如有一個(gè)進(jìn)程的任務(wù)是把數(shù)據(jù)復(fù)制到磁盤中再打印,前期只需獲得磁盤資源而不需要獲得打印機(jī)資源,待復(fù)制完畢后再釋放掉磁盤資源。這種方法比第一種方法好,會(huì)使資源利用率上升。
2、破壞不可搶占條件
這種方法代價(jià)大,實(shí)現(xiàn)復(fù)雜。
3、破壞循壞等待條件
對(duì)各進(jìn)程請(qǐng)求資源的順序做一個(gè)規(guī)定,避免相互等待。這種方法對(duì)資源的利用率比前兩種都高,但是前期要為設(shè)備指定序號(hào),新設(shè)備加入會(huì)有一個(gè)問(wèn)題,其次對(duì)用戶編程也有限制。

死鎖,基本就是資源不夠,互相需要對(duì)方資源卻不肯放棄自身資源。N線程訪問(wèn)N資源,為了避免死鎖,可以為其加鎖并指定獲取鎖的順序,這樣線程按照順序加鎖訪問(wèn)資源,依次使用依次釋放,可以避免死鎖。

使用多線程的時(shí)候,一種非常簡(jiǎn)單的避免死鎖的方式就是:指定獲取鎖的順序,并強(qiáng)制線程按照指定的順序獲取鎖。因此,如果所有的線程都是以同樣的順序加鎖和釋放鎖,就不會(huì)出現(xiàn)死鎖了??梢源_保N個(gè)線程可以訪問(wèn)N個(gè)資源同時(shí)又不導(dǎo)致死鎖了。

16. ThreadLocal 是什么?有哪些使用場(chǎng)景?

16.1 概述

ThreadLocal,即線程變量,是一個(gè)以 ThreadLocal 對(duì)象為鍵、任意對(duì)象為值的存儲(chǔ)結(jié)構(gòu)。
ThreadLocal是各線程將值存入該線程的map中,以ThreadLocal自身作為key,需要用時(shí)獲得的是該線程之前存入的值,各個(gè)線程的數(shù)據(jù)互不干擾。如果存入的是共享變量,那取出的也是共享變量,并發(fā)問(wèn)題還是存在的。

16.2 底層原理

ThreadLocal 底層是通過(guò)ThreadLocalMap數(shù)據(jù)結(jié)構(gòu)來(lái)實(shí)現(xiàn)的,每個(gè)線程中都有一個(gè)ThreadLocalMap數(shù)據(jù)結(jié)構(gòu)。

在ThreadLoalMap中,也是初始化一個(gè)大小16的Entry數(shù)組,Entry對(duì)象用來(lái)保存每一個(gè)key-value鍵值對(duì),只不過(guò)這里的key永遠(yuǎn)都是ThreadLocal對(duì)象,ThreadLoalMap的Entry是繼承WeakReference,和HashMap很大的區(qū)別是,Entry中沒(méi)有next字段,所以就不存在鏈表的情況了。

image.png

因?yàn)闆](méi)有鏈表,所以hash沖突時(shí),會(huì)多次尋址。

16.3 內(nèi)存泄漏

從上面介紹,我們知道每個(gè)Thread中都存在一個(gè)ThreadLocalMap,ThreadLocalMap的key為threadLocal實(shí)例,value為任意對(duì)象。
####### 16.3.1 原因
ThreadLocal為線程變量,即在線程的生命周期中這個(gè)變量都不會(huì)被顯示的回收,但是我們通常只會(huì)在一段時(shí)間內(nèi)使用這個(gè)變量,不能被回收的話,太浪費(fèi)內(nèi)存空間了,同時(shí),如果是下面這種數(shù)據(jù)庫(kù)連接和Session管理應(yīng)用的話,線程一直存活,那threadlocal一直不能被釋放,會(huì)發(fā)送內(nèi)存泄漏。

java為了防止出現(xiàn)這種情況,把ThreadLocalMap的key設(shè)為弱引用指向threadlocal,如下

static class ThreadLocalMap {
     
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
}

當(dāng)把threadlocal實(shí)例置為null以后,沒(méi)有任何強(qiáng)引用指向threadlocal實(shí)例,所以threadlocal就可以順利被gc回收。

現(xiàn)在threadlocal能被及時(shí)回收了,但是ThreadLocalMap中的value卻沒(méi)有被回收,因?yàn)榇嬖谝粭l從current thread連接過(guò)來(lái)的強(qiáng)引用,而這塊value永遠(yuǎn)不會(huì)被訪問(wèn)到, 所以存在著內(nèi)存泄露。
只有當(dāng)前thread結(jié)束以后,current thread就不會(huì)存在棧中,強(qiáng)引用斷開(kāi),Current Thread, ThreadLocalMap, value將全部被GC回收。
####### 16.3.2 解決方案
當(dāng)線程的某個(gè)ThreadLocal使用完了,馬上調(diào)用threadlocal的remove方法,那就啥事沒(méi)有了!

16.4 使用場(chǎng)景

ThreadLocal是用來(lái)維護(hù)本線程的變量的,并不能解決共享變量的并發(fā)問(wèn)題。
ThreadLocal既然不能解決并發(fā)問(wèn)題,那么它適用的場(chǎng)景是什么呢?
ThreadLocal的主要用途是為了保持線程自身對(duì)象和避免參數(shù)傳遞,主要適用場(chǎng)景是按線程多實(shí)例(每個(gè)線程對(duì)應(yīng)一個(gè)實(shí)例)的對(duì)象的訪問(wèn),并且這個(gè)對(duì)象很多地方都要用到。
最常見(jiàn)的ThreadLocal使用場(chǎng)景為 用來(lái)解決數(shù)據(jù)庫(kù)連接、Session管理等。如:

#數(shù)據(jù)庫(kù)連接:
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
    public Connection initialValue() {  
        return DriverManager.getConnection(DB_URL);  
    }  
};  
  
public static Connection getConnection() {  
    return connectionHolder.get();  
}  

#Session管理:
private static final ThreadLocal threadSession = new ThreadLocal();  
  
public static Session getSession() throws InfrastructureException {  
    Session s = (Session) threadSession.get();  
    try {  
        if (s == null) {  
            s = getSessionFactory().openSession();  
            threadSession.set(s);  
        }  
    } catch (HibernateException ex) {  
        throw new InfrastructureException(ex);  
    }  
    return s;  
}  

個(gè)人認(rèn)為使用ThreadLocal的場(chǎng)景最好滿足兩個(gè)條件,一是該對(duì)象不需要在多線程之間共享;二是該對(duì)象需要在線程內(nèi)被傳遞

17. 說(shuō)一下 synchronized 底層實(shí)現(xiàn)原理?

synchronized的語(yǔ)義底層是通過(guò)一個(gè)monitor的對(duì)象來(lái)完成。
其實(shí)wait/notify等方法也依賴于monitor對(duì)象,這就是為什么只有在同步的塊或者方法中才能調(diào)用wait/notify等方法,否則會(huì)拋出java.lang.IllegalMonitorStateException的異常的原因。

涉及兩條指令:(1)monitorenter

每個(gè)對(duì)象有一個(gè)監(jiān)視器鎖(monitor)。當(dāng)monitor被占用時(shí)就會(huì)處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時(shí)嘗試獲取monitor的所有權(quán),過(guò)程如下:

1、如果monitor的進(jìn)入數(shù)為0,則該線程進(jìn)入monitor,然后將進(jìn)入數(shù)設(shè)置為1,該線程即為monitor的所有者。

2、如果線程已經(jīng)占有該monitor,只是重新進(jìn)入,則進(jìn)入monitor的進(jìn)入數(shù)加1。

3、如果其他線程已經(jīng)占用了monitor,則該線程進(jìn)入阻塞狀態(tài),直到monitor的進(jìn)入數(shù)為0,再重新嘗試獲取monitor的所有權(quán)。

(2)monitorexit

執(zhí)行monitorexit的線程必須是objectref所對(duì)應(yīng)的monitor的所有者。

指令執(zhí)行時(shí),monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(shù)為0,那線程退出monitor,不再是這個(gè)monitor的所有者。其他被這個(gè)monitor阻塞的線程可以嘗試去獲取這個(gè)
monitor 的所有權(quán)。

18. synchronized 和 volatile 的區(qū)別是什么?

首先需要理解線程安全的兩個(gè)方面:執(zhí)行控制內(nèi)存可見(jiàn)。

執(zhí)行控制的目的是控制代碼執(zhí)行(順序)及是否可以并發(fā)執(zhí)行。

內(nèi)存可見(jiàn)控制的是線程執(zhí)行結(jié)果在內(nèi)存中對(duì)其它線程的可見(jiàn)性。根據(jù)Java內(nèi)存模型
的實(shí)現(xiàn),線程在具體執(zhí)行時(shí),會(huì)先拷貝主存數(shù)據(jù)到線程本地(CPU緩存),操作完成后再把結(jié)果從線程本地刷到主存。

synchronized關(guān)鍵字解決的是執(zhí)行控制的問(wèn)題,它會(huì)阻止其它線程獲取當(dāng)前對(duì)象的監(jiān)控鎖,這樣就使得當(dāng)前對(duì)象中被synchronized關(guān)鍵字保護(hù)的代碼塊無(wú)法被其它線程訪問(wèn),也就無(wú)法并發(fā)執(zhí)行。更重要的是,synchronized還會(huì)創(chuàng)建一個(gè)內(nèi)存屏障,內(nèi)存屏障指令保證了所有CPU操作結(jié)果都會(huì)直接刷到主存中,從而保證了操作的內(nèi)存可見(jiàn)性,同時(shí)也使得先獲得這個(gè)鎖的線程的所有操作,都happens-before于隨后獲得這個(gè)鎖的線程的操作。

volatile關(guān)鍵字解決的是內(nèi)存可見(jiàn)性的問(wèn)題,會(huì)使得所有對(duì)volatile變量的讀寫都會(huì)直接刷到主存,即保證了變量的可見(jiàn)性。這樣就能滿足一些對(duì)變量可見(jiàn)性有要求而對(duì)讀取順序沒(méi)有要求的需求。

區(qū)別
volatile本質(zhì)是在告訴jvm當(dāng)前變量在寄存器(工作內(nèi)存)中的值是不確定的,需要從主存中讀??; synchronized則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問(wèn)該變量,其他線程被阻塞住。
volatile僅能使用在變量級(jí)別;synchronized則可以使用在變量、方法、和類級(jí)別的
volatile僅能實(shí)現(xiàn)變量的修改可見(jiàn)性,不能保證原子性;而synchronized則可以保證變量的修改可見(jiàn)性和原子性
volatile不會(huì)造成線程的阻塞;synchronized可能會(huì)造成線程的阻塞。
volatile標(biāo)記的變量不會(huì)被編譯器優(yōu)化;synchronized標(biāo)記的變量可以被編譯器優(yōu)化

19. synchronized 和 Lock 有什么區(qū)別?

1)Lock是一個(gè)接口,而synchronized是Java中的關(guān)鍵字,synchronized是內(nèi)置的語(yǔ)言實(shí)現(xiàn);

2)synchronized在發(fā)生異常時(shí),會(huì)自動(dòng)釋放線程占有的鎖,因此不會(huì)導(dǎo)致死鎖現(xiàn)象發(fā)生;而Lock在發(fā)生異常時(shí),如果沒(méi)有主動(dòng)通過(guò)unLock()去釋放鎖,則很可能造成死鎖現(xiàn)象,因此使用Lock時(shí)需要在finally塊中釋放鎖;

3)Lock可以讓等待鎖的線程響應(yīng)中斷,而synchronized卻不行,使用synchronized時(shí),等待的線程會(huì)一直等待下去,不能夠響應(yīng)中斷;

4)通過(guò)Lock可以知道有沒(méi)有成功獲取鎖,而synchronized卻無(wú)法辦到。

5)Lock可以提高多個(gè)線程進(jìn)行讀操作的效率。

在性能上來(lái)說(shuō),如果競(jìng)爭(zhēng)資源不激烈,兩者的性能是差不多的,而當(dāng)競(jìng)爭(zhēng)資源非常激烈時(shí)(即有大量線程同時(shí)競(jìng)爭(zhēng)),此時(shí)Lock的性能要遠(yuǎn)遠(yuǎn)優(yōu)于synchronized。所以說(shuō),在具體使用時(shí)要根據(jù)適當(dāng)情況選擇。

55. synchronized 和 ReentrantLock 區(qū)別是什么?

ReenTrantLock可重入鎖(和synchronized的區(qū)別)總結(jié)

可重入性:

從名字上理解,ReenTrantLock的字面意思就是再進(jìn)入的鎖,其實(shí)synchronized關(guān)鍵字所使用的鎖也是可重入的,兩者關(guān)于這個(gè)的區(qū)別不大。兩者都是同一個(gè)線程每進(jìn)入一次,鎖的計(jì)數(shù)器都自增1,所以要等到鎖的計(jì)數(shù)器下降為0時(shí)才能釋放鎖。

鎖的實(shí)現(xiàn):

Synchronized是依賴于JVM實(shí)現(xiàn)的,而ReenTrantLock是JDK實(shí)現(xiàn)的,有什么區(qū)別,說(shuō)白了就類似于操作系統(tǒng)來(lái)控制實(shí)現(xiàn)和用戶自己敲代碼實(shí)現(xiàn)的區(qū)別。前者的實(shí)現(xiàn)是比較難見(jiàn)到的,后者有直接的源碼可供閱讀。

性能的區(qū)別:

在Synchronized優(yōu)化以前,synchronized的性能是比ReenTrantLock差很多的,但是自從Synchronized引入了偏向鎖,輕量級(jí)鎖(自旋鎖)后,兩者的性能就差不多了,在兩種方法都可用的情況下,官方甚至建議使用synchronized,其實(shí)synchronized的優(yōu)化我感覺(jué)就借鑒了ReenTrantLock中的CAS技術(shù)。都是試圖在用戶態(tài)就把加鎖問(wèn)題解決,避免進(jìn)入內(nèi)核態(tài)的線程阻塞。

功能區(qū)別:

便利性:很明顯Synchronized的使用比較方便簡(jiǎn)潔,并且由編譯器去保證鎖的加鎖和釋放,而ReenTrantLock需要手工聲明來(lái)加鎖和釋放鎖,為了避免忘記手工釋放鎖造成死鎖,所以最好在finally中聲明釋放鎖。

鎖的細(xì)粒度和靈活度:很明顯ReenTrantLock優(yōu)于Synchronized

ReenTrantLock獨(dú)有的能力:

  1. ReenTrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。

  2. ReenTrantLock提供了一個(gè)Condition(條件)類,用來(lái)實(shí)現(xiàn)分組喚醒需要喚醒的線程們,而不是像synchronized要么隨機(jī)喚醒一個(gè)線程要么喚醒全部線程。

  3. ReenTrantLock提供了一種能夠中斷等待鎖的線程的機(jī)制,通過(guò)lock.lockInterruptibly()來(lái)實(shí)現(xiàn)這個(gè)機(jī)制。

ReenTrantLock實(shí)現(xiàn)的原理:
簡(jiǎn)單來(lái)說(shuō),ReenTrantLock的實(shí)現(xiàn)是一種自旋鎖,通過(guò)循環(huán)調(diào)用CAS操作來(lái)實(shí)現(xiàn)加鎖。它的性能比較好也是因?yàn)楸苊饬耸咕€程進(jìn)入內(nèi)核態(tài)的阻塞狀態(tài)。想盡辦法避免線程進(jìn)入內(nèi)核的阻塞狀態(tài)是我們?nèi)シ治龊屠斫怄i設(shè)計(jì)的關(guān)鍵鑰匙。

20. 說(shuō)一下 atomic 的原理?

在多線程的場(chǎng)景中,我們需要保證數(shù)據(jù)安全,就會(huì)考慮同步的方案,通常會(huì)使用synchronized或者lock來(lái)處理,使用了synchronized意味著內(nèi)核態(tài)的一次切換。這是一個(gè)很重的操作。

有沒(méi)有一種方式,可以比較便利的實(shí)現(xiàn)一些簡(jiǎn)單的數(shù)據(jù)同步,比如計(jì)數(shù)器等等。concurrent包下的atomic提供我們這么一種輕量級(jí)的數(shù)據(jù)同步的選擇。

優(yōu)缺點(diǎn)
CAS相對(duì)于其他鎖,不會(huì)進(jìn)行內(nèi)核態(tài)操作,有著一些性能的提升。但同時(shí)引入自旋,當(dāng)鎖競(jìng)爭(zhēng)較大的時(shí)候,自旋次數(shù)會(huì)增多。cpu資源會(huì)消耗很高。

換句話說(shuō),CAS+自旋適合使用在低并發(fā)有同步數(shù)據(jù)的應(yīng)用場(chǎng)景。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容