2020字節跳動最新春招面經分享(附增最新整理面試文檔PDF)

 

前言:

越來越多的人都想去字節跳動了,小編也不例外,這次面試字節跳動也是做了很多的準備,還好順利拿到了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爲了實現同步,所有方法都加了synchronized

線程安全:

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周某人)即可領取

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章