過關(guān)斬將之路-線程基礎(chǔ)&Volatile(IT楓斗者)
線程基礎(chǔ)
1問:線程的狀態(tài)是怎樣的?
1答:

2問:多線程一定比單線程快嗎?
2答:不一定,因?yàn)榫€程切換是有開銷的,需要消耗性能。若CPU是單核,那你開多個線程可能會使程序變慢,而單線程則會很快。
3問:多線程的優(yōu)缺點(diǎn)有哪些?
3答:
優(yōu)點(diǎn)
資源利用率更好
比如:下載文件。我們的流程是這樣的:
將下載任務(wù)放到隊列。
從隊列里取出下載鏈接去下載。
若是單線程的話,那費(fèi)老勁了,一個一個的下載,CPU大部分時間是空閑的,若是多線程呢?同時下載一批任務(wù),豈不是更爽快?讓CPU忙起來吧!
提高系統(tǒng)的吞吐率
多線程編程使得一個進(jìn)程中可以有多個并發(fā)(即同時進(jìn)行)的操作。例如,當(dāng)一個線程因?yàn)镮/O操作而處于等待時,其他線程仍然可以執(zhí)行其操作。
響應(yīng)速度快
還以下載文件的案例,若我們請求一個下載接口,要等下載完才返回成功,那豈不是需要等太久了,如果我們業(yè)務(wù)邏輯都沒問題直接返回成功豈不是更好?然后下載任務(wù)交由其他線程去處理。
缺點(diǎn)
線程切換是有開銷的,這會在一定情況下導(dǎo)致程序運(yùn)行變慢。
多線程程序必須非常小心地同步代碼,否則會引起死鎖或數(shù)據(jù)不準(zhǔn)確。
多線程程序極難調(diào)試,并且一些bug非常隱蔽,可能你99次都是對的,但是有1次是錯的,不像單線程程序那么容易暴露問題。
4問:線程和進(jìn)程的區(qū)別?
4答:
進(jìn)程是資源分配的基本單位。
線程是處理器(CPU)調(diào)度的基本單位。
進(jìn)程是操作系統(tǒng)級別的,線程是進(jìn)程級別的。
一個進(jìn)程包含多個線程。(我們可以打開一個IDEA/Eclipse,然后JConsole去看線程數(shù),會發(fā)現(xiàn)一個IDEA/Eclipse進(jìn)程啟動了N多個線程。)
5問:用Runnable還是Thread?
5答:
這個問題很容易回答,如果你知道Java不支持類的多重繼承,但允許你實(shí)現(xiàn)多個接口。所以如果你要繼承其他類,當(dāng)然是調(diào)用Runnable接口好了。也是大家一直所說的:面向接口編程。
6答:Thread 類中的start()和 run()方法有什么區(qū)別?
6答:真正啟動線程的是start()方法而不是run(),run()和普通的成員方法一樣,可以重復(fù)使用,但不能啟動一個新線程。start()方法才會啟動新線程。
7問:sleep()和wait()的區(qū)別?
7答:
sleep為Thread的方法,而wait為Object的方法。
最大本質(zhì)的區(qū)別是:sleep不釋放鎖,wait釋放鎖。
用法上的不同:sleep(milliseconds)可以用時間指定來使他自動醒過來,如果時間不到你只能調(diào)用interreput()來終止線程;wait()可以用notify()/notifyAll()直接喚起。
wait在使用前必須要獲取鎖(synchronized塊包起來),而sleep可以在任何地方使用。
8問:阻塞與等待的區(qū)別?
8答:
阻塞:當(dāng)一個線程試圖獲取對象鎖(非java.util.concurrent庫中的鎖,即synchronized),而該鎖被其他線程持有,則該線程進(jìn)入阻塞狀態(tài)。它的特點(diǎn)是使用簡單,由JVM調(diào)度器來決定喚醒自己,而不需要由另一個線程來喚醒自己,不響應(yīng)中斷。
等待:當(dāng)一個線程等待另一個線程通知調(diào)度器一個條件時,該線程進(jìn)入等待狀態(tài)。它的特點(diǎn)是需要等待另一個線程顯式地喚醒自己,實(shí)現(xiàn)靈活,語義更豐富,可響應(yīng)中斷。例如調(diào)用:Object.wait()、Thread.join()以及等待Lock或Condition。
9問:創(chuàng)建線程的方式有哪些?
9答:
繼承Thread類
實(shí)現(xiàn)Runnable接口
匿名內(nèi)部類(直接Thread/實(shí)現(xiàn)Runnable接口 )
帶返回值的方式(FutureTask)
線程池的方式
10問:為什么wait和notify方法要在同步塊中調(diào)用?
10答:因?yàn)橹挥凶哌M(jìn)同步塊,才說明該線程有對資源的持有權(quán),而只要對資源有持有權(quán),才有資格去進(jìn)行釋放鎖和通知其他沒有獲取到鎖的線程。否則就無法保證代碼的原子性。
11問:什么是同步鎖和死鎖?
11答:
同步鎖
當(dāng)多個線程同時訪問同一個數(shù)據(jù)時,很容易出現(xiàn)問題。為了避免這種情況出現(xiàn),我們要保證線程同步互斥,就是指并發(fā)執(zhí)行的多個線程,在同一時間內(nèi)只允許一個線程訪問共享數(shù)據(jù)。?Java?中可以使用?synchronized?關(guān)鍵字來取得一個對象的同步鎖。
死鎖
何為死鎖,就是多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。
Volatile
12問:簡單聊聊volatile?
12答:
被volatile修飾的共享變量,就具有了以下兩點(diǎn)特性:
保證了多線程對該變量操作的內(nèi)存可見性
禁止指令重排序
但是并不保證原子性。
13問:什么是可見性?
13答:每個線程都有獨(dú)立的工作內(nèi)存,所以線程對變量進(jìn)行修改的時候會先將值修改到工作內(nèi)存,其他線程是不可見的,可見性是指當(dāng)一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。加上volatile可以保證可見性,volatile變量保證修改的新值能夠立馬同步到主存,其他線程使用時也會感知到這個共享變量已經(jīng)有新值了,讓自己的工作內(nèi)存值失效,立即從主存重新獲取 ,這就保證了多線程操作時變量的可見性。
14問:指令重排是什么意思?
14答:
JVM會對編譯后的class進(jìn)行優(yōu)化,重排序也是JVM優(yōu)化的手段之一,也就是A、B兩行代碼的順序編譯后可能變成B、A,這就是重新排序了。有時候可能會造成一些問題,比如狀態(tài)標(biāo)記量、雙重檢查鎖的單例寫法。
比如雙重檢查鎖的單例(這個解決方案需要JDK5或更高版本):

若不加volatile的話這就是一個線程不安全的單例寫法。在線程執(zhí)行到第1處,代碼讀取到instance不為null時,instance引用的對象有可能發(fā)生了指令重排給分配了內(nèi)存空間,但是還沒有完成初始化!所以業(yè)務(wù)系統(tǒng)獲取到的instance實(shí)例可能是null。
問題的根源如下:
前面的雙重檢查鎖定實(shí)例代碼的第4處 instance = new Instance(); 創(chuàng)建了一個對象。這一行代碼可以分解為如下的3行偽代碼。

上面3行偽代碼中的2和3之間可能會被重排序,2和3之間重排序之后的執(zhí)行時序如下:

15問:volatile怎么保證原子性的?
15答:這是個坑,因?yàn)関olatile無法保證原子性。

16問:volatile和synchronized有啥區(qū)別?
16答:
volatile僅僅作用在變量上,synchronized可以作用在變量上也可以方法上,還支持代碼塊。
volatile能保證可見性和防止重排序,不能保證原子性。synchronized能保證原子性和可見性和防止指令重排序。
volatile不會造成線程阻塞,synchronized會造成線程阻塞。
其實(shí)全是圍繞這句話來說的:volatile不是鎖,synchronized是把可重入排他鎖。
17問:volatile用在什么地方?
17答:
線程安全的單例:雙重鎖檢查無法保證線程安全性,需要volatile防止重排序來保證。
狀態(tài)標(biāo)記量:一個狀共享態(tài)變量需要多個線程讀寫的時候,可以用volatile保證可見性。
18問:volatile是如何保證可見性和指令重排的(它的實(shí)現(xiàn)原理)?
18答:
加入volatile關(guān)鍵字時,會多出一個lock前綴指令,lock前綴指令實(shí)際上相當(dāng)于一個內(nèi)存屏障,這個內(nèi)存屏障包含如下三個功能:
它確保指令重排序時不會把其后面的指令排到內(nèi)存屏障之前的位置,也不會把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時,在它前面的操作已經(jīng)全部完成。
它會強(qiáng)制將對緩存的修改操作立即寫入主存。
如果是寫操作,它會導(dǎo)致其他CPU中對應(yīng)的緩存行失效。