前言:
java
愈來愈多的人都想去字節跳動了,小編也不例外,此次面試字節跳動也是作了不少的準備,還好順利拿到了offer,特分享一下此次的面試題,可能有些記不全了,但多少也可以給一些正在面試字節或計劃面試字節的朋友提供幫助。面試
hashmap和hashTable的區別?爲什麼一個線程安全一個線程不安全?編程
區別:數組
Hashtable 是早期Java類庫提供的一個哈希表實現,自己是同步(synchronized)的,不支持 null 鍵和值,因爲同步致使的性能開銷,因此已經不多被推薦使用。安全
HashMap與 HashTable主要區別在於 HashMap 不是同步的,支持 null 鍵和值等。一般狀況下,HashMap 進行 put 或者 get 操做,能夠達到常數時間的性能,因此它是絕大部分利用鍵值對存取場景的首選。數據結構
底層實現:多線程
JDK1.8 以前 HashMap 底層是 數組和鏈表 結合在一塊兒使用也就是 鏈表散列。HashMap 經過 key 的 hashCode 通過擾動函數處理事後獲得 hash 值,而後經過 (n - 1) & hash 判斷當前元素存放的位置(這裏的 n 指的是數組的長度),若是當前位置存在元素的話,就判斷該元素與要存入的元素的 hash 值以及 key 是否相同,若是相同的話,直接覆蓋,不相同就經過拉鍊法解決衝突。JDK1.8 之後的 HashMap 在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默認爲8)時,將鏈表轉化爲紅黑樹,以減小搜索時間。Hashtable 沒有這樣的機制。併發
而HashTable底層與HashMap基本相似,主要區別在HashTable爲了實現同步,全部方法都加了synchronizedide
線程安全:函數
HashMap 是非線程安全的,HashTable 是線程安全的;由於HashTable 內部的方法基本都通過synchronized 修飾。但HashTable效率低於HashMap,雖然能保證多線程下同步,但也會大大下降程序的性能(若是你要保證線程安全的話就使用 ConcurrentHashMap 吧,下面會有說到!);
- java中的hashmap 和 concurrenHashmap的區別?
區別:
HashMap是非同步的,這也意味着,HashMap在進行插入、刪除等操做的時候,是線程不安全的,若是本身沒有在程序上對HashMap進行同步的處理,則不能讓多個線程共享一個變量。
concurrenHashmap
底層數據結構: JDK1.7的 ConcurrentHashMap 底層採用 分段的數組+鏈表 實現,JDK1.8 採用的數據結構跟HashMap1.8的結構同樣,數組+鏈表/紅黑二叉樹。Hashtable 和 JDK1.8 以前的 HashMap 的底層數據結構相似都是採用 數組+鏈表 的形式,數組是 HashMap 的主體,鏈表則是主要爲了解決哈希衝突而存在的;
實現線程安全的方式(重要): ① 在JDK1.7的時候,ConcurrentHashMap(分段鎖) 對整個桶數組進行了分割分段(Segment),每一把鎖只鎖容器其中一部分數據,多線程訪問容器裏不一樣數據段的數據,就不會存在鎖競爭,提升併發訪問率。 到了 JDK1.8 的時候已經摒棄了Segment的概念,而是直接用 Node 數組+鏈表+紅黑樹的數據結構來實現,併發控制使用 synchronized 和 CAS 來操做。(JDK1.6之後 對 synchronized鎖作了不少優化) 整個看起來就像是優化過且線程安全的 HashMap,雖然在JDK1.8中還能看到 Segment 的數據結構,可是已經簡化了屬性,只是爲了兼容舊版本;② Hashtable(同一把鎖) :使用 synchronized 來保證線程安全,效率很是低下。當一個線程訪問同步方法時,其餘線程也訪問同步方法,可能會進入阻塞或輪詢狀態,如使用 put 添加元素,另外一個線程不能使用 put 添加元素,也不能使用 get,競爭會愈來愈激烈效率越低。
- concurrenHashMap如何實現線程安全且併發性能比hashTable高?
鎖分段:
在多線程狀況下,既然不能全鎖(HashTable)又不能不鎖(HashMap),因此就搞個部分鎖,只鎖部分,用到哪部分就鎖哪部分。一個大倉庫,裏面有若干個隔間,每一個隔間都有鎖,同時只容許一我的進隔間存取東西。可是,在存取東西以前,須要有一個全局索引,告訴你要操做的資源在哪一個隔間裏,而後當你看到隔間空閒時,就能夠進去存取,若是隔間正在佔用,那你就得等着。
解釋下鎖分段?
首先將數據分爲一段一段的存儲,而後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據時,其餘段的數據也能被其餘線程訪問。
JDK1.7的ConcurrentHashMap 是由 Segment 數組結構和 HashEntry 數組結構組成。
Segment 實現了 ReentrantLock,因此 Segment 是一種可重入鎖,扮演鎖的角色。HashEntry 用於存儲鍵值對數據。
static class Segment<K,V> extends ReentrantLock implements Serializable {
}
一個 ConcurrentHashMap 裏包含一個 Segment 數組。Segment 的結構和HashMap相似,是一種數組和鏈表結構,一個 Segment 包含一個 HashEntry 數組,每一個 HashEntry 是一個鏈表結構的元素,每一個 Segment 守護着一個HashEntry數組裏的元素,當對 HashEntry 數組的數據進行修改時,必須首先得到對應的 Segment的鎖。
JDK1.8的ConcurrentHashMap取消了Segment分段鎖,採用CAS和synchronized來保證併發安全。數據結構跟HashMap1.8的結構相似,數組+鏈表/紅黑二叉樹。Java 8在鏈表長度超過必定閾值(8)時將鏈表(尋址時間複雜度爲O(N))轉換爲紅黑樹(尋址時間複雜度爲O(log(N)))
synchronized只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要hash不衝突,就不會產生併發,效率又提高N倍。
-聊一下volatile做用?
做用:
(1)保證可見性,不保證原子性
(2)禁止指令重排
談談原子性:
定義: 即一個操做或者多個操做 要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。
原子性是拒絕多線程操做的,不管是多核仍是單核,具備原子性的量,同一時刻只能有一個線程來對它進行操做。簡而言之,在整個操做過程當中不會被線程調度器中斷的操做,均可認爲是原子性。例如 a=1是原子性操做,可是a++和a +=1就不是原子性操做。Java中的原子性操做包括:
a. 基本類型的讀取和賦值操做,且賦值必須是數字賦值給變量,變量之間的相互賦值不是原子性操做。
b.全部引用reference的賦值操做
c.java.concurrent.Atomic.* 包中全部類的一切操做
談談可見性:
定義:指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。
在多線程環境下,一個線程對共享變量的操做對其餘線程是不可見的。Java提供了volatile來保證可見性,當一個變量被volatile修飾後,表示着線程本地內存無效,當一個線程修改共享變量後他會當即被更新到主內存中,其餘線程讀取共享變量時,會直接從主內存中讀取。固然,synchronize和Lock均可以保證可見性。synchronized和Lock能保證同一時刻只有一個線程獲取鎖而後執行同步代碼,而且在釋放鎖以前會將對變量的修改刷新到主存當中。所以能夠保證可見性。
談談有序性(指令重排):
定義:即程序執行的順序按照代碼的前後順序執行。
Java內存模型中的有序性能夠總結爲:若是在本線程內觀察,全部操做都是有序的;若是在一個線程中觀察另外一個線程,全部操做都是無序的。前半句是指「線程內表現爲串行語義」,後半句是指「指令重排序」現象和「工做內存主主內存同步延遲」現象。
在Java內存模型中,爲了效率是容許編譯器和處理器對指令進行重排序,固然重排序不會影響單線程的運行結果,可是對多線程會有影響。Java提供volatile來保證必定的有序性。最著名的例子就是單例模式裏面的DCL(雙重檢查鎖)。另外,能夠經過synchronized和Lock來保證有序性,synchronized和Lock保證每一個時刻是有一個線程執行同步代碼,至關因而讓線程順序執行同步代碼,天然就保證了有序性。
volatile使用場景
單例模式的雙重鎖
共享變量讀多寫少的狀況
聊一下synchronize關鍵字實現原理?
使用方式:
同步普通方法,鎖的是當前對象。
同步靜態方法,鎖的是當前 Class 對象。
同步塊,鎖的是 {} 中的對象。
實現原理:
JVM 是經過進入、退出對象監視器( Monitor )來實現對方法、同步塊的同步的。具體實現是在編譯以後在同步方法調用前加入一個 monitor.enter 指令,在退出方法和異常處插入 monitor.exit 的指令。其本質就是對一個對象監視器( Monitor )進行獲取,而這個獲取過程具備排他性從而達到了同一時刻只能一個線程訪問的目的。而對於沒有獲取到鎖的線程將會阻塞到方法入口處,直到獲取鎖的線程 monitor.exit 以後才能嘗試繼續獲取鎖。
synchronize的鎖升級?
- 無鎖 -> 輕量鎖 -> 解鎖 -> 偏向鎖 -> 釋放鎖
談談輕量鎖:
當代碼進入同步塊時,若是同步對象爲無鎖狀態時,當前線程會在棧幀中建立一個鎖記錄(Lock Record)區域,同時將鎖對象的對象頭中 Mark Word 拷貝到鎖記錄中,再嘗試使用 CAS 將 Mark Word 更新爲指向鎖記錄的指針。
若是更新成功,當前線程就得到了鎖。
若是更新失敗 JVM 會先檢查鎖對象的 Mark Word 是否指向當前線程的鎖記錄。
若是是則說明當前線程擁有鎖對象的鎖,能夠直接進入同步塊。
不是則說明有其餘線程搶佔了鎖,若是存在多個線程同時競爭一把鎖,輕量鎖就會膨脹爲重量鎖。
解鎖:
輕量鎖的解鎖過程也是利用 CAS 來實現的,會嘗試鎖記錄替換回鎖對象的 Mark Word 。若是替換成功則說明整個同步操做完成,失敗則說明有其餘線程嘗試獲取鎖,這時就會喚醒被掛起的線程(此時已經膨脹爲重量鎖)
輕量鎖能提高性能的緣由是:認爲大多數鎖在整個同步週期都不存在競爭,因此使用 CAS 比使用互斥開銷更少。但若是鎖競爭激烈,輕量鎖就不但有互斥的開銷,還有 CAS 的開銷,甚至比重量鎖更慢。
偏向鎖:
爲了進一步的下降獲取鎖的代價,JDK1.6 以後還引入了偏向鎖。
偏向鎖的特徵是:鎖不存在多線程競爭,而且應由一個線程屢次得到鎖。
當線程訪問同步塊時,會使用 CAS 將線程 ID 更新到鎖對象的 Mark Word 中,若是更新成功則得到偏向鎖,而且以後每次進入這個對象鎖相關的同步塊時都不須要再次獲取鎖了。
釋放鎖:
當有另一個線程獲取這個鎖時,持有偏向鎖的線程就會釋放鎖,釋放時會等待全局安全點(這一時刻沒有字節碼運行),接着會暫停擁有偏向鎖的線程,根據鎖對象目前是否被鎖來斷定將對象頭中的 Mark Word 設置爲無鎖或者是輕量鎖狀態。
輕量鎖能夠提升帶有同步卻沒有競爭的程序性能,但若是程序中大多數鎖都存在競爭時,那偏向鎖就起不到太大做用。可使用 -XX:-userBiasedLocking=false 來關閉偏向鎖,並默認進入輕量鎖。
有用過java中的隊列嗎?
隊列是一種特殊的線性表,遵循的原則就是「先入先出」。在咱們平常使用中,常常會用來併發操做數據。在併發編程中,有時候須要使用線程安全的隊列。若是要實現一個線程安全的隊列一般有兩種方式:一種是使用阻塞隊列,另外一種是使用線程同步鎖。
阻塞隊列有哪些:
ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)對元素進行排序。
LinkedBlokcingQueue:是一個基於鏈表結構的阻塞隊列,此隊列按 FIFO(先進先出)對元素進行排序,吞吐量一般要高於 ArrayBlockingQueue。
SynchronousQueue:是一個不存儲元素的阻塞隊列,每一個插入操做必須等到另外一個線程調用移除操做,不然插入操做一直處於阻塞狀態,吞吐量一般要高於 LinkedBlokcingQueue。
什麼是阻塞隊列:
阻塞隊列,顧名思義,首先它是一個隊列,而一個阻塞隊列在數據結構中所起的做用大體如圖所示:
當阻塞隊列是空時,從隊列中獲取元素的操做將會被阻塞。
當阻塞隊列是滿時,往隊列裏添加元素的操做將會被阻塞。
後序更新……
今天就給你們分享了這麼多,後續有機會在給你們分享更多的經驗,今天給你們分享一份千道的面試題資料分享給你們,有答案,帶詳細的解析。
領取方式:關注個人供種號(Java周某人)便可領取