1.1 并行和并發(fā)有什么區(qū)別?
并發(fā)(concurrency)和并行(parallellism)是:
- 并行是指兩個或者多個事件在同一時刻發(fā)生秸侣;而并發(fā)是指兩個或多個事件在同一時間間隔發(fā)生愚铡。
- 并行是在不同實體上的多個事件,并發(fā)是在同一實體上的多個事件。比如在單核CPU系統(tǒng)上瓷叫,只可能存在并發(fā)而不可能存在并行拢军。
- 并行是在一臺處理器上“同時”處理多個任務(wù)楞陷,在多臺處理器上同時處理多個任務(wù)。如hadoop分布式集群
所以并發(fā)編程的目標(biāo)是充分的利用處理器的每一個核茉唉,以達(dá)到最高的處理性能固蛾。那為什么并發(fā)就能充分利用cpu的執(zhí)行能力
首先執(zhí)行多個任務(wù)如果是串行執(zhí)行那么cpu一定會存在等待一個任務(wù)執(zhí)行完再去執(zhí)行下一個任務(wù),但是如果是并發(fā)開啟多個線程去分別執(zhí)行不同的任務(wù)的時候度陆,這個時候便可以充分的利用cpu艾凯,多個線程進(jìn)行切換去搶占cpu,cpu的空閑時間就會減少懂傀。
1.2 多線程 VS 高并發(fā)
多線程是完成任務(wù)的一種方法趾诗,高并發(fā)是系統(tǒng)運行的一種狀態(tài),通過多線程有助于系統(tǒng)承受高并發(fā)的狀態(tài)的實現(xiàn)蹬蚁。
高并發(fā)是系統(tǒng)運行過程中遇到的一種“短時間內(nèi)遇到大量的操作請求” 的情況恃泪,主要發(fā)生在web系統(tǒng)集中大量訪問或者socket端口集中行收到大量請求(例如12306搶票;天貓雙十一活動)犀斋。該情況會導(dǎo)致系統(tǒng)在這段時間內(nèi)大量操作贝乎,例如對資源的請求,對數(shù)據(jù)庫的集中操作等叽粹。如果并發(fā)處理不好览效,不僅降低了客戶體驗度(請求時間過長) ,同時可能導(dǎo)致宕機(jī)虫几,系統(tǒng)停止工作等锤灿。如果想要系統(tǒng)適應(yīng)高并發(fā)的狀態(tài),則需要從辆脸,硬件但校,軟件,網(wǎng)絡(luò)每强,系統(tǒng)架構(gòu)始腾,開發(fā)語言的選取,數(shù)據(jù)結(jié)構(gòu)的運用空执,算法優(yōu)化浪箭,數(shù)據(jù)庫優(yōu)化等。辨绊。奶栖。而多線程只是解決方案其中之一。
2. 線程和進(jìn)程的區(qū)別?
進(jìn)程:是執(zhí)行中一段程序宣鄙,即一旦程序被載入到內(nèi)存中并準(zhǔn)備執(zhí)行袍镀,它就是一個進(jìn)程。進(jìn)程是表示資源分配的的基本概念冻晤,又是調(diào)度運行的基本單位苇羡,是系統(tǒng)中的并發(fā)執(zhí)行的單位。
線程:單個進(jìn)程中執(zhí)行中每個任務(wù)就是一個線程鼻弧。線程是進(jìn)程中執(zhí)行運算的最小單位设江。
一個線程只能屬于一個進(jìn)程,但是一個進(jìn)程可以擁有多個線程攘轩。多線程處理就是允許一個進(jìn)程中在同一時刻執(zhí)行多個任務(wù)叉存。
進(jìn)程和線程的主要差別在于它們是不同的操作系統(tǒng)資源管理方式。進(jìn)程有獨立的地址空間度帮,一個進(jìn)程崩潰后歼捏,在保護(hù)模式下不會對其它進(jìn)程產(chǎn)生影響,而線程只是一個進(jìn)程中的不同執(zhí)行路徑笨篷。線程有自己的堆棧和局部變量亿汞,但線程之間沒有單獨的地址空間获高,一個線程死掉就等于整個進(jìn)程死掉衰絮,所以多進(jìn)程的程序要比多線程的程序健壯躯泰,但在進(jìn)程切換時,耗費資源較大安聘,效率要差一些。但對于一些要求同時進(jìn)行并且又要共享某些變量的并發(fā)操作瓢棒,只能用線程浴韭,不能用進(jìn)程。
- 簡而言之,一個程序至少有一個進(jìn)程,一個進(jìn)程至少有一個線程.**
- 線程的劃分尺度小于進(jìn)程脯宿,使得多線程程序的并發(fā)性高念颈。
- 另外,進(jìn)程在執(zhí)行過程中擁有獨立的內(nèi)存單元连霉,而多個線程共享內(nèi)存榴芳,從而極大地提高了程序的運行效率。
- 線程在執(zhí)行過程中與進(jìn)程還是有區(qū)別的跺撼。每個獨立的線程有一個程序運行的入口窟感、順序執(zhí)行序列和程序的出口。但是線程不能夠獨立執(zhí)行歉井,必須依存在應(yīng)用程序中柿祈,由應(yīng)用程序提供多個線程執(zhí)行控制。
- 從邏輯角度來看,多線程的意義在于一個應(yīng)用程序中躏嚎,有多個執(zhí)行部分可以同時執(zhí)行蜜自。但操作系統(tǒng)并沒有將多個線程看做多個獨立的應(yīng)用,來實現(xiàn)進(jìn)程的調(diào)度和管理以及資源分配卢佣。這就是進(jìn)程和線程的重要區(qū)別重荠。
- 線程和進(jìn)程在使用上各有優(yōu)缺點:
線程執(zhí)行開銷小,但不利于資源的管理和保護(hù)虚茶;
而進(jìn)程正相反戈鲁。
同時,線程適合于在SMP機(jī)器上運行媳危,而進(jìn)程則可以跨機(jī)器遷移荞彼。
- 線程和進(jìn)程在使用上各有優(yōu)缺點:
3. 守護(hù)線程是什么?
守護(hù)線程(即daemon thread)待笑,是個服務(wù)線程鸣皂,準(zhǔn)確地來說就是服務(wù)其他的線程,這是它的作用——而其他的線程只有一種暮蹂,那就是用戶線程寞缝。所以java里線程分2種,
1仰泻、守護(hù)線程荆陆,比如垃圾回收線程,就是最典型的守護(hù)線程集侯。
2被啼、用戶線程,就是應(yīng)用程序里的自定義線程棠枉。
將線程轉(zhuǎn)換為守護(hù)線程可以通過調(diào)用Thread對象的setDaemon(true)方法來實現(xiàn)浓体。在使用守護(hù)線程時需要注意一下幾點:
(1) thread.setDaemon(true)必須在thread.start()之前設(shè)置,否則會跑出一個IllegalThreadStateException異常辈讶。你不能把正在運行的常規(guī)線程設(shè)置為守護(hù)線程命浴。
(2) 在Daemon線程中產(chǎn)生的新線程也是Daemon的。
(3) 守護(hù)線程應(yīng)該永遠(yuǎn)不去訪問固有資源贱除,如文件生闲、數(shù)據(jù)庫,因為它會在任何時候甚至在一個操作的中間發(fā)生中斷
守護(hù)線程和用戶線程的沒啥本質(zhì)的區(qū)別:唯一的不同之處就在于虛擬機(jī)的離開:如果用戶線程已經(jīng)全部退出運行了月幌,只剩下守護(hù)線程存在了碍讯,虛擬機(jī)也就退出了。 因為沒有了被守護(hù)者扯躺,守護(hù)線程也就沒有工作可做了冲茸,也就沒有繼續(xù)運行程序的必要了屯阀。
4. 創(chuàng)建線程有哪幾種方式?
一轴术、繼承Thread類創(chuàng)建線程類
二难衰、通過Runnable接口創(chuàng)建線程類
三、通過Callable和Future創(chuàng)建線程
采用實現(xiàn)Runnable逗栽、Callable接口的方式創(chuàng)見多線程時盖袭,優(yōu)勢是:
線程類只是實現(xiàn)了Runnable接口或Callable接口,還可以繼承其他類彼宠。
在這種方式下鳄虱,多個線程可以共享同一個target對象,所以非常適合多個相同線程來處理同一份資源的情況凭峡,從而可以將CPU拙已、代碼和數(shù)據(jù)分開,形成清晰的模型摧冀,較好地體現(xiàn)了面向?qū)ο蟮乃枷搿?/p>
劣勢是:
編程稍微復(fù)雜倍踪,如果要訪問當(dāng)前線程,則必須使用Thread.currentThread()方法索昂。
使用繼承Thread類的方式創(chuàng)建多線程時優(yōu)勢是:
編寫簡單建车,如果需要訪問當(dāng)前線程,則無需使用Thread.currentThread()方法椒惨,直接使用this即可獲得當(dāng)前線程缤至。
劣勢是:
線程類已經(jīng)繼承了Thread類,所以不能再繼承其他父類康谆。
5. 說一下 runnable 和 callable 有什么區(qū)別领斥?
相同點
- 都是接口
- 都可以編寫多線程程序
- 都采用Thread.start()啟動線程
不同點
- Runnable沒有返回值;Callable可以返回執(zhí)行結(jié)果沃暗,是個泛型戒突,和Future、FutureTask配合可以用來獲取異步執(zhí)行的結(jié)果
- Callable接口的call()方法允許拋出異常描睦;Runnable的run()方法異常只能在內(nèi)部消化,不能往上繼續(xù)拋
注:Callalbe接口支持返回執(zhí)行結(jié)果导而,需要調(diào)用FutureTask.get()得到忱叭,此方法會阻塞主進(jìn)程的繼續(xù)往下執(zhí)行,如果不調(diào)用不會阻塞今艺。
Thread(抽象類) 和 runnable(接口)區(qū)別
- 1韵丑、由于Java不允許多繼承,因此實現(xiàn)了Runnable接口可以再繼承其他類虚缎,但是Thread明顯不可以撵彻;
- 2钓株、Runnable可以實現(xiàn)多個相同的程序代碼的線程去共享同一個資源,而Thread并不是不可以陌僵,而是相比于Runnable來說轴合,不太適合。
比說碗短,在實現(xiàn)runnable接口的類A中受葛,定義一個變量B,然后使用new Thread(A).start()啟動線程偎谁,通過多線程來共享A類中的變量B总滩;但是通過Thread就不能這樣做,每次new Thread類C巡雨,C中的變量D都是新的闰渔,不共享的。如果需要共享铐望,則和繼承runnable一樣冈涧,在繼承thread的類A中,定義一個變量B,然后然后使用new Thread(A).start()啟動線程蝌以。
其實我們從Thread源碼中也可以看到炕舵,當(dāng)以Thread方式去實現(xiàn)資源共享時,實際上源碼內(nèi)部是將thread向下轉(zhuǎn)型為了Runnable跟畅,實際上內(nèi)部依然是以Runnable形式去實現(xiàn)的資源共享
在程序開發(fā)中只要是多線程肯定永遠(yuǎn)以實現(xiàn)Runnable接口為主咽筋。
6. 線程有哪些狀態(tài)?
Java中的線程的生命周期大體可分為5種狀態(tài)徊件。
- 新建(NEW):新創(chuàng)建了一個線程對象奸攻。
- 可運行(RUNNABLE):線程對象創(chuàng)建后,其他線程(比如main線程)調(diào)用了該對象的start()方法虱痕。該狀態(tài)的線程位于可運行線程池中睹耐,等待被線程調(diào)度選中,獲取cpu 的使用權(quán) 部翘。
- 運行(RUNNING):可運行狀態(tài)(runnable)的線程獲得了cpu 時間片(timeslice) 硝训,執(zhí)行程序代碼。
-
- 阻塞(BLOCKED):阻塞狀態(tài)是指線程因為某種原因放棄了cpu 使用權(quán)新思,也即讓出了cpu timeslice窖梁,暫時停止運行。直到線程進(jìn)入可運行(runnable)狀態(tài)夹囚,才有機(jī)會再次獲得cpu timeslice 轉(zhuǎn)到運行(running)狀態(tài)纵刘。阻塞的情況分三種:
- (一). 等待阻塞:運行(running)的線程執(zhí)行o.wait()方法,JVM會把該線程放入等待隊列(waitting queue)中荸哟。
- (二). 同步阻塞:運行(running)的線程在獲取對象的同步鎖時假哎,若該同步鎖被別的線程占用瞬捕,則JVM會把該線程放入鎖池(lock pool)中。
- (三). 其他阻塞:運行(running)的線程執(zhí)行Thread.sleep(long ms)或t.join()方法舵抹,或者發(fā)出了I/O請求時肪虎,JVM會把該線程置為阻塞狀態(tài)。當(dāng)sleep()狀態(tài)超時掏父、join()等待線程終止或者超時笋轨、或者I/O處理完畢時,線程重新轉(zhuǎn)入可運行(runnable)狀態(tài)赊淑。
- 死亡(DEAD):線程run()爵政、main() 方法執(zhí)行結(jié)束,或者因異常退出了run()方法陶缺,則該線程結(jié)束生命周期钾挟。死亡的線程不可再次復(fù)生。
7. sleep() 和 wait() 有什么區(qū)別饱岸?
1掺出、這兩個方法來自不同的類分別是,sleep來自Thread類苫费,和wait來自O(shè)bject類汤锨。
2、sleep() 和 wait() 的區(qū)別就是 調(diào)用sleep方法的線程不會釋放對象鎖百框,而調(diào)用wait() 方法會釋放對象鎖
sleep是Thread的靜態(tài)類方法闲礼,誰調(diào)用的誰去睡覺,即使在a線程里調(diào)用了b的sleep方法铐维,實際上還是a去睡覺柬泽,要讓b線程睡覺要在b的代碼中調(diào)用sleep。
wait()是Object類的方法嫁蛇,當(dāng)一個線程執(zhí)行到wait方法時锨并,它就進(jìn)入到一個和該對象相關(guān)的等待池,同時釋放對象的機(jī)鎖睬棚,使得其他線程能夠訪問第煮,可以通過notify,notifyAll方法來喚醒等待的線程
sleep不出讓系統(tǒng)資源抑党;wait是進(jìn)入線程等待池等待包警,出讓系統(tǒng)資源,其他線程可以占用CPU新荤。一般wait不會加時間限制,因為如果wait線程的運行資源不夠台汇,再出來也沒用苛骨,要等待其他線程調(diào)用notify/notifyAll喚醒等待池中的所有線程篱瞎,才會進(jìn)入就緒隊列等待OS分配系統(tǒng)資源。sleep(milliseconds)可以用時間指定使它自動喚醒過來痒芝,如果時間不到只能調(diào)用interrupt()強(qiáng)行打斷俐筋。
Thread.Sleep(0)的作用是“觸發(fā)操作系統(tǒng)立刻重新進(jìn)行一次CPU競爭”。
- 3严衬、使用范圍:wait澄者,notify和notifyAll只能在同步控制方法或者同步控制塊里面使用,而sleep可以在任何地方使用
synchronized(x){
x.notify()
//或者wait()
}
- 4请琳、sleep必須捕獲異常粱挡,而wait,notify和notifyAll不需要捕獲異常
8. notify()和 notifyAll()有什么區(qū)別俄精?
如果線程調(diào)用了對象的 wait()方法询筏,那么線程便會處于該對象的等待池中,等待池中的線程不會去競爭該對象的鎖竖慧。
當(dāng)有線程調(diào)用了對象的 notifyAll()方法(喚醒所有 wait 線程)或 notify()方法(只隨機(jī)喚醒一個 wait 線程)嫌套,被喚醒的的線程便會進(jìn)入該對象的鎖池中,鎖池中的線程會去競爭該對象鎖圾旨。也就是說踱讨,調(diào)用了notify后只要一個線程會由等待池進(jìn)入鎖池,而notifyAll會將該對象等待池內(nèi)的所有線程移動到鎖池中砍的,等待鎖競爭
優(yōu)先級高的線程競爭到對象鎖的概率大痹筛,假若某線程沒有競爭到該對象鎖,它還會留在鎖池中挨约,唯有線程再次調(diào)用 wait()方法味混,它才會重新回到等待池中。而競爭到對象鎖的線程則繼續(xù)往下執(zhí)行诫惭,直到執(zhí)行完了 synchronized 代碼塊翁锡,它會釋放掉該對象鎖,這時鎖池中的線程會繼續(xù)競爭該對象鎖夕土。
綜上馆衔,所謂喚醒線程,另一種解釋可以說是將線程由等待池移動到鎖池怨绣,notifyAll調(diào)用后角溃,會將全部線程由等待池移到鎖池,然后參與鎖的競爭篮撑,競爭成功則繼續(xù)執(zhí)行减细,如果不成功則留在鎖池等待鎖被釋放后再次參與競爭。而notify只會喚醒一個線程赢笨。
notify可能會導(dǎo)致死鎖未蝌,而notifyAll則不會
9. 線程的 run()和 start()有什么區(qū)別驮吱?
每個線程都有要執(zhí)行的任務(wù)。線程的任務(wù)處理邏輯可以在Tread類的run實例方法中直接實現(xiàn)或通過該方法進(jìn)行調(diào)用萧吠,因此
run()相當(dāng)于線程的任務(wù)處理邏輯的入口方法左冬,它由Java虛擬機(jī)在運行相應(yīng)線程時直接調(diào)用,而不是由應(yīng)用代碼進(jìn)行調(diào)用纸型。
start()的作用是啟動相應(yīng)的線程拇砰。啟動一個線程實際是請求Java虛擬機(jī)運行相應(yīng)的線程,而這個線程何時能夠運行是由線程調(diào)度器決定的狰腌。start()調(diào)用結(jié)束并不表示相應(yīng)線程已經(jīng)開始運行除破,這個線程可能稍后運行,也可能永遠(yuǎn)也不會運行癌别。
10.創(chuàng)建線程池有哪幾種方式皂岔?
Java通過Executors提供四種線程池,分別為:
- newCachedThreadPool:創(chuàng)建一個可緩存線程池展姐,如果線程池長度超過處理需要躁垛,可靈活回收空閑線程,若無可回收圾笨,則新建線程教馆,隊列是SynchronousQueue<Runnable>;
- newFixedThreadPool 創(chuàng)建一個定長線程池擂达,可控制線程最大并發(fā)數(shù)土铺,超出的線程會在隊列中等待,隊列是LinkedBlockingQueue<Runnable>板鬓;
- newScheduledThreadPool 創(chuàng)建一個定長線程池悲敷,支持定時及周期性任務(wù)執(zhí)行,隊列是DelayedWorkQueue()俭令;
- newSingleThreadExecutor 創(chuàng)建一個單線程化的線程池后德,它只會用唯一的工作線程來執(zhí)行任務(wù),保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級)執(zhí)行抄腔,隊列是LinkedBlockingQueue<Runnable>瓢湃;
阿里的 Java開發(fā)手冊,上面有線程池的一個建議:線程池不允許使用 Executors 去創(chuàng)建赫蛇,而是通過 ThreadPoolExecutor 的方式绵患,這樣的處理方式讓寫的同學(xué)更加明確線程池的運行規(guī)則,規(guī)避資源耗盡的風(fēng)險悟耘。Executors利用工廠模式向我們提供了4種線程池實現(xiàn)方式落蝙,但是并不推薦使用,原因是使用Executors創(chuàng)建線程池不會傳入拒絕策略這個參數(shù)而使用默認(rèn)值所以我們常常忽略這一參數(shù),而且默認(rèn)使用的參數(shù)會導(dǎo)致資源浪費筏勒,不可取赚瘦。
說明:Executors 各個方法的弊端:
1)newFixedThreadPool 和 newSingleThreadExecutor:主要問題是堆積的請求處理隊列可能會耗費非常大的內(nèi)存,甚至 OOM奏寨,因為隊列是無界阻塞隊列LinkedBlockingQueue;
2)newCachedThreadPool 和 newScheduledThreadPool:主要問題是線程數(shù)最大數(shù)是 Integer.MAX_VALUE鹰服,可能會創(chuàng)建數(shù)量非常多的線程病瞳,甚至 OOM。因為SynchronousQueue是一個內(nèi)部只能包含一個元素的隊列悲酷;
11. 線程池中 submit()和 execute()方法有什么區(qū)別套菜?
- 接收的參數(shù)不一樣;submit callable,execute 是runnable
- submit()有返回值设易,而execute()沒有;
例如逗柴,有個validation的task,希望該task執(zhí)行完后告訴我它的執(zhí)行結(jié)果顿肺,是成功還是失敗戏溺,然后繼續(xù)下面的操作。
- submit()有返回值设易,而execute()沒有;
- submit()可以進(jìn)行Exception處理;
例如屠尊,如果task里會拋出checked或者unchecked exception旷祸,而你又希望外面的調(diào)用者能夠感知這些exception并做出及時的處理,那么就需要用到submit讼昆,通過對Future.get()進(jìn)行拋出異常的捕獲托享,然后對其進(jìn)行處理。
- submit()可以進(jìn)行Exception處理;
12.1 在 java 程序中怎么保證多線程的運行安全浸赫?
一闰围、線程安全在三個方面體現(xiàn)
1.原子性:提供互斥訪問,同一時刻只能有一個線程對數(shù)據(jù)進(jìn)行操作既峡,(atomic,synchronized)羡榴;
2.可見性:一個線程對主內(nèi)存的修改可以及時地被其他線程看到,(synchronized,volatile)涧狮;
3.有序性:一個線程觀察其他線程中的指令執(zhí)行順序炕矮,由于指令重排序,該觀察結(jié)果一般雜亂無序者冤,(happens-before原則)肤视。
當(dāng)然由于synchronized和Lock保證每個時刻只有一個線程執(zhí)行同步代碼,所以是線程安全的涉枫,也可以實現(xiàn)這一功能邢滑,但是由于線程是同步執(zhí)行的,所以會影響效率。
原子性:即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷困后,要么就都不執(zhí)行乐纸。JDK里面提供了很多atomic類,AtomicInteger,AtomicLong,AtomicBoolean等等摇予。它們是通過CAS完成原子性
可見性:指當(dāng)多個線程訪問同一個變量時汽绢,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值侧戴。
當(dāng)一個共享變量被volatile修飾時宁昭,它會保證修改的值會立即被更新到主存,當(dāng)有其他線程需要讀取共享變量時酗宋,它會去內(nèi)存中讀取新值积仗。
普通的共享變量不能保證可見性,因為普通共享變量被修改后蜕猫,什么時候被寫入主存是不確定的寂曹,當(dāng)其他線程去讀取時,此時內(nèi)存中可能還是原來的舊值回右,因此無法保證可見性隆圆。
更新主存的步驟:當(dāng)前線程將其他線程的工作內(nèi)存中的緩存變量的緩存行設(shè)置為無效,然后當(dāng)前線程將變量的值跟新到主存翔烁,更新成功后將其他線程的緩存行更新為新的主存地址
其他線程讀取變量時匾灶,發(fā)現(xiàn)自己的緩存行無效,它會等待緩存行對應(yīng)的主存地址被更新之后租漂,然后去對應(yīng)的主存讀取最新的值阶女。
有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
在Java內(nèi)存模型中哩治,允許編譯器和處理器對指令進(jìn)行重排序秃踩,但是重排序過程不會影響到單線程程序的執(zhí)行,卻會影響到多線程并發(fā)執(zhí)行的正確性业筏。
可以通過volatile關(guān)鍵字來保證一定的“有序性”憔杨。
12.2 在 java 程序中如何保證多個線程的執(zhí)行順序?
方法一:創(chuàng)建一個單線程線程池
//創(chuàng)建只有一根線程的線程池蒜胖,保證所有任務(wù)按照指定順序執(zhí)行
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(new A());
executorService.submit(new B());
executorService.submit(new C());
executorService.shutdown();
方法二:使用join()方法消别,等前一個線程執(zhí)行完畢,下一個線程才能執(zhí)行
當(dāng)調(diào)用了t.join()台谢,就必須要等待線程t執(zhí)行完畢后寻狂,才能繼續(xù)執(zhí)行其他線程。這里其實是運用了Java中最頂級對象Object提供的方法wait()朋沮。wait()方法用于線程間通信蛇券,它的含義是通知一個線程等待一下,讓出CPU資源,注意這里是會放棄已經(jīng)占有的資源的纠亚。直到t線程執(zhí)行完畢塘慕,再調(diào)用notify()喚醒當(dāng)前正在運行的線程。
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 辨別線程安全與線程不安全
我們在Java中常常會有某個對象是線程安全的蒂胞,另一個對象是線程不安全的图呢,那么怎么判斷這些對象是線程安全不安全的呢?是什么決定的線程安全問題呢骗随?
線程安全問題都是由全局變量及靜態(tài)變量引起的岳瞭。
若每個線程中對全局變量、靜態(tài)變量只有讀操作蚊锹,而無寫操作,一般來說稚瘾,這個全局變量是線程安全的牡昆;若有多個線程同時對全局變量(靜態(tài)變量)執(zhí)行寫操作——并發(fā)訪問資源,一般都需要考慮線程同步摊欠,否則的話就可能影響線程安全丢烘。
那么怎么解決多線程并發(fā)訪問資源的安全問題呢?
通常有三種方式:同步代碼塊synchronized 些椒、同步方法synchronized和鎖機(jī)制(Lock)
所以播瞳,一個對象是不是線程安全的,直接看其有沒有對全局變量做寫操作就可以免糕,如果有赢乓,則繼續(xù)看其寫操作時有沒有加鎖Lock(或者同步),如果有的話石窑,就是線程安全的牌芋,如果沒有的話,就是線程不安全的松逊。
13. 多線程鎖的升級原理是什么躺屁?
在Java中,鎖共有4種狀態(tài)经宏,級別從低到高依次為:無狀態(tài)鎖犀暑,偏向鎖,輕量級鎖和重量級鎖狀態(tài)烁兰,這幾個狀態(tài)會隨著競爭情況逐漸升級耐亏。鎖可以升級但不能降級。
偏向鎖
偏向鎖的核心思想就是鎖會偏向第一個獲取它的線程沪斟,在接下來的執(zhí)行過程中該鎖沒有其他的線程獲取苹熏,則持有偏向鎖的線程永遠(yuǎn)不需要再進(jìn)行同步。
當(dāng)一個線程訪問同步塊并獲取鎖的時候,會在對象頭和棧幀中的鎖記錄里存儲偏向的線程 ID轨域,以后該線程在進(jìn)入和退出同步塊時不需要進(jìn)行 CAS 操作來加鎖和解鎖袱耽,只需要檢查當(dāng)前 Mark Word 中存儲的線程是否為當(dāng)前線程,如果是干发,則表示已經(jīng)獲得對象鎖朱巨;否則,需要測試 Mark Word 中偏向鎖的標(biāo)志是否為1枉长,如果沒有則使用 CAS 操作競爭鎖冀续,如果設(shè)置了,則嘗試使用 CAS 將對象頭的偏向鎖指向當(dāng)前線程必峰。
需要注意的是洪唐,偏向鎖使用一種等待競爭出現(xiàn)才釋放鎖的機(jī)制,所以當(dāng)有其他線程嘗試獲得鎖時吼蚁,才會釋放鎖凭需。偏向鎖的撤銷,需要等到安全點肝匆。它首先會暫停擁有偏向鎖的線程粒蜈,然后檢查持有偏向鎖的線程是否活著,如果不處于活動狀態(tài)旗国,則將對象頭設(shè)置為無鎖狀態(tài)枯怖;如果依然活動,擁有偏向鎖的棧會被執(zhí)行能曾,遍歷偏向?qū)ο蟮逆i記錄度硝,棧中的鎖記錄和對象頭的Mark Word要么重新偏向其他線程,要么恢復(fù)到無鎖或者標(biāo)記對象不合適作為偏向鎖(膨脹為輕量級鎖)寿冕,最后喚醒暫停的線程塘淑。
輕量級鎖
線程在執(zhí)行同步塊之前,JVM會現(xiàn)在當(dāng)前線程的棧幀中創(chuàng)建用于儲存鎖記錄的空間(LockRecord)蚂斤,并將對象頭的Mark Word信息復(fù)制到鎖記錄中存捺。然后線程嘗試使用 CAS 將對象頭的MarkWord替換為指向鎖記錄的指針。如果成功曙蒸,當(dāng)前線程獲得鎖捌治,并且對象的鎖標(biāo)志位轉(zhuǎn)變?yōu)椤?0”,如果失敗纽窟,表示其他線程競爭鎖肖油,當(dāng)前線程便會嘗試自旋獲取鎖。如果有兩條以上的線程競爭同一個鎖臂港,那么輕量級鎖就不再有效森枪,要膨脹為重量級鎖视搏,鎖標(biāo)志的狀態(tài)變?yōu)椤?0”,MarkWord中儲存的就是指向重量級鎖(互斥量)的指針县袱,后面等待的線程也要進(jìn)入阻塞狀態(tài)浑娜。
輕量級鎖解鎖時,同樣通過CAS操作將對象頭換回來式散。如果成功筋遭,則表示沒有競爭發(fā)生。如果失敗暴拄,說明有其他線程嘗試過獲取該鎖漓滔,鎖同樣會膨脹為重量級鎖。在釋放鎖的同時乖篷,喚醒被掛起的線程响驴。
重量級鎖
重量級鎖(Heavyweight Lock)是將程序運行交出控制權(quán),將線程掛起撕蔼,由操作系統(tǒng)來負(fù)責(zé)線程間的調(diào)度豁鲤,負(fù)責(zé)線程的阻塞和執(zhí)行。這樣會出現(xiàn)頻繁地對線程運行狀態(tài)的切換罕邀,線程的掛起和喚醒,消耗大量的系統(tǒng)資源养距,導(dǎo)致性能低下诉探。
最后看一下,鎖升級的圖示過程:
14. 什么是死鎖棍厌?
死鎖可以這樣理解肾胯,就是互相不讓步不放棄,同時需要對方的資源耘纱。造成互相不滿足資源需求敬肚,也不放棄自身已有資源。死鎖就這樣了束析。
死鎖是指多個進(jìn)程因競爭資源而造成的一種僵局(互相等待)艳馒,若無外力作用,這些進(jìn)程都將無法向前推進(jìn)员寇。
死鎖是指兩個或兩個以上的進(jìn)程在執(zhí)行過程中,因爭奪資源而造成的一種互相等待的現(xiàn)象,若無外力作用,它們都將無法推進(jìn)下去弄慰,如果系統(tǒng)資源充足,進(jìn)程的資源請求都能夠得到滿足蝶锋,死鎖出現(xiàn)的可能性就很低陆爽,否則就會因爭奪有限的資源而陷入死鎖。
產(chǎn)生死鎖的原因主要是:
“饴啤(1) 因為系統(tǒng)資源不足慌闭。
”鹜(2) 進(jìn)程運行推進(jìn)的順序不合適。
÷刻蕖(3) 資源分配不當(dāng)?shù)取?br>
產(chǎn)生死鎖的四個必要條件:
∈」拧(1) 互斥條件:一個資源每次只能被一個進(jìn)程使用预愤。
∈固住(2) 請求與保持條件:一個進(jìn)程因請求資源而阻塞時腿倚,對已獲得的資源保持不放济蝉。
〖自帷(3) 不剝奪條件:進(jìn)程已獲得的資源拇勃,在末使用完之前霉赡,不能強(qiáng)行剝奪辅辩。
〕词隆(4) 循環(huán)等待條件:若干進(jìn)程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系臀栈。
這四個條件是死鎖的必要條件,只要系統(tǒng)發(fā)生死鎖挠乳,這些條件必然成立权薯,而只要上述條件之
一不滿足,就不會發(fā)生死鎖睡扬。
15. 怎么防止死鎖盟蚣?
理解了死鎖的原因,尤其是產(chǎn)生死鎖的四個必要條件卖怜,就可以最大可能地避免屎开、預(yù)防和解除死鎖。所以马靠,在系統(tǒng)設(shè)計奄抽、進(jìn)程調(diào)度等方面注意如何不讓這四個必要條件成立,如何確定資源的合理分配算法甩鳄,避免進(jìn)程永久占據(jù)系統(tǒng)資源逞度。此外,也要防止進(jìn)程在處于等待狀態(tài)
的情況下占用資源妙啃。因此档泽,對資源的分配要給予合理的規(guī)劃。
預(yù)防死鎖揖赴,預(yù)先破壞產(chǎn)生死鎖的四個條件茁瘦。互斥不可能破壞储笑,所以有如下三種方法:
1甜熔、破壞請求和保持條件,
進(jìn)程必須等所有要請求的資源都空閑時才能申請資源突倍,這種方法會使資源浪費嚴(yán)重(有些資源可能僅在運行初期或結(jié)束時才使用腔稀,甚至根本不使用)盆昙。
允許進(jìn)程獲取初期所需資源后,便開始運行焊虏,運行過程中再逐步釋放自己占有的資源淡喜,比如有一個進(jìn)程的任務(wù)是把數(shù)據(jù)復(fù)制到磁盤中再打印,前期只需獲得磁盤資源而不需要獲得打印機(jī)資源诵闭,待復(fù)制完畢后再釋放掉磁盤資源炼团。這種方法比第一種方法好,會使資源利用率上升疏尿。
2瘟芝、破壞不可搶占條件
這種方法代價大,實現(xiàn)復(fù)雜褥琐。
3锌俱、破壞循壞等待條件
對各進(jìn)程請求資源的順序做一個規(guī)定,避免相互等待敌呈。這種方法對資源的利用率比前兩種都高贸宏,但是前期要為設(shè)備指定序號,新設(shè)備加入會有一個問題磕洪,其次對用戶編程也有限制吭练。
死鎖,基本就是資源不夠析显,互相需要對方資源卻不肯放棄自身資源鲫咽。N線程訪問N資源,為了避免死鎖叫榕,可以為其加鎖并指定獲取鎖的順序浑侥,這樣線程按照順序加鎖訪問資源姊舵,依次使用依次釋放晰绎,可以避免死鎖。
使用多線程的時候括丁,一種非常簡單的避免死鎖的方式就是:指定獲取鎖的順序荞下,并強(qiáng)制線程按照指定的順序獲取鎖。因此史飞,如果所有的線程都是以同樣的順序加鎖和釋放鎖尖昏,就不會出現(xiàn)死鎖了」棺剩可以確保N個線程可以訪問N個資源同時又不導(dǎo)致死鎖了抽诉。
16. ThreadLocal 是什么?有哪些使用場景吐绵?
16.1 概述
ThreadLocal迹淌,即線程變量河绽,是一個以 ThreadLocal 對象為鍵、任意對象為值的存儲結(jié)構(gòu)唉窃。
ThreadLocal是各線程將值存入該線程的map中耙饰,以ThreadLocal自身作為key,需要用時獲得的是該線程之前存入的值纹份,各個線程的數(shù)據(jù)互不干擾苟跪。如果存入的是共享變量,那取出的也是共享變量蔓涧,并發(fā)問題還是存在的件已。
16.2 底層原理
ThreadLocal 底層是通過ThreadLocalMap數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)的,每個線程中都有一個ThreadLocalMap數(shù)據(jù)結(jié)構(gòu)蠢笋。
在ThreadLoalMap中拨齐,也是初始化一個大小16的Entry數(shù)組,Entry對象用來保存每一個key-value鍵值對昨寞,只不過這里的key永遠(yuǎn)都是ThreadLocal對象瞻惋,ThreadLoalMap的Entry是繼承WeakReference,和HashMap很大的區(qū)別是援岩,Entry中沒有next字段歼狼,所以就不存在鏈表的情況了。
因為沒有鏈表享怀,所以hash沖突時羽峰,會多次尋址。
16.3 內(nèi)存泄漏
從上面介紹添瓷,我們知道每個Thread中都存在一個ThreadLocalMap梅屉,ThreadLocalMap的key為threadLocal實例,value為任意對象鳞贷。
####### 16.3.1 原因
ThreadLocal為線程變量坯汤,即在線程的生命周期中這個變量都不會被顯示的回收,但是我們通常只會在一段時間內(nèi)使用這個變量搀愧,不能被回收的話惰聂,太浪費內(nèi)存空間了,同時咱筛,如果是下面這種數(shù)據(jù)庫連接和Session管理應(yīng)用的話搓幌,線程一直存活,那threadlocal一直不能被釋放迅箩,會發(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實例置為null以后拐揭,沒有任何強(qiáng)引用指向threadlocal實例罢缸,所以threadlocal就可以順利被gc回收。
現(xiàn)在threadlocal能被及時回收了投队,但是ThreadLocalMap中的value卻沒有被回收枫疆,因為存在一條從current thread連接過來的強(qiáng)引用,而這塊value永遠(yuǎn)不會被訪問到敷鸦, 所以存在著內(nèi)存泄露息楔。
只有當(dāng)前thread結(jié)束以后,current thread就不會存在棧中扒披,強(qiáng)引用斷開值依,Current Thread, ThreadLocalMap, value將全部被GC回收。
####### 16.3.2 解決方案
當(dāng)線程的某個ThreadLocal使用完了碟案,馬上調(diào)用threadlocal的remove方法愿险,那就啥事沒有了!
16.4 使用場景
ThreadLocal是用來維護(hù)本線程的變量的价说,并不能解決共享變量的并發(fā)問題辆亏。
ThreadLocal既然不能解決并發(fā)問題,那么它適用的場景是什么呢鳖目?
ThreadLocal的主要用途是為了保持線程自身對象和避免參數(shù)傳遞扮叨,主要適用場景是按線程多實例(每個線程對應(yīng)一個實例)的對象的訪問,并且這個對象很多地方都要用到领迈。
最常見的ThreadLocal使用場景為 用來解決數(shù)據(jù)庫連接彻磁、Session管理等。如:
#數(shù)據(jù)庫連接:
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;
}
個人認(rèn)為使用ThreadLocal的場景最好滿足兩個條件狸捅,一是該對象不需要在多線程之間共享衷蜓;二是該對象需要在線程內(nèi)被傳遞
17. 說一下 synchronized 底層實現(xiàn)原理?
synchronized的語義底層是通過一個monitor的對象來完成尘喝。
其實wait/notify等方法也依賴于monitor對象磁浇,這就是為什么只有在同步的塊或者方法中才能調(diào)用wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException的異常的原因瞧省。
涉及兩條指令:(1)monitorenter
每個對象有一個監(jiān)視器鎖(monitor)扯夭。當(dāng)monitor被占用時就會處于鎖定狀態(tài)鳍贾,線程執(zhí)行monitorenter指令時嘗試獲取monitor的所有權(quán)鞍匾,過程如下:
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所對應(yīng)的monitor的所有者模闲。
指令執(zhí)行時建瘫,monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(shù)為0尸折,那線程退出monitor啰脚,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個
monitor 的所有權(quán)实夹。
18. synchronized 和 volatile 的區(qū)別是什么橄浓?
首先需要理解線程安全的兩個方面:執(zhí)行控制和內(nèi)存可見。
執(zhí)行控制的目的是控制代碼執(zhí)行(順序)及是否可以并發(fā)執(zhí)行亮航。
內(nèi)存可見控制的是線程執(zhí)行結(jié)果在內(nèi)存中對其它線程的可見性贮配。根據(jù)Java內(nèi)存模型
的實現(xiàn),線程在具體執(zhí)行時塞赂,會先拷貝主存數(shù)據(jù)到線程本地(CPU緩存)泪勒,操作完成后再把結(jié)果從線程本地刷到主存。
synchronized
關(guān)鍵字解決的是執(zhí)行控制的問題宴猾,它會阻止其它線程獲取當(dāng)前對象的監(jiān)控鎖圆存,這樣就使得當(dāng)前對象中被synchronized
關(guān)鍵字保護(hù)的代碼塊無法被其它線程訪問,也就無法并發(fā)執(zhí)行仇哆。更重要的是沦辙,synchronized
還會創(chuàng)建一個內(nèi)存屏障,內(nèi)存屏障指令保證了所有CPU操作結(jié)果都會直接刷到主存中讹剔,從而保證了操作的內(nèi)存可見性油讯,同時也使得先獲得這個鎖的線程的所有操作,都happens-before于隨后獲得這個鎖的線程的操作延欠。
volatile
關(guān)鍵字解決的是內(nèi)存可見性的問題陌兑,會使得所有對volatile
變量的讀寫都會直接刷到主存,即保證了變量的可見性由捎。這樣就能滿足一些對變量可見性有要求而對讀取順序沒有要求的需求兔综。
區(qū)別
volatile本質(zhì)是在告訴jvm當(dāng)前變量在寄存器(工作內(nèi)存)中的值是不確定的,需要從主存中讀取软驰; synchronized則是鎖定當(dāng)前變量涧窒,只有當(dāng)前線程可以訪問該變量,其他線程被阻塞住锭亏。
volatile僅能使用在變量級別纠吴;synchronized則可以使用在變量、方法慧瘤、和類級別的
volatile僅能實現(xiàn)變量的修改可見性呜象,不能保證原子性;而synchronized則可以保證變量的修改可見性和原子性
volatile不會造成線程的阻塞碑隆;synchronized可能會造成線程的阻塞恭陡。
volatile標(biāo)記的變量不會被編譯器優(yōu)化;synchronized標(biāo)記的變量可以被編譯器優(yōu)化
19. synchronized 和 Lock 有什么區(qū)別上煤?
1)Lock是一個接口休玩,而synchronized是Java中的關(guān)鍵字,synchronized是內(nèi)置的語言實現(xiàn)劫狠;
2)synchronized在發(fā)生異常時拴疤,會自動釋放線程占有的鎖,因此不會導(dǎo)致死鎖現(xiàn)象發(fā)生独泞;而Lock在發(fā)生異常時呐矾,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現(xiàn)象懦砂,因此使用Lock時需要在finally塊中釋放鎖蜒犯;
3)Lock可以讓等待鎖的線程響應(yīng)中斷,而synchronized卻不行荞膘,使用synchronized時罚随,等待的線程會一直等待下去,不能夠響應(yīng)中斷羽资;
4)通過Lock可以知道有沒有成功獲取鎖淘菩,而synchronized卻無法辦到。
5)Lock可以提高多個線程進(jìn)行讀操作的效率屠升。
在性能上來說潮改,如果競爭資源不激烈,兩者的性能是差不多的腹暖,而當(dāng)競爭資源非常激烈時(即有大量線程同時競爭)汇在,此時Lock的性能要遠(yuǎn)遠(yuǎn)優(yōu)于synchronized。所以說微服,在具體使用時要根據(jù)適當(dāng)情況選擇趾疚。
55. synchronized 和 ReentrantLock 區(qū)別是什么?
ReenTrantLock可重入鎖(和synchronized的區(qū)別)總結(jié)
可重入性:
從名字上理解以蕴,ReenTrantLock的字面意思就是再進(jìn)入的鎖糙麦,其實synchronized關(guān)鍵字所使用的鎖也是可重入的,兩者關(guān)于這個的區(qū)別不大丛肮。兩者都是同一個線程每進(jìn)入一次赡磅,鎖的計數(shù)器都自增1,所以要等到鎖的計數(shù)器下降為0時才能釋放鎖宝与。
鎖的實現(xiàn):
Synchronized是依賴于JVM實現(xiàn)的焚廊,而ReenTrantLock是JDK實現(xiàn)的,有什么區(qū)別习劫,說白了就類似于操作系統(tǒng)來控制實現(xiàn)和用戶自己敲代碼實現(xiàn)的區(qū)別咆瘟。前者的實現(xiàn)是比較難見到的,后者有直接的源碼可供閱讀诽里。
性能的區(qū)別:
在Synchronized優(yōu)化以前袒餐,synchronized的性能是比ReenTrantLock差很多的,但是自從Synchronized引入了偏向鎖谤狡,輕量級鎖(自旋鎖)后灸眼,兩者的性能就差不多了,在兩種方法都可用的情況下墓懂,官方甚至建議使用synchronized焰宣,其實synchronized的優(yōu)化我感覺就借鑒了ReenTrantLock中的CAS技術(shù)。都是試圖在用戶態(tài)就把加鎖問題解決捕仔,避免進(jìn)入內(nèi)核態(tài)的線程阻塞匕积。
功能區(qū)別:
便利性:很明顯Synchronized的使用比較方便簡潔,并且由編譯器去保證鎖的加鎖和釋放榜跌,而ReenTrantLock需要手工聲明來加鎖和釋放鎖闸天,為了避免忘記手工釋放鎖造成死鎖,所以最好在finally中聲明釋放鎖斜做。
鎖的細(xì)粒度和靈活度:很明顯ReenTrantLock優(yōu)于Synchronized
ReenTrantLock獨有的能力:
ReenTrantLock可以指定是公平鎖還是非公平鎖苞氮。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖瓤逼。
ReenTrantLock提供了一個Condition(條件)類笼吟,用來實現(xiàn)分組喚醒需要喚醒的線程們,而不是像synchronized要么隨機(jī)喚醒一個線程要么喚醒全部線程霸旗。
ReenTrantLock提供了一種能夠中斷等待鎖的線程的機(jī)制贷帮,通過lock.lockInterruptibly()來實現(xiàn)這個機(jī)制。
ReenTrantLock實現(xiàn)的原理:
簡單來說诱告,ReenTrantLock的實現(xiàn)是一種自旋鎖撵枢,通過循環(huán)調(diào)用CAS操作來實現(xiàn)加鎖。它的性能比較好也是因為避免了使線程進(jìn)入內(nèi)核態(tài)的阻塞狀態(tài)。想盡辦法避免線程進(jìn)入內(nèi)核的阻塞狀態(tài)是我們?nèi)シ治龊屠斫怄i設(shè)計的關(guān)鍵鑰匙锄禽。
20. 說一下 atomic 的原理潜必?
在多線程的場景中,我們需要保證數(shù)據(jù)安全沃但,就會考慮同步的方案磁滚,通常會使用synchronized或者lock來處理,使用了synchronized意味著內(nèi)核態(tài)的一次切換宵晚。這是一個很重的操作垂攘。
有沒有一種方式,可以比較便利的實現(xiàn)一些簡單的數(shù)據(jù)同步淤刃,比如計數(shù)器等等晒他。concurrent包下的atomic提供我們這么一種輕量級的數(shù)據(jù)同步的選擇。
優(yōu)缺點
CAS相對于其他鎖逸贾,不會進(jìn)行內(nèi)核態(tài)操作仪芒,有著一些性能的提升。但同時引入自旋耕陷,當(dāng)鎖競爭較大的時候掂名,自旋次數(shù)會增多。cpu資源會消耗很高哟沫。
換句話說饺蔑,CAS+自旋適合使用在低并發(fā)有同步數(shù)據(jù)的應(yīng)用場景。