這篇文章是研一剛入學時寫的,今天整理草稿時才被我挖出來。當時混混沌沌的面試,記下來了一些併發的面試問題,很多還沒有回答。到現在也學習了不少併發的知識,回過頭來看這些問題和當時整理的答案,漏洞百出又十分可笑。發表出來權當對自己的一個提醒——如果不能一直進步,你就看不到當初傻逼的自己。
曾經,我在面試Java研發實習生時最常聽到的一句話就是:
搞Java怎麼能不學併發呢?
沒錯,真的是經過了面試官的無數鄙視,我才知道Java併發程式設計在Java語言中的重要性。
併發模型
悲觀鎖和樂觀鎖的理解及如何實現,有哪些實現方式?
悲觀鎖
悲觀鎖假設最壞的情況(如果你不鎖門,那麼搗蛋鬼就會闖入並搞得一團糟),並且只有在確保其他執行緒不會干擾(通過獲取正確的鎖)的情況下才能執行下去。
常見實現如獨佔鎖等。
安全性更高,但在中低併發程度下的效率更低。
樂觀鎖
樂觀鎖藉助衝突檢查機制來判斷在更新過程中是否存在其他執行緒的干擾,如果存在,這個操作將失敗,並且可以重試(也可以不重試)。
常見實現如CAS等。
部分樂觀鎖削弱了一致性,但中低併發程度下的效率大大提高。
併發程式設計
Java中如何建立一個執行緒
從面相介面的角度上講,實際上只有一種方法實現Runable介面;但Thread類為執行緒操作提供了更多的支援,所以通常做法是實現Runable介面,例項化並傳入Thread類的建構函式。
- 繼承Thread,覆寫run方法
- 實現Runable介面,覆寫run方法
Vector(HashTable)如何實現執行緒安全
通過synchronized關鍵字修飾每個方法。
依據synchronized關鍵字引申出以下問題。
synchronized修飾方法和修飾程式碼塊時有何不同
持有鎖的物件不同:
- 修飾方法時:this引用的當前例項持有鎖
- 修飾程式碼塊時:要指定一個物件,該物件持有鎖
從而導致二者的意義不同:
- 同步程式碼塊在鎖定的範圍上可能比同步方法要小,一般來說鎖的範圍大小和效能是成反比的。
- 修飾程式碼塊可以選擇對哪個物件加鎖,但是修飾方法只能給this物件加鎖。
ConcurrentHashMap的如何實現執行緒安全
ConcurrentHashMap的執行緒安全實現與HashTable不同:
- 可以將ConcurrentHashMap理解為,不直接持有一個HashMao,而是用多個Segment代替了一個HashMap。但實際實現的Map部分和HashMap的原理基本相同,對腳標取模來確定table[i]所屬段,從而對不同的段獲取不同的段鎖。
- 每個Segment持有一個鎖,通過分段加鎖的方式,既實現了執行緒安全,又兼顧了效能
Java中有哪些實現併發程式設計的方法
要從最簡單的答起,業界最常用的是重點,有新意就放在最後。
- synchronized關鍵字
- 使用繼承自Object類的wait、notify、notifyAll方法
- 使用執行緒安全的API和集合類:
- 使用Vector、HashTable等執行緒安全的集合類
- 使用Concurrent包中提供的ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue等弱一致性的集合類
- 在Collections類中有多個靜態方法,它們可以獲取通過同步方法封裝非同步集合而得到的集合,如
List list = Collection.synchronizedList(new ArrayList())
。 - 使用原子變數、volatile變數等
- 使用Concurrent包中提供的訊號量Semaphore、閉鎖Latch、柵欄Barrier、交換器Exchanger、Callable&Future、阻塞佇列BlockingQueue等.
- 手動使用Lock實現基於鎖的併發控制
- 手動使用Condition或AQS實現基於條件佇列的併發控制
- 使用CAS和SPIN等實現非阻塞的併發控制
- 使用不變類
- 其他併發模型還沒有涉及
從而引申出如下問題:
ConcurrentHashMap的的實現原理(見前)
CopyOnWriteArrayList的複製操作發生在什麼時機
synchronizedList&Vector的區別
- synchronizedList的實現中,synchronized關鍵字修飾程式碼塊;Vector的實現中修飾方法。
- synchronizedList只封裝了add、get、remove等程式碼塊,但Iterator卻不是同步的,進行遍歷時要手動進行同步處理;Vector中對Iterator也進行了加鎖。
- synchronizedList能夠將所有List實現類封裝為同步集合,其內部持有的仍然是List的實現類(ArrayList/LinkedList),所以除同步外,幾乎只有該實現類和Vector的區別。
synchronized修飾方法和修飾程式碼塊時有何不同(見前)
訊號量Semaphore、閉鎖Latch、柵欄Barrier、交換器
Exchanger、Callable&Future、阻塞佇列BlockingQueue的實現原理
ConcurrentLinkedQueue的插入演算法
演算法核心可概括為兩步:
- 先檢測是否是中間狀態(SPIN)
- 再嘗試CAS插入
詳細待補充。
參考:
在java中wait和sleep方法的不同?
最大的不同是在等待時wait會釋放鎖,而sleep一直持有鎖。Wait通常被用於執行緒間互動,sleep通常被用於暫停執行。
為什麼wait, notify 和 notifyAll這些方法不在thread類裡面?
主要原因是JAVA提供的鎖是物件級的而不是執行緒級的,每個物件都有鎖,通過執行緒獲得。由於wait,notify和notifyAll都是鎖級別的操作,所以把他們定義在Object類中因為鎖屬於物件。
為什麼wait和notify方法要在同步塊中呼叫?
Java API強制要求這樣做,如果你不這麼做,你的程式碼會丟擲IllegalMonitorStateException異常。還有一個原因是為了避免wait和notify之間產生競態條件。
為什麼你應該在迴圈中檢查等待條件?
處於等待狀態的執行緒可能會收到錯誤警報和偽喚醒,如果不在迴圈中檢查等待條件,程式就會在沒有滿足結束條件的情況下退出。
Java執行緒池中submit() 和 execute()方法有什麼區別?
兩個方法都可以向執行緒池提交任務,execute()方法的返回型別是void,它定義在Executor介面中, 而submit()方法可以返回持有計算結果的Future物件,它定義在ExecutorService介面中,它擴充套件了Executor介面,其它執行緒池類像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有這些方法。
volatile 變數和 atomic 變數有什麼不同?
Volatile變數可以確保先行關係,即寫操作會發生在後續的讀操作之前, 但它並不能保證原子性。例如用volatile修飾count變數那麼 count++ 操作就不是原子性的。而AtomicInteger類提供的atomic方法可以讓這種操作具有原子性如getAndIncrement()方法會原子性的進行增量操作把當前值加一,其它資料型別和引用變數也可以進行相似操作。
為什麼Thread類的sleep()和yield ()方法是靜態的?
Thread類的sleep()和yield()方法將在當前正在執行的執行緒上執行。所以在其他處於等待狀態的執行緒上呼叫這些方法是沒有意義的。這就是為什麼這些方法是靜態的。它們可以在當前正在執行的執行緒中工作,並避免程式設計師錯誤的認為可以在其他非執行執行緒呼叫這些方法。
本文連結:Java高併發綜合
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。