1. 程式人生 > 實用技巧 >美團上海後端開發一面面經

美團上海後端開發一面面經

主要也就問到了這些問題,當時緊張,回答得不是很好,有很多東西沒有講清楚,現在回過頭總結一下,也強化一下記憶。

HashMap HashTable ConcurrentHashMap的區別? put如何解決hash衝突

HashTable

底層陣列+連結串列實現,無論key還是value都不能為null,執行緒安全,實現執行緒安全的方式是在修改資料時鎖住整個HashTable,效率低,ConcurrentHashMap做了相關優化
初始size為11,擴容:newsize = olesize*2+1
計算index的方法:index = (hash & 0x7FFFFFFF) % tab.length

HashMap

底層陣列+連結串列實現,可以儲存null鍵和null值,執行緒不安全
初始size為16,擴容:newsize = oldsize*2,size一定為2的n次冪
擴容針對整個Map,每次擴容時,原來陣列中的元素依次重新計算存放位置,並重新插入
插入元素後才判斷該不該擴容,有可能無效擴容(插入後如果擴容,如果沒有再次插入,就會產生無效擴容)
當Map中元素總數超過Entry陣列的75%,觸發擴容操作,為了減少連結串列長度,元素分配更均勻
計算index方法:index = hash & (tab.length 1)

HashMap的初始值還要考慮載入因子:
雜湊衝突:若干Key的雜湊值按陣列大小取模後,如果落在同一個陣列下標上,將組成一條Entry鏈,對Key的查詢需要遍歷Entry鏈上的每個元素執行equals()比較。
載入因子:為了降低雜湊衝突的概率,預設當HashMap中的鍵值對達到陣列大小的75%時,即會觸發擴容。因此,如果預估容量是100,即需要設定100/0.75=134的陣列大小。
空間換時間:如果希望加快Key查詢的時間,還可以進一步降低載入因子,加大初始大小,以降低雜湊衝突的概率。
HashMap和Hashtable都是用hash演算法來決定其元素的儲存,因此HashMap和Hashtable的hash表包含如下屬性:
容量(capacity):hash表中桶的數量
初始化容量(initial capacity):建立hash表時桶的數量,HashMap允許在構造器中指定初始化容量
尺寸(size):當前hash表中記錄的數量
負載因子(load factor):負載因子等於“size/capacity”。負載因子為0,表示空的hash表,0.5表示半滿的散列表,依此類推。輕負載的散列表具有衝突少、適宜插入與查詢的特點(但是使用Iterator迭代元素時比較慢)
除此之外,hash表裡還有一個“負載極限”,“負載極限”是一個0~1的數值,“負載極限”決定了hash表的最大填滿程度。當hash表中的負載因子達到指定的“負載極限”時,hash表會自動成倍地增加容量(桶的數量),並將原有的物件重新分配,放入新的桶內,這稱為rehashing。
HashMap和Hashtable的構造器允許指定一個負載極限,HashMap和Hashtable預設的“負載極限”為0.75,這表明當該hash表的3/4已經被填滿時,hash表會發生rehashing。
“負載極限”的預設值(0.75)是時間和空間成本上的一種折中:
較高的“負載極限”可以降低hash表所佔用的記憶體空間,但會增加查詢資料的時間開銷,而查詢是最頻繁的操作(HashMap的get()與put()方法都要用到查詢)
較低的“負載極限”會提高查詢資料的效能,但會增加hash表所佔用的記憶體開銷
程式猿可以根據實際情況來調整“負載極限”值


ConcurrentHashMap

底層採用分段的陣列+連結串列實現,執行緒安全
通過把整個Map分為N個Segment,可以提供相同的執行緒安全,但是效率提升N倍,預設提升16倍。(讀操作不加鎖,由於HashEntry的value變數是 volatile的,也能保證讀取到最新的值。)
Hashtable的synchronized是針對整張Hash表的,即每次鎖住整張表讓執行緒獨佔,ConcurrentHashMap允許多個修改操作併發進行,其關鍵在於使用了鎖分離技術
有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖
擴容:段內擴容(段內元素超過該段對應Entry陣列長度的75%觸發擴容,不會對整個Map進行擴容),插入前檢測需不需要擴容,有效避免無效擴容

HashMap基於雜湊思想,實現對資料的讀寫。當我們將鍵值對傳遞給put()方法時,它呼叫鍵物件的hashCode()方法來計算hashcode,然後找到bucket位置來儲存值物件。當獲取物件時,通過鍵物件的equals()方法找到正確的鍵值對,然後返回值物件。HashMap使用連結串列來解決碰撞問題,當發生碰撞時,物件將會儲存在連結串列的下一個節點中。HashMap在每個連結串列節點中儲存鍵值對物件。當兩個不同的鍵物件的hashcode相同時,它們會儲存在同一個bucket位置的連結串列中,可通過鍵物件的equals()方法來找到鍵值對。如果連結串列大小超過閾值(TREEIFY_THRESHOLD,8),連結串列就會被改造為紅黑樹結構。
在HashMap中,null可以作為鍵,這樣的鍵只有一個,但可以有一個或多個鍵所對應的值為null。當get()方法返回null值時,即可以表示HashMap中沒有該key,也可以表示該key所對應的value為null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個key,應該用containsKey()方法來判斷。而在Hashtable中,無論是key還是value都不能為null。
Hashtable與HashMap另一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以當有其它執行緒改變了HashMap的結構(增加或者移除元素),將會丟擲ConcurrentModificationException,但迭代器本身的remove()方法移除元素則不會丟擲ConcurrentModificationException異常。但這並不是一個一定發生的行為,要看JVM。
ConcurrentHashMap比HashMap多出了一個類Segment,而Segment是一個可重入鎖。
ConcurrentHashMap是使用了鎖分段技術來保證執行緒安全的。
鎖分段技術:首先將資料分成一段一段的儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問。

ConcurrentHashMap提供了與Hashtable和SynchronizedMap不同的鎖機制。Hashtable中採用的鎖機制是一次鎖住整個hash表,從而在同一時刻只能由一個執行緒對其進行操作;而ConcurrentHashMap中則是一次鎖住一個桶。

ConcurrentHashMap預設將hash表分為16個桶,諸如get、put、remove等常用操作只鎖住當前需要用到的桶。這樣,原來只能一個執行緒進入,現在卻能同時有16個寫執行緒執行,併發效能的提升是顯而易見的

a == b a.equals(b) 的區別

String a = "abc" ; String b = new String("abc"); a == b  a.equals(b) 的區別

首先,a==b的結果是false, a.equals(b) 的結果是true.
雙等號比較的是兩個物件是不是同一個物件,比較的是這兩個物件的引用,如果這兩個物件的引用地址相同,則表明是同一個物件,返回 true ,否則返回false, equals比較的是兩個物件的hashCode,如果兩個物件是同一個的,那麼他們的hashCode一定相同,但如果兩個物件的hashCode相同,他們未必是同一個物件,例如我們上面的a 和b
一般我們比較物件equals之前,需要先重寫它的hashCode方法,否則預設返回的是他的地址引用。
程式碼驗證:

public class demo1 {
public static void main(String[] args ){
String a = "abc";
String b = new String("abc");
System.out.println(a==b);
System.out.println(a.equals(b));
}
}
輸出結果:
false
true

notify 與 notifyAll 的區別

先說兩個概念:鎖池和等待池

鎖池:假設執行緒A已經擁有了某個物件(注意:不是類)的鎖,而其它的執行緒想要呼叫這個物件的某個synchronized方法(或者synchronized塊),由於這些執行緒在進入物件的synchronized方法之前必須先獲得該物件的鎖的擁有權,但是該物件的鎖目前正被執行緒A擁有,所以這些執行緒就進入了該物件的鎖池中。
等待池:假設一個執行緒A呼叫了某個物件的wait()方法,執行緒A就會釋放該物件的鎖後,進入到了該物件的等待池中
————————————————
如果執行緒呼叫了物件的 wait()方法,那麼執行緒便會處於該物件的等待池中,等待池中的執行緒不會去競爭該物件的鎖。
當有執行緒呼叫了物件的 notifyAll()方法(喚醒所有 wait 執行緒)或 notify()方法(只隨機喚醒一個 wait 執行緒),被喚醒的的執行緒便會進入該物件的鎖池中,鎖池中的執行緒會去競爭該物件鎖。也就是說,呼叫了notify後只要一個執行緒會由等待池進入鎖池,而notifyAll會將該物件等待池內的所有執行緒移動到鎖池中,等待鎖競爭
優先順序高的執行緒競爭到物件鎖的概率大,假若某執行緒沒有競爭到該物件鎖,它還會留在鎖池中,唯有執行緒再次呼叫 wait()方法,它才會重新回到等待池中。而競爭到物件鎖的執行緒則繼續往下執行,直到執行完了 synchronized 程式碼塊,它會釋放掉該物件鎖,這時鎖池中的執行緒會繼續競爭該物件鎖。
————————————————
綜上,所謂喚醒執行緒,另一種解釋可以說是將執行緒由等待池移動到鎖池,notifyAll呼叫後,會將全部執行緒由等待池移到鎖池,然後參與鎖的競爭,競爭成功則繼續執行,如果不成功則留在鎖池等待鎖被釋放後再次參與競爭。而notify只會喚醒一個執行緒。

有了這些理論基礎,後面的notify可能會導致死鎖,而notifyAll則不會的例子也就好解釋了

wait和sleep的區別

1、sleep是執行緒中的方法,但是wait是Object中的方法。

2、sleep方法不會釋放lock,但是wait會釋放,而且會加入到等待佇列中。

3、sleep方法不依賴於同步器synchronized,但是wait需要依賴synchronized關鍵字。

4、sleep不需要被喚醒(休眠之後推出阻塞),但是wait需要(不指定時間需要被別人中斷)。

列舉幾個java中執行緒安全的類

執行緒安全(Thread-safe)的集合物件

Vector
HashTable
StringBuffer
非執行緒安全的集合物件
ArrayList
LinkedList
HashMap
HashSet
TreeMap
TreeSet
StringBulider

尋找第K大


有一個整數陣列,請你根據快速排序的思路,找出陣列中第K大的數
給定一個整數陣列a,同時給定它的大小n和要找的K,(K在1到n之間)請返回第K大的數,保證答案存在
測試樣例:
[1,3,5,2,2] 5 3
返回:
2
public static int getK(int[] nums,int k ){
if(k>nums.length){
return -1;
}
int temp=0;
for(int i=nums.length-2;i>0;i--){
for(int j=0;j<i;j++){
if(nums[j]<nums[j+1] ){
// 交換
temp = nums[j+1];
nums[j+1] = nums[j];
nums[j] = temp;
}
}
}
return nums[k-1];
}

描述下列程式碼再記憶體中的執行過程(堆區棧區方法區)。

public class Solution{
public static void main(Strig[] args){
A a = new A();
A b = new A();
swap(a,b);
}
public static void swap(A x,A y){
A z = new A();
z = x;
x = y;
y = z;
}
}