面試發現經常有些重複的面試問題,自己也應該學會記錄下來,最好自己能做成筆記,在下一次面的時候說得有條不紊,深入具體,面試官想必也很開心。以下是我個人總結,請參考:
HashSet底層原理:(問了大機率跟HashMap一起面)
HashMap底層原理:(非常大機率問到)
Hashtable底層原理:(問的少,問了大機率問你跟HashMap的區別)
synchronized底層如何實現?鎖優化,怎麼優化?
ReentrantLock 底層實現;
ConcurrentHashMap 的工作原理,底層原理(談到多線程高併發大機率會問它)
JVM調優(JVM層層漸進問時大機率問)
JVM內存管理,JVM的常見的垃圾收集器,GC調優,Minor GC ,Full GC 觸發條件(像是必考題)
java內存模型
線程池的工作原理(談到多線程高併發大機率會問它)
ThreadLocal的底層原理(有時問)
voliate底層原理
NIO底層原理
IOC底層實現原理(Spring IOC ,AOP會問的兩個原理,面試官經常會問看過源碼嗎?所以你有所準備吧)
AOP底層實現原理
MyisAM和innodb的有關索引的疑問(容易混淆,可以問的會深入)
HashSet底層原理:(面試過)
http://zhangshixi.iteye.com/blog/673143
https://blog.csdn.net/HD243608836/article/details/80214413
HashSet實現Set接口,由哈希表(實際上是一個HashMap實例)支持。它不保證set 的迭代順序;特別是它不保證該順序恆久不變。此類允許使用null元素。
2. HashSet的實現:
對於HashSet而言,它是基於HashMap實現的,HashSet底層使用HashMap來保存所有元素,因此HashSet 的實現比較簡單,相關HashSet的操作,基本上都是直接調用底層HashMap的相關方法來完成, (實際底層會初始化一個空的HashMap,並使用默認初始容量爲16和加載因子0.75。)
HashSet的源代碼
對於HashSet中保存的對象,請注意正確重寫其equals和hashCode方法,以保證放入的對象的唯一性。
插入
當有新值加入時,底層的HashMap會判斷Key值是否存在(HashMap細節請移步深入理解HashMap),如果不存在,則插入新值,同時這個插入的細節會依照HashMap插入細節;如果存在就不插入
HashMap底層原理:
1. HashMap概述:
HashMap是基於哈希表的Map接口的非同步實現。此實現提供所有可選的映射操作,並允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。
2. HashMap的數據結構:
HashMap實際上是一個“數組+鏈表+紅黑樹”的數據結構
3. HashMap的存取實現:
(1.8之前的)
當我們往HashMap中put元素的時候,先根據key的hashCode重新計算hash值,根據hash值得到這個元素在數組中的位置(即下標),如果數組該位置上已經存放有其他元素了,那麼在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。
1.8:
put():
根據key計算得到key.hash = (h = k.hashCode()) ^ (h >>> 16);
根據key.hash計算得到桶數組的索引index = key.hash & (table.length - 1),這樣就找到該key的存放位置了:
①如果該位置沒有數據,用該數據新生成一個節點保存新數據,返回null;
②如果該位置有數據是一個紅黑樹,那麼執行相應的插入 / 更新操作
③如果該位置有數據是一個鏈表,分兩種情況一是該鏈表沒有這個節點,另一個是該鏈表上有這個節點,注意這裏判斷的依據是key.hash是否一樣:如果該鏈表沒有這個節點,那麼採用尾插法新增節點保存新數據,返回null;如果該鏈表已經有這個節點了,那麼找到該節點並更新新數據,返回老數據。注意: HashMap的put會返回key的上一次保存的數據。
get():
計算需獲取數據的hash值(計算過程跟put一樣),計算存放在數組table中的位置(計算過程跟put一樣),然後依次在數組,紅黑樹,鏈表中查找(通過equals()判斷),最後再判斷獲取的數據是否爲空,若爲空返回null否則返回該數據
樹化與還原
哈希表的最小樹形化容量
當哈希表中的容量大於這個值時(64),表中的桶才能進行樹形化
否則桶內元素太多時會擴容,而不是樹形化
爲了避免進行擴容、樹形化選擇的衝突,這個值不能小於 4 * TREEIFY_THRESHOLD
一個桶的樹化閾值
當桶中元素個數超過這個值時(8),需要使用紅黑樹節點替換鏈表節點
這個值必須爲 8,要不然頻繁轉換效率也不高
一個樹的鏈表還原閾值
當擴容時,桶中元素個數小於這個值(6),就會把樹形的桶元素還原(切分)爲鏈表結構
這個值應該比上面那個小,至少爲 6,避免頻繁轉換
條件1. 如果當前桶數組爲null或者桶數組的長度 < MIN_TREEIFY_CAPACITY(64),則進行擴容處理(見代碼片段2:resize());
條件2. 當不滿足條件1的時候則將桶中鏈表內的元素轉換成紅黑樹!!!稍後再詳細討論紅黑樹。
擴容機制的實現
擴容(resize)就是重新計算容量。當向HashMap對象裏不停的添加元素,而HashMap對象內部的桶數組無法裝載更多的元素時,HashMap對象就需要擴大桶數組的長度,以便能裝入更多的元素。
capacity 就是數組的長度/大小,loadFactor 是這個數組填滿程度的最大比比例。
size表示當前HashMap中已經儲存的Node<key,value>的數量,包括桶數組和鏈表 / 紅黑樹中的的Node<key,value>。
threshold表示擴容的臨界值,如果size大於這個值,則必需調用resize()方法進行擴容。
在jdk1.7及以前,threshold = capacity * loadFactor,其中 capacity 爲桶數組的長度。這裏需要說明一點,默認負載因子0.75是是對空間和時間(縱向橫向)效率的一個平衡選擇,建議大家不要修改。 jdk1.8對threshold值進行了改進,通過一系列位移操作算法最後得到一個power of two size的值
什麼時候擴容
當向容器添加元素的時候,會判斷當前容器的元素個數,如果大於等於閾值---即當前數組的長度乘以加載因子的值的時候,就要自動擴容啦。
擴容必須滿足兩個條件:
1、存放新值的時候 當前已有元素的個數 (size) 必須大於等於閾值
2、存放新值的時候當前存放數據發生hash碰撞(當前key計算的hash值換算出來的數組下標位置已經存在值)
//如果計算的哈希位置有值(及hash衝突),且key值一樣,則覆蓋原值value,並返回原值value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
resize()方法:該函數有2種使用情況1.初始化哈希表 2.當前數組容量過小,需擴容
過程:
插入鍵值對時發現容量不足,調用resize()方法方法,
1.首先進行異常情況的判斷,如是否需要初始化,二是若當前容量》最大值則不擴容,
2.然後根據新容量(是就容量的2倍)新建數組,將舊數組上的數據(鍵值對)轉移到新的數組中,這裏包括:(遍歷舊數組的每個元素,重新計算每個數據在數組中的存放位置(原位置或者原位置+舊容量),將舊數組上的每個數據逐個轉移到新數組中,這裏採用的是尾插法。)
3.新數組table引用到HashMap的table屬性上
4.最後重新設置擴容闕值,此時哈希表table=擴容後(2倍)&轉移了舊數據的新table
synchronized底層如何實現?鎖優化,怎麼優化?
synchronized 是 Java 內建的同步機制,所以也有人稱其爲 Intrinsic Locking,它提供了互斥的語義和可見性,當一個線程已經獲取當前鎖時,其他試圖獲取的線程只能等待或者阻塞在那裏。
原理:
synchronized可以保證方法或者代碼塊在運行時,同一時刻只有一個方法可以進入到臨界區,同時它還可以保證共享變量的內存可見性
底層實現:
同步代碼塊是使用monitorenter和monitorexit指令實現的,,當且一個monitor被持有之後,他將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor所有權,即嘗試獲取對象的鎖;
同步方法(在這看不出來需要看JVM底層實現)依靠的是方法修飾符上的ACC_SYNCHRONIZED實現。 synchronized方法則會被翻譯成普通的方法調用和返回指令如:invokevirtual、areturn指令,在VM字節碼層面並沒有任何特別的指令來實現被synchronized修飾的方法,而是在Class文件的方法表中將該方法的access_flags字段中的synchronized標誌位置1,表示該方法是同步方法並使用調用該方法的對象或該方法所屬的Class在JVM的內部對象表示 Klass 做爲鎖對象。
Java對象頭和monitor是實現synchronized的基礎!
synchronized存放的位置:
synchronized用的鎖是存在Java對象頭裏的。
其中, Java對象頭包括:
Mark Word(標記字段):用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳等等。它是實現輕量級鎖和偏向鎖的關鍵
Klass Pointer(類型指針):是對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例
monitor: 可以把它理解爲一個同步工具,它通常被描述爲一個對象。 是線程私有的數據結構
鎖優化,怎麼優化?
jdk1.6對鎖的實現引入了大量的優化。鎖主要存在四中狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨着競爭的激烈而逐漸升級。注意鎖可以升級不可降級,這種策略是爲了提高獲得鎖和釋放鎖的效率。重量級鎖降級發生於STW階段,降級對象爲僅僅能被VMThread訪問而沒有其他JavaThread訪問的對象。( HotSpot JVM/JRockit JVM是支持鎖降級的)
偏斜鎖:
當沒有競爭出現時,默認會使用偏斜鎖。JVM 會利用 CAS 操作(compare and swap),在對象頭上的 Mark Word 部分設置線程 ID,以表示這個對象偏向於當前線程,所以並不涉及真正的互斥鎖。
自旋鎖:
自旋鎖 for(;;)結合cas確保線程獲取取鎖
就是讓該線程等待一段時間,不會被立即掛起,看持有鎖的線程是否會很快釋放鎖。怎麼等待呢?執行一段無意義的循環即可(自旋)。
輕量級鎖:
引入偏向鎖主要目的是:爲了在無多線程競爭的情況下儘量減少不必要的輕量級鎖執行路徑。當關閉偏向鎖功能或者多個線程競爭偏向鎖導致偏向鎖升級爲輕量級鎖,則會嘗試獲取輕量級鎖
重量級鎖:
重量級鎖通過對象內部的監視器(monitor)實現,其中monitor的本質是依賴於底層操作系統的Mutex Lock實現,操作系統實現線程之間的切換需要從用戶態到內核態的切換,切換成本非常高。
ReentrantLock 底層實現
https://blog.csdn.net/u011202334/article/details/73188404
AQS原理:
AQS和Condition各自維護了不同的隊列,在使用lock和condition的時候,其實就是兩個隊列的互相移動。如果我們想自定義一個同步器,可以實現AQS。它提供了獲取共享鎖和互斥鎖的方式,都是基於對state操作而言的。
概念+實現:
ReentrantLock實現了Lock接口,是AQS( 一個用來構建鎖和同步工具的框架, AQS沒有鎖之類的概念)的一種。加鎖和解鎖都需要顯式寫出,注意一定要在適當時候unlock。ReentranLock這個是可重入的。其實要弄明白它爲啥可重入的呢,咋實現的呢。其實它內部自定義了同步器Sync,這個又實現了AQS,同時又實現了AOS,而後者就提供了一種互斥鎖持有的方式。其實就是每次獲取鎖的時候,看下當前維護的那個線程和當前請求的線程是否一樣,一樣就可重入了。
和synhronized相比:
synchronized相比,ReentrantLock用起來會複雜一些。在基本的加鎖和解鎖上,兩者是一樣的,所以無特殊情況下,推薦使用synchronized。ReentrantLock的優勢在於它更靈活、更強大,增加了輪訓、超時、中斷等高級功能。
可重入鎖。可重入鎖是指同一個線程可以多次獲取同一把鎖。ReentrantLock和synchronized都是可重入鎖。
可中斷鎖。可中斷鎖是指線程嘗試獲取鎖的過程中,是否可以響應中斷。synchronized是不可中斷鎖,而ReentrantLock則z,dz提供了中斷功能。
公平鎖與非公平鎖。公平鎖是指多個線程同時嘗試獲取同一把鎖時,獲取鎖的順序按照線程達到的順序,而非公平鎖則允許線程“插隊”。synchronized是非公平鎖,而ReentrantLock的默認實現是非公平鎖,但是也可以設置爲公平鎖。
lock()和unlock()是怎麼實現的呢?
由lock()和unlock的源碼可以看到,它們只是分別調用了sync對象的lock()和release(1)方法。而 Sync是ReentrantLock的內部類, 其擴展了AbstractQueuedSynchronizer。
lock():
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
首先用一個CAS操作,判斷state是否是0(表示當前鎖未被佔用),如果是0則把它置爲1,並且設置當前線程爲該鎖的獨佔線程,表示獲取鎖成功。當多個線程同時嘗試佔用同一個鎖時,CAS操作只能保證一個線程操作成功,剩下的只能乖乖的去排隊啦。( “非公平”即體現在這裏)。
設置state失敗,走到了else裏面。我們往下看acquire。
第一步。嘗試去獲取鎖。如果嘗試獲取鎖成功,方法直接返回。
2. 第二步,入隊。(自旋+CAS組合來實現非阻塞的原子操作)
3. 第三步,掛起。讓已經入隊的線程嘗試獲取鎖,若失敗則會被掛起
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
unlock():
流程大致爲先嚐試釋放鎖,若釋放成功,那麼查看頭結點的狀態是否爲SIGNAL,如果是則喚醒頭結點的下個節點關聯的線程,
如果釋放失敗那麼返回false表示解鎖失敗。這裏我們也發現了,每次都只喚起頭結點的下一個節點關聯的線程。
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
ConcurrentHashMap 的工作原理
概念:
ConcurrentHashMap的目標是實現支持高併發、高吞吐量的線程安全的HashMap。
1.8之前:
數據結構:
ConcurrentHashMap是由Segment數組結構和多個HashEntry數組結構組成。Segment是一種可重入鎖ReentrantLock,在ConcurrentHashMap裏扮演鎖的角色,HashEntry則用於存儲鍵值對數據。一個ConcurrentHashMap裏包含一個Segment數組,Segment的結構和HashMap相似,是一種數組和鏈表結構,一個Segment裏包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素,每個Segment守護者一個HashEntry數組裏的元素,當對HashEntry數組的數據進行修改時,必須首先獲得它對應的Segment鎖。
put和get的時候,都是現根據key.hashCode()算出放到哪個Segment中: ConcurrentHashMap中默認是把segments初始化爲長度爲16的數組
https://www.cnblogs.com/wuzhitong/p/8492228.html
1.8後:
變化:
ConcurrentHashMap的JDK8與JDK7版本的併發實現相比,最大的區別在於JDK8的鎖粒度更細,理想情況下talbe數組元素的大小就是其支持併發的最大個數
實現:
改進一:取消segments字段,直接採用transient volatile HashEntry<K,V>[] table保存數據,採用table數組元素作爲鎖,從而實現了對每一行數據進行加鎖,進一步減少併發衝突的概率。
數據結構:
改進二:將原先table數組+單向鏈表的數據結構,變更爲table數組+單向鏈表+紅黑樹的結構。對於hash表來說,最核心的能力在於將key hash之後能均勻的分佈在數組中。
概念:
JDK1.8的實現已經摒棄了Segment的概念,而是直接用Node數組+鏈表+紅黑樹的數據結構來實現,併發控制使用Synchronized和CAS來操作,整個看起來就像是優化過且線程安全的HashMap,雖然在JDK1.8中還能看到Segment的數據結構,但是已經簡化了屬性,只是爲了兼容舊版本。
樹化和還原:
與HashMap一樣。
一些成員:
Node是ConcurrentHashMap存儲結構的基本單元,繼承於HashMap中的Entry,用於存儲數據。,就是一個鏈表,但是隻允許對數據進行查找,不允許進行修改
通過TreeNode作爲存儲結構代替Node來轉換成黑紅樹。
TreeBin
TreeBin就是封裝TreeNode的容器,它提供轉換黑紅樹的一些條件和鎖的控制
// 讀寫鎖狀態
static final int WRITER = 1; // 獲取寫鎖的狀態
static final int WAITER = 2; // 等待寫鎖的狀態
static final int READER = 4; // 增加數據時讀鎖的狀態
構造器
public ConcurrentHashMap() {
} 初始化其實是一個空實現,初始化操作並不是在構造函數實現的,而是在put操作中實現。還提供了其他的構造函數,有指定容量大小或者指定負載因子,跟HashMap一樣。
存取實現:
put():對當前的table進行無條件自循環直到put成功
如果沒有初始化就先調用initTable()方法來進行初始化過程
如果沒有hash衝突就直接CAS插入
如果還在進行擴容操作就先進行擴容
如果存在hash衝突,就加鎖來保證線程安全,這裏有兩種情況,一種是鏈表形式就直接遍歷到尾端插入,一種是紅黑樹就按照紅黑樹結構插入,
最後一個如果該鏈表的數量大於閾值8,就要先轉換成黑紅樹的結構,break再一次進入循環
如果添加成功就調用addCount()方法統計size,並且檢查是否需要擴容。
get()
計算hash值,定位到該table索引位置,如果是首節點符合就返回
如果遇到擴容的時候,會調用標誌正在擴容節點ForwardingNode的find方法,查找該節點,匹配就返回
以上都不符合的話,就往下遍歷節點,匹配就返回,否則最後就返回null
概括版:
(1)對於get讀操作,如果當前節點有數據,還沒遷移完成,此時不影響讀,能夠正常進行。
如果當前鏈表已經遷移完成,那麼頭節點會被設置成fwd節點,此時get線程會幫助擴容。
(2)對於put/remove寫操作,如果當前鏈表已經遷移完成,那麼頭節點會被設置成fwd節點,此時寫線程會幫助擴容,如果擴容沒有完成,當前鏈表的頭節點會被鎖住,所以寫線程會被阻塞,直到擴容完成。
擴容機制:https://www.e-learn.cn/content/java/1154828
引入了一個ForwardingNode類,在一個線程發起擴容的時候,就會改變sizeCtl這個值,
sizeCtl :默認爲0,用來控制table的初始化和擴容操作,具體應用在後續會體現出來。
-1 代表table正在初始化
-N 表示有N-1個線程正在進行擴容操作 。
擴容時候會判斷這個值,
如果超過閾值就要擴容,首先根據運算得到需要遍歷的次數i,然後利用tabAt方法獲得i位置的元素f,初始化一個forwardNode實例fwd,如果f == null,則在table中的i位置放入fwd,
否則採用頭插法的方式把當前舊table數組的指定任務範圍的數據給遷移到新的數組中,
然後
給舊table原位置賦值fwd。直到遍歷過所有的節點以後就完成了複製工作,把table指向nextTable,並更新sizeCtl爲新數組大小的0.75倍,擴容完成。在此期間如果其他線程的有讀寫操作都會判斷head節點是否爲forwardNode節點,如果是就幫助擴容。
Hashtable底層原理:
概念:
HashTable類繼承自Dictionary類,實現了Map接口。 大部分的操作都是通過synchronized鎖保護的,是線程安全的, key、value都不可以爲null, 每次put方法不允許null值,如果發現是null,則直接拋出異常。
官方文檔也說了:如果在非線程安全的情況下使用,建議使用HashMap替換,如果在線程安全的情況下使用,建議使用ConcurrentHashMap替換。
數據結構:
數組+鏈表。
存取實現:
put():
限制了value不能爲null。
由於直接使用key.hashcode(),而沒有向hashmap一樣先判斷key是否爲null,所以key爲null時,調用key.hashcode()會出錯,所以hashtable中key也不能爲null。
Hashtable是在鏈表的頭部添加元素的。
int index = (hash & 0x7FFFFFFF) %tab.length;獲取index的方式與HashMap不同
擴容機制:
Hashtable默認capacity是11,默認負載因子是0.75.。當前表中的Entry數量,如果超過了閾值,就會擴容,即調用rehash方法,重新計算每個鍵值對的hashCode;
判斷新的容量是否超過了上限,沒超過就新建一個新數組,大小爲原數組的2倍+1,將舊數的鍵值對重新hash添加到新數組中。
JVM調優
查看堆空間大小分配(年輕代、年老代、持久代分配)
垃圾回收監控(長時間監控回收情況)
線程信息監控:系統線程數量
線程狀態監控:各個線程都處在什麼樣的狀態下
線程詳細信息:查看線程內部運行情況,死鎖檢查
CPU熱點:檢查系統哪些方法佔用了大量CPU時間
內存熱點:檢查哪些對象在系統中數量最大
jvm問題排查和調優:
jps主要用來輸出JVM中運行的進程狀態信息。
jstat命令可以用於持續觀察虛擬機內存中各個分區的使用率以及GC的統計數據
jmap可以用來查看堆內存的使用詳情。
jstack可以用來查看Java進程內的線程堆棧信息。 jstack是個非常好用的工具,結合應用日誌可以迅速定位到問題線程。
Java性能分析工具
jdk會自帶JMC(JavaMissionControl)工具。可以分析本地應用以及連接遠程ip使用。提供了實時分析線程、內存,CPU、GC等信息的可視化界面。
JVM內存調優
對JVM內存的系統級的調優主要的目的是減少GC的頻率和Full GC的次數。過多的GC和Full GC是會佔用很多的系統資源(主要是CPU),影響系統的吞吐量。
使用JDK提供的內存查看工具,比如JConsole和Java VisualVM。
導致Full GC一般由於以下幾種情況:
舊生代空間不足
調優時儘量讓對象在新生代GC時被回收、讓對象在新生代多存活一段時間和不要創建過大的對象及數組避免直接在舊生代創建對象
新生代設置過小
一是新生代GC次數非常頻繁,增大系統消耗;二是導致大對象直接進入舊生代,佔據了舊生代剩餘空間,誘發Full GC
2). 新生代設置過大
一是新生代設置過大會導致舊生代過小(堆總量一定),從而誘發Full GC;二是新生代GC耗時大幅度增加
3). Survivor設置過小
導致對象從eden直接到達舊生代
4). Survivor設置過大
導致eden過小,增加了GC頻率
一般說來新生代佔整個堆1/3比較合適
GC策略的設置方式
1). 吞吐量優先可由-XX:GCTimeRatio=n來設置
2). 暫停時間優先可由-XX:MaxGCPauseRatio=n來設置
JVM內存管理:
1.先講內存5大模塊以及他們各種的作用。
2.將垃圾收集器,垃圾收集算法
3.適當講講GC優化,JVM優化
JVM的常見的垃圾收集器:
(注:此回答源於楊曉峯的Java核心技術36講之一)
GC調優:
GC日誌分析
調優命令
調優工具
調優命令
Sun JDK監控和故障處理命令有jps jstat jmap jhat jstack jinfo
jps,JVM Process Status Tool,顯示指定系統內所有的HotSpot虛擬機進程。
jstat,JVM statistics Monitoring是用於監視虛擬機運行時狀態信息的命令,它可以顯示出虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據。
jmap,JVM Memory Map命令用於生成heap dump文件
jhat,JVM Heap Analysis Tool命令是與jmap搭配使用,用來分析jmap生成的dump,jhat內置了一個微型的HTTP/HTML服務器,生成dump的分析結果後,可以在瀏覽器中查看
jstack,用於生成java虛擬機當前時刻的線程快照。
jinfo,JVM Configuration info 這個命令作用是實時查看和調整虛擬機運行參數。
調優工具
常用調優工具分爲兩類,jdk自帶監控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。
jconsole,Java Monitoring and Management Console是從java5開始,在JDK中自帶的java監控和管理控制檯,用於對JVM中內存,線程和類等的監控
GC觸發的條件有兩種。(1)程序調用System.gc時可以觸發;(2)系統自身來決定GC觸發的時機。
要完全回收一個對象,至少需要經過兩次標記的過程。
第一次標記:對於一個沒有其他引用的對象,篩選該對象是否有必要執行finalize()方法,如果沒有執行必要,則意味可直接回收。(篩選依據:是否複寫或執行過finalize()方法;因爲finalize方法只能被執行一次)。
第二次標記:如果被篩選判定位有必要執行,則會放入FQueue隊列,並自動創建一個低優先級的finalize線程來執行釋放操作。如果在一個對象釋放前被其他對象引用,則該對象會被移除FQueue隊列。
Minor GC ,Full GC 觸發條件
Minor GC觸發條件:當Eden區滿時,觸發Minor GC。
Full GC觸發條件:
(1)調用System.gc時,系統建議執行Full GC,但是不必然執行
(2)老年代空間不足
(3)方法區空間不足
(4)通過Minor GC後進入老年代的平均大小大於老年代的可用內存
(5)由Eden區、From Space區向To Space區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小
java內存模型
與JVM 內存模型不同。
Java內存模型即Java Memory Model,簡稱JMM。JMM定義了Java 虛擬機(JVM)在計算機內存(RAM)中的工作方式。JVM是整個計算機虛擬模型,所以JMM是隸屬於JVM的。
Java內存模型定義了多線程之間共享變量的可見性以及如何在需要的時候對共享變量進行同步。
Java線程之間的通信採用的是過共享內存模型,這裏提到的共享內存模型指的就是Java內存模型(簡稱JMM),JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。
線程池的工作原理
1.先講下作用
減少資源的開銷 可以減少每次創建銷燬線程的開銷
提高響應速度 由於線程已經創建成功
提高線程的可管理性
2.講實現
線程池主要有兩部分組成,多個工作線程和一個阻塞隊列。
其中工作線程是一組已經處在運行中的線程,它們不斷地向阻塞隊列中領取任務執行。而阻塞隊列用於存儲工作線程來不及處理的任務。
3.細分講下線程的組成
創建一個線程池需要要的一些核心參數。
corePoolSize:基本線程數量它表示你希望線程池達到的一個值。線程池會盡量把實際線程數量保持在這個值上下。
maximumPoolSize:最大線程數量這是線程數量的上界。如果實際線程數量達到這個值:阻塞隊列未滿:任務存入阻塞隊列等待執行阻塞隊列已滿:調用飽和策略。
keepAliveTime:空閒線程的存活時間當實際線程數量超過corePoolSize時,若線程空閒的時間超過該值,就會被停止。 PS:當任務很多,且任務執行時間很短的情況下,可以將該值調大,提高線程利用率。
timeUnit:keepAliveTime的單位
runnableTaskQueue:任務隊列
這是一個存放任務的阻塞隊列,可以有如下幾種選擇:
ArrayBlockingQueue 它是一個由數組實現的阻塞隊列,FIFO。
LinkedBlockingQueue 它是一個由鏈表實現的阻塞隊列,FIFO。吞吐量通常要高於ArrayBlockingQueue。fixedThreadPool使用的阻塞隊列就是它。它是一個無界隊列。
SynchronousQueue 它是一個沒有存儲空間的阻塞隊列,任務提交給它之後必須要交給一條工作線程處理;如果當前沒有空閒的工作線程,則立即創建一條新的工作線程。 cachedThreadPool用的阻塞隊列就是它。它是一個無界隊列。 PriorityBlockingQueue 它是一個優先權阻塞隊列。
handler:飽和策略當實際線程數達到maximumPoolSize,並且阻塞隊列已滿時,就會調用飽和策略。
AbortPolicy 默認。直接拋異常。 CallerRunsPolicy 只用調用者所在的線程執行任務。 DiscardOldestPolicy 丟棄任務隊列中最久的任務。 DiscardPolicy 丟棄當前任務。
4.運行機制
當有請求到來時:
1.若當前實際線程數量少於 corePoolSize,即使有空閒線程,也會創建一個新的工作線程;
2 若當前實際線程數量處於corePoolSize和maximumPoolSize之間,並且阻塞隊列沒滿,則任務將被放入阻塞隊列中等待執行;
3.若當前實際線程數量小於 maximumPoolSize,但阻塞隊列已滿,則直接創建新線程處理任務;
4.若當前實際線程數量已經達到maximumPoolSize,並且阻塞隊列已滿,則使用飽和策略。
ThreadLocal的底層原理
概括:
該類提供了線程局部 (thread-local) 變量。這些變量不同於它們的普通對應物,因爲訪問某個變量(通過其 get 或 set 方法)的每個線程都有自己的局部變量
使用:
set(obj):向當前線程中存儲數據 get():獲取當前線程中的數據 remove():刪除當前線程中的數據
實現原理:
ThreadLocal並不維護ThreadLocalMap(ThreadLocalMap是Thread的)並不是一個存儲數據的容器,它只是相當於一個工具包,提供了操作該容器的方法,如get、set、remove等。而ThreadLocal內部類ThreadLocalMap纔是存儲數據的容器,並且該容器由Thread維護。每一個Thread對象均含有一個ThreadLocalMap類型的成員變量threadLocals,它存儲本線程中所有ThreadLocal對象及其對應的值( ThreadLocalMap 是個弱引用類,內部一個Entry由ThreadLocal對象和Object構成,
爲什麼要用弱引用呢?
如果是直接new一個對象的話,使用完之後設置爲null後才能被垃圾收集器清理,如果爲弱引用,使用完後垃圾收集器自動清理key,程序員不用再關注指針。
操作細節
進行set,get等操作都是首先會獲取當前線程對象,然後獲取當前線程的ThreadLocalMap對象。再以當前ThreadLocal對象爲key ,再做相應的處理。
內存泄露問題
在ThreadLocalMap中,只有key是弱引用,value仍然是一個強引用。
每次操作set、get、remove操作時,ThreadLocal都會將key爲null的Entry刪除,從而避免內存泄漏。
當然,當如果一個線程運行週期較長,而且將一個大對象放入LocalThreadMap後便不再調用set、get、remove方法,此時該仍然可能會導致內存泄漏。這個問題確實存在,沒辦法通過ThreadLocal解決,而是需要程序員在完成ThreadLocal的使用後要養成手動調用remove的習慣,從而避免內存泄漏。
使用場景;
Web系統Session的存儲
當請求到來時,可以將當前Session信息存儲在ThreadLocal中,在請求處理過程中可以隨時使用Session信息,每個請求之間的Session信息互不影響。當請求處理完成後通過remove方法將當前Session信息清除即可。
voliate 的實現原理
爲什麼volatile能保證共享變量的內存可見性?
volatile變量寫
當被volatile修飾的變量進行寫操作時,這個變量將會被直接寫入共享內存,而非線程的專屬存儲空間。
volatile變量讀
當讀取一個被volatile修飾的變量時,會直接從共享內存中讀,而非線程專屬的存儲空間中讀。
禁止指令重排序
volatile讀
若volatile讀操作的前一行爲volatile讀/寫,則這兩行不會發生重排序 volatile讀操作和它後一行代碼都不會發生重排序
volatile寫
volatile寫操作和它前一行代碼都不會發生重排序; 若volatile寫操作的後一行代碼爲volatile讀/寫,則這兩行不會發生重排序。
當volatile變量寫後,線程中本地內存中共享變量就會置爲失效的狀態,因此線程B再需要讀取從主內存中去讀取該變量的最新值。
NIO底層原理
1概念:
NIO 指新IO,核心是同步非阻塞,解決傳統IO的阻塞問題。操作對象是Buffer。其實NIO的核心是IO線程池,(一定要記住這個關鍵點)。 NIO中的IO多路複用調用系統級別的select和poll模型,由系統進行監控IO狀態,避免用戶線程通過反覆嘗試的方式查詢狀態。
Java NIO :同步非阻塞,服務器實現模式爲一個請求一個線程,即客戶端發送的連接請求都會註冊到多路複用器上,多路複用器輪詢到連接有I/O請求時才啓動一個線程進行處理。
2.工作原理:
由一個專門的線程來處理所有的 IO 事件,並負責分發。
事件驅動機制:事件到的時候觸發,而不是同步的去監視事件。
線程通訊:線程之間通過 wait,notify 等方式通訊。保證每次上下文切換都是有意義的。減少無謂的線程切換。
3.通信模型是怎麼實現的呢?
java NIO採用了雙向通道(channel)進行數據傳輸,而不是單向的流(stream),在通道上可以註冊我們感興趣的事件。
四種事件
服務端接收客戶端連接事件SelectionKey.OP_ACCEPT(16)
客戶端連接服務端事件SelectionKey.OP_CONNECT(8)
讀事件SelectionKey.OP_READ(1)
寫事件SelectionKey.OP_WRITE(4)
服務端和客戶端各自維護一個管理通道的對象,我們稱之爲selector,該對象能檢測一個或多個通道 (channel) 上的事件。我們以服務端爲例,如果服務端的selector上註冊了讀事件,某時刻客戶端給服務端發送了一些數據,阻塞I/O這時會調用read()方法阻塞地讀取數據,而NIO的服務端會在selector中添加一個讀事件。服務端的處理線程會輪詢地訪問selector,如果訪問selector時發現有感興趣的事件到達,則處理這些事件,如果沒有感興趣的事件到達,則處理線程會一直阻塞直到感興趣的事件到達爲止。
IOC底層實現原理
概念:
IOC 是面向對象編程中的一種設計原則,IOC理論提出的觀點大體是這樣的:藉助於“第三方”實現具有依賴關係的對象之間的解耦。。所謂IoC,對於spring框架來說,就是由spring來負責控制對象的生命週期和對象間的關係。是說創建對象的控制權進行轉移,以前創建對象的主動權和創建時機是由自己把控的,而現在這種權力轉移到第三方。
實現原理:
它是通過反射機制+工廠模式實現的,在實例化一個類時,它通過反射調用類中set方法將事先保存在HashMap中的類屬性注入到類中。
控制反轉就是:獲得依賴對象的方式反轉了。
1、依賴注入發生的時間
(1).用戶第一次通過getBean方法向IoC容索要Bean時,IoC容器觸發依賴注入。
(2).當用戶在Bean定義資源中爲元素配置了lazy-init屬性,即讓容器在解析註冊Bean定義時進行預實例化,觸發依賴注入。
2.依賴注入實現在以下兩個方法中:
(1).createBeanInstance:生成Bean所包含的java對象實例。
(2).populateBean :對Bean屬性的依賴注入進行處理。
AOP底層實現原理
概念
AOP(Aspect-OrientedProgramming,面向方面編程),可以說是OOP(Object-Oriented Programing,面向對象編程)的補充和完善。OOP引入封裝、繼承和多態性等概念來建立一種對象層次結構,用以模擬公共行爲的一個集合。 而AOP技術則恰恰相反,它利用一種稱爲“橫切”的技術,剖解開封裝的對象內部,並將那些影響了多個類的公共行爲封裝到一個可重用模塊,並將其名爲“Aspect”,即方面。簡單地說,就是將那些與業務無關,卻爲業務模塊所共同調用的邏輯或責任封裝起來,便於減少系統的重複代碼,降低模塊間的耦合度,並有利於未來的可操作性和可維護性。
AOP的核心思想就是“將應用程序中的商業邏輯同對其提供支持的通用服務進行分離。
AOP的實現
實現AOP的技術,主要分爲兩大類:一是採用動態代理技術,利用截取消息的方式,對該消息進行裝飾,以取代原有對象行爲的執行;二是採用靜態織入的方式,引入特定的語法創建“方面”,從而使得編譯器可以在編譯期間織入有關“方面”的代碼。
如何使用Spring AOP
可以通過配置文件或者編程的方式來使用Spring AOP。 配置可以通過xml文件來進行,大概有四種方式:
配置ProxyFactoryBean,顯式地設置advisors, advice, target等
2. 配置AutoProxyCreator,這種方式下,還是如以前一樣使用定義的bean,但是從容器中獲得的其實已經是代理對象
3. 通過來配置
4. 通過來配置,使用AspectJ的註解來標識通知及切入點
Spring AOP的實現
如何生成代理類:
Spring提供了兩種方式來生成代理對象: JDKProxy和Cglib,具體使用哪種方式生成由AopProxyFactory根據AdvisedSupport對象的配置來決定。默認的策略是如果目標類是接口,則使用JDK動態代理技術,否則使用Cglib來生成代理
切面是如何織入的?
InvocationHandler是JDK動態代理的核心,生成的代理對象的方法調用都會委託到InvocationHandler.invoke()方法。
MyisAM和innodb的有關索引的疑問
兩者都是什麼索引?聚集還是非聚集https://www.cnblogs.com/olinux/p/5217186.html
MyISAM(非聚集)
使用B+Tree作爲索引結構,葉節點的data域存放的是數據記錄的地址。
MyISAM中索引檢索的算法爲首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,則取出其data域的值,然後以data域的值爲地址,讀取相應數據記錄。
InnoDB(聚集索引)
第一個重大區別是InnoDB的數據文件本身就是索引文件,這棵樹的葉節點data域保存了完整的數據記錄。
但是輔助索引搜索需要檢索兩遍索引:首先檢索輔助索引獲得主鍵,然後用主鍵到主索引中檢索獲得記錄。
因爲InnoDB的數據文件本身要按主鍵聚集,所以InnoDB要求表必須有主鍵(MyISAM可以沒有),如果沒有顯式指定,則MySQL系統會自動選擇一個可以唯一標識數據記錄的列作爲主鍵,如果不存在這種列,則MySQL自動爲InnoDB表生成一個隱含字段作爲主鍵,這個字段長度爲6個字節,類型爲長整形。
簡單說:
如果我們定義了主鍵(PRIMARY KEY),那麼InnoDB會選擇其作爲聚集索引;如果沒有顯式定義主鍵,則InnoDB會選擇第一個不包含有NULL值的唯一索引作爲主鍵索引
關於面試準備
先推薦一個寫的不錯的博客,專門關於面試的,比較詳盡仔細:關於面試。我在這裏簡單總結幾點:
1、簡歷要用心準備好,個人信息,特別是聯繫方式一定要清晰明確,自身掌握的技能要完成清晰,項目經歷最好按照時間順序,說明本人在項目中的職責,完成的工作,有什麼樣的提升或收穫;
2、一般面試流程是電面=》HR現場面=》技術面=》結果,並不是每一個面試結果就能立馬有結果,所以當面試官說回去等消息的時候,並不代表沒有機會,有時候需要討論篩選才能最終確定人選。
3、關於自我介紹,最好簡明扼要,能體現自身的特點,表達流暢、自信,提前最好準備;
4、準備好紮實的基礎知識,以及對經歷過的項目要有足夠的認識,每一個項目都是一次學習、提升的機會,一般JAVA集合類是考察的重點;
5、一般好一點的面試官會順着知識點逐漸深入或者逐漸擴展,所以對於知識點的掌握最好全面深入,不要走馬觀花式的學習;
6、當遇到一些設計類的問題時,一般面試官考察的是你的思路,對問題的應變能力,對於事物觀察的點;
JAVA基礎(答案僅供參考,如有不對之處請批評指正)
1、HashMap源碼,實現原理,JDK8以後對HashMap做了怎樣的優化。
答:HashMap是基於哈希表的Map接口的非同步實現,提供所有可選的映射操作,並允許使用null值和null鍵,不保證映射的順序;HashMap是一個“鏈表散列”的數據結構,即數組和鏈表的結合體;它的底層就是一個數組結構,數組中的每一項又是一個鏈表,每當新建一個HashMap時,就會初始化一個數組;
可參考博客:徹底搞懂JAVA集合HashMap,HashTable,ConcurrentHashMap之關聯
而在JDK8中引入了紅黑樹的部分,當存入到數組中的鏈表長度大於(默認)8時,即轉爲紅黑樹;利用紅黑樹快速增刪改查的特點提高HashMap的性能,其中會用到紅黑樹的插入、刪除、查找等算法。本文不再對紅黑樹展開討論,想了解更多紅黑樹數據結構的工作原理可以參考http://blog.csdn.net/v_july_v/article/details/6105630。
可參考博客:JAVA8系列之重新認識HashMap
2、HashMap的擴容是怎樣擴容的,爲什麼都是2的N次冪的大小。
答:可以參考上文 JAVA8系列之重新認識HashMap 有詳細的講解
3、HashMap,HashTable,ConcurrentHashMap的區別
答:
a、HashMap是非線程安全的,HashTable是線程安全的。
b、HashMap的鍵和值都允許有null值存在,而HashTable則不行。
c、因爲線程安全的問題,HashMap效率比HashTable的要高。
HashMap:它根據鍵的hashCode值存儲數據,大多數情況下可以直接定位到它的值,因而具有很快的訪問速度,但遍歷順序卻是不確定的。 HashMap最多隻允許一條記錄的鍵爲null,允許多條記錄的值爲null。HashMap非線程安全,即任一時刻可以有多個線程同時寫HashMap,可能會導致數據的不一致。如果需要滿足線程安全,可以用 Collections的synchronizedMap方法使HashMap具有線程安全的能力,或者使用ConcurrentHashMap。
Hashtable:Hashtable是遺留類,很多映射的常用功能與HashMap類似,不同的是它承自Dictionary類,並且是線程安全的,任一時間只有一個線程能寫Hashtable,併發性不如ConcurrentHashMap,因爲ConcurrentHashMap引入了分段鎖。
4、極高併發下HashTable和ConcurrentHashMap哪個性能更好,爲什麼,如何實現的。
答:當然是ConcurrentHashMap,因爲ConcurrentHashMap引入了分段鎖,而HashTable則使用的是方法級別的鎖;因此在新版本中一般不建議使用HashTable,不需要線程安全的場合可以使用HashMap,而需要線程安全的場合可以使用ConcurrentHashMap;
5、HashMap在高併發下如果沒有處理線程安全會有怎樣的隱患,具體表現是什麼。
答:可能造成死循環,具體表現鏈表的循環指向;
6、JAVA中四種修飾符的限制範圍。
private:修飾的成員只能在同類中別訪問,而在同包、子類和其他包中都不能被訪問
public:修飾的成員在同類、同包、子類(繼承自本類)、其他包都可以訪問
protected:修飾的成員在同類、同包、子類中可以訪問,其他包中不能被訪問
default:修飾的成員在同類、同包中可以訪問,但其他包中不管是不是子類都不能被訪問
7、Object中的方法
構造函數
hashCode():用戶獲取對象的hash值,用於檢索
queals():用於確認兩個對象是否相等;補充,哈希值相同的對象不一定equals(),但equals()的兩個對象,hash值一定相等
toString():返回一個String對象,用來標識自己
getClass():返回一個class對象,打印的格式一般爲 class package.name.xxx,經常用於java的反射機制
clone():用來另存一個當前存在的對象
finalize():垃圾回收的時候回用到,匿名對象回收之前會調用到
wait():用於讓當前線程失去操作權限,當前線程進入等待序列
wait(long)、wait(long,int):用戶設定下一次獲取鎖的距離當前釋放鎖的間隔時間
notify():用於隨機通知一個持有對象鎖的線程獲取操作的權限
notifyAll():用於通知所有持有對象鎖的線程獲取操作權限
8、接口和抽象類的區別
答:一個類可以實現多個接口,但只能繼承一個抽象類;抽象類可以包含具體的方法,接口所有的方法都是抽象的(JDK8開始新增功能接口中有default方法);抽象類可以聲明和使用字段,接口則不能,但可以創建靜態的final常量;抽象類的方法可以是protected、public、private或者默認的package,接口的方法都是public;抽象類可以定義構造函數,接口不能;接口被聲明爲public,省略後,包外的類不能訪問接口;
9、動態代理的兩種方式,以及區別
答:jdk動態代理和cglib動態代理;
JDK動態代理只能對實現了接口的類生成代理,而不能針對類;cglib是針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法,因爲是繼承,所以該類或方法最好不要聲明稱final,final可以阻止繼承和多態;
10、java序列化的方式
答:實現Serializable接口、實現Externalizable接口(一般只希望序列化一部分數據,其他數據都使用transient修飾的話有點麻煩,這時候可以使用externalizable接口,指定序列化的屬性)
11、傳值和傳引用的區別,java是怎麼樣的,有沒有傳值傳引用
答:首先,java中是沒有指針的,只存在值傳遞;而我們經常看到對於對象的傳遞似乎有點像引用傳遞,可以改變對象中的某個屬性的值,請不要被這個假象矇蔽了雙眼,實際上這個傳入函數的值是對象引用的拷貝,即傳遞的是引用的地址值,所以還是按值傳遞;
傳值調用時,改變的是形參的值,並沒有改變實參的值,實參的值可以傳遞給形參,但是這個傳遞是單向的,形參不能傳遞會實參;
傳引用調用時,如果參數是對象,無論是對象做了何種操作,都不會改變實參對象的引用,但是如果改變了對象的內容,就會改變實參對象的內容;
12、@transactional註解在什麼情況下會失效,爲什麼。
答:一個目標對象的方法調用改目標對象的另外一個方法時,即使被調用的方法已使用了@Transactional註解標記,事務也不會有效執行;Spring的官方說明在代理下(默認或者配置爲proxy-targer-class="true"),只有當前代理類的外部方法調用註解方法時代理纔會被攔截。
數據結構和算法
1、B+樹
參考:B+樹介紹
2、八大排序算法
參考:八大排序算法JAVA實現
3、一致性Hash算法,一致性Hash算法的應用
答:一致性hash算法是一個負載均衡算法,可以用在分佈式緩存、數據庫的分庫分表等場景,還可以應用在負載均衡器中作爲負載均衡算法。在多臺服務器時,對於某個請求資源通過hash算法,映射到某一臺服務器,當增加或者減少一臺服務器時,可能會改變這些資源對應的hash值,這樣可能導致一部分緩存或者數據的丟失。一致性hash就是儘可能在將同一個資源請求到同一臺服務器中;
JVM
1、JVM的內存結構
答:主要分爲三大塊堆內存、方法區、棧;棧又分爲JVM棧、本地方法棧
堆(heap space),堆內存是JVM中最大的一塊,有年輕代和老年代組成,而年輕代又分爲三分部分,Eden區,From Survivor,To Survivor,默認情況下按照8:1:1來分配
方法區(Method area),存儲類信息、常量、靜態變量等數據,是線程共享的區域
程序計數器(Program counter Register),是一塊較小的內存空間,是當前線程所執行的字節碼的行號指示器
JVM棧(JVM stacks),也是線程私有的,生命週期與線程相同,每個方法被執行時都會創建一個棧幀,用於存儲局部變量表、操作棧、動態鏈接、方法出口等信息
本地方法棧(Native Mthod Stacks),爲虛擬機使用的native方法服務
2、關於垃圾回收和常見的GC算法,請參考:GC專家系列-理解java垃圾回收
多線程
1、JAVA實現多線程的幾種方式
a、繼承Thread類實現
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
b、實現Runnable接口
如果自己的類已經extends另一個類,就無法直接extends Thread,此時,必須實現一個Runnable接口,如下:
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
c、使用ExecutorService、Callable、Future實現有返回結果的多線程
import java.util.concurrent.*;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
/**
* 有返回值的線程
*/
@SuppressWarnings("unchecked")
public class Test {
public static void main(String[] args) throws ExecutionException,
InterruptedException {
System.out.println("----程序開始運行----");
Date date1 = new Date();
int taskSize = 5;
// 創建一個線程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 創建多個有返回值的任務
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 執行任務並獲取Future對象
Future f = pool.submit(c);
// System.out.println(">>>" + f.get().toString());
list.add(f);
}
// 關閉線程池
pool.shutdown();
// 獲取所有併發任務的運行結果
for (Future f : list) {
// 從Future對象上獲取任務的返回值,並輸出到控制檯
System.out.println(">>>" + f.get().toString());
}
Date date2 = new Date();
System.out.println("----程序結束運行----,程序運行時間【"
+ (date2.getTime() - date1.getTime()) + "毫秒】");
}
}
class MyCallable implements Callable<Object> {
private String taskNum;
MyCallable(String taskNum) {
this.taskNum = taskNum;
}
public Object call() throws Exception {
System.out.println(">>>" + taskNum + "任務啓動");
Date dateTmp1 = new Date();
Thread.sleep(1000);
Date dateTmp2 = new Date();
long time = dateTmp2.getTime() - dateTmp1.getTime();
System.out.println(">>>" + taskNum + "任務終止");
return taskNum + "任務返回運行結果,當前任務時間【" + time + "毫秒】";
}
}
2、Callable和Future
答:Callable接口類似於Runnable,但是Runnable不會返回結果,並且無法拋出返回結果的異常,而Callable更強大,被線程執行以後,可以返回值,這個返回值就是通過Future拿到,也就是說,Future可以拿到異步執行任務的返回值,可以看以下例子:
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class Test {
public static void main(String[] args) {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return new Random().nextInt(100);
}
};
FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);
new Thread(futureTask).start();
try {
Thread.sleep(1000);
System.err.println(futureTask.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
ExecutorService繼承自Executor,目的是爲我們管理Thread對象,從而簡化併發變成,Executor使我們無需顯示的去管理線程的聲明週期,是JDK5之後啓動任務的首選方式。
執行多個帶返回值的任務,並取得多個返回值,代碼如下:
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CallableAndFuture {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
CompletionService<Integer> cs = new ExecutorCompletionService<Integer>(threadPool);
for( int i = 0; i < 5; i++ ){
final int taskId = i;
cs.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return taskId;
}
});
}
for( int i = 0; i < 5; i++ ){
try {
System.err.println(cs.take().get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
3、線程池的參數有哪些,在線程池創建一個線程的過程
corePoolSize:核心線程數,能夠同時執行的任務數量
maximumPoolSize:除去緩衝隊列中等待的任務,最大能容納的任務數(其實就是包括了核心線程池的數量)
keepAliveTime:超出workQueue的等待任務的存活時間,就是指maximumPoolSize裏面的等待任務的存活等待時間
unit:時間單位
workQueue:阻塞等待線程的隊列,一般使用new LinkedBlockingQueue()這個,如果不指定容量,會一直往裏添加,沒有限制,workQueue永遠不會滿,一般選擇沒有容量上限的隊列
threadFactory:創建線程的工廠,使用系統默認的類
handler:當任務數超過maximumPoolSize時,對任務的處理策略,默認策略是拒絕添加
執行流程:當線程數小於corePoolSize時,每添加一個任務,則立即開啓線程執行;當corePoolSize滿的時候,後面添加的任務將放入緩衝隊列workQueue等待;當workQueue滿的時候,看是否超過maximumPoolSize線程數,如果超過,則拒絕執行,如果沒有超過,則創建線程理解執行;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit
/**
* 對線程池進行管理和封裝
* @author guoqing
*
*/
public class ThreadPoolManager {
private static ThreadPoolManager mInstance = new ThreadPoolManager();
private ThreadPoolExecutor executor;
private int corePoolSize; //核心線程池數量,表示能夠同時執行的任務數量
private int maximumPoolSize; //最大線程池數量,其實是包含了核心線程池數量在內的
private long keepAliveTime = 1; //存活時間,表示最大線程池中等待任務的存活時間
private TimeUnit unit = TimeUnit.HOURS; //存活時間的時間單位
public static ThreadPoolManager getInstance() {
return mInstance;
}
private ThreadPoolManager() {
//核心線程數量的計算規則:當前設備的可用處理器核心數*2+1,能夠讓cpu得到最大效率的發揮
corePoolSize = Runtime.getRuntime().availableProcessors()*2+1;
maximumPoolSize = corePoolSize; //雖然用不到,但是不能爲0,否則會報錯
//線程池機制:領工資的機制
executor = new ThreadPoolExecutor(corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
new LinkedBlockingQueue<Runnable>(), //緩衝隊列,超出核心線程池的任務會被放入緩衝隊列中等待
Executors.defaultThreadFactory(), //創建線程的工廠類
new ThreadPoolExecutor.AbortPolicy() //當最大線程池也超出的時候,則拒絕執行
);
}
/**
* 往線程池中添加任務
* @param r
*/
public void executor(Runnable r) {
if(r!=null) {
executor.execute(r);
}
}
/**
* 從線程池中移除任務
* @param r
*/
public void remove(Runnable r) {
if(r!=null) {
executor.remove(r);
}
}
}
4、volatile關鍵字的作用,原理
答:保證內存可見性和禁止指令重排。實現原理可參考:JAVA併發變成--valatile關鍵字剖
5、synchronized關鍵字的用法,優缺點
答:java關鍵字,當它用來修飾一個方法或者代碼塊的時候,能夠保證在同一時刻最多隻有一個線程執行該代碼段的代碼;
synchronized修飾的方法或者對象,只能以同步的方式執行,會引起性能問題;無法中斷一個正在等候獲得鎖的線程,也無法通過投票獲得鎖;一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級倒置,引起性能風險;
6、Lock接口有哪些實現類,使用場景是什麼
答:Lock接口有三個實現類,一個是ReentrantLock,另兩個是ReentrantReadWriteLock類中的兩個靜態內部類ReadLock和WriteLock。
使用場景:一般應用於多度少寫,因爲讀的線程之間沒有競爭,所以比起synchronzied,性能要好很多;
7、悲觀鎖、樂觀鎖的優缺點,CAS有什麼缺陷,該如何解決
悲觀鎖:總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次拿數據的時候都會上鎖,這樣別人拿數據的時候就會阻塞知道它拿到鎖;比如關係型數據庫的行鎖、表鎖、讀鎖、寫鎖;比如java裏面的同步原語synchronized關鍵字的實現也是悲觀鎖;
樂觀鎖:每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下再次期間別人有沒有更新這個數據。樂觀鎖適用於多讀的應用類型,可以提高吞吐量。java中java.util.conncurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的;
CAS:CAS是樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其他線程都失敗,失敗的線程不會被掛起,而是被告知這次競爭失敗,並可以再次嘗試;
CAS的缺陷:ABA問題、循環時間長開銷大,只能保證一個共享變量的原子操作;
8、ABC三個線程如何保證順序執行
答:用Thread.join() 方法,或者線程池newSingleThreadExecutor(原理是會將所有線程放入一個隊列,而隊列則保證了FIFO),也可以通過ReentrantLock,state整數用阿里判斷輪到誰來執
9、線程的狀態都有哪些(五大狀態)
新建狀態(new):當用new操作符創建一個線程時,如new Thread(),線程還沒有開始運行,此時處於仙劍狀態;
就緒狀態(runnable):一個新創建的線程並不自動開始運行,要執行線程,必須要調用線程的start()方法,當線程對象調用start()方法即啓動了線程,start()方法創建線程運行的系統資源,並調度線程運行run()方法。當start()方法返回後,線程就處於就緒狀態;
運行狀態(running):當線程獲得cpu時間後,他才進入運行狀態,真正開始實行run()方法
阻塞狀態(blocked):當線程運行過程中,可能由於各種原因進入阻塞狀態;
a.線程通過調用sleep方法進入睡眠狀態
b.線程調用一個在I/O上被阻塞的操作,即該操作在輸入輸出操作完成之前不會返回到它的調用者
c.線程試圖得到一個鎖,而該鎖正被其他線程持有
d.線程正等待某個觸發條件
死亡狀態(dead):run方法自然退出而自然死亡,或者一個未捕獲的異常終止了run方法而使線程猝死
10、sleep和wait的區別
答:首先,sleep()方法屬於Thread類的,而wait()方法是屬於Object類的;sleep()方法導致了程序暫停執行指定的時間,讓出cpu給其他線程,但是他的監控狀態依然保持,當指定的時間到了又自動回恢復運行狀態,調用了sleep()方法的過程中,線程不會釋放對象鎖;而當調用了wait()方法的時候,線程回放棄對象鎖,進入等待此對象的等待鎖定池,只有針對此對象調用notify()方法後本線程才進入對象鎖定池準備。
11、notify()和notifyAll()的區別
答:notify()方法表示,當前線程已經放棄對資源的佔有,通知等待的線程來獲取對資源的佔有權,但是隻有一個線程能夠從wait狀態中恢復;notifyAll()方法表示,當前的線程已經放棄對資源的佔有,通知所有的等待線程從wait()方法後的語句開始執行,但最終只有一個線程能競爭獲得鎖並執行;notify()是對notifyAll()的一個優化,
12、ThreadLocal的瞭解,實現原理。
答:ThreadLocal,線程本地變量。定義了一個ThreadLocal,每個線程往這個ThreadLocal中讀寫都是線程隔離的,互相之間不會影響,他提供了一種將可變數據通過每個線程有自己的獨立副本從而實現線程封閉的機制;實現的思路,Thread類有一個類型爲ThreadLocal.ThreadLocalMap的實例變量threadLocals,也就是說每個線程都有一個自己的ThreadLocalMap。ThreadLocalMap有自己的獨立實現,可以簡單的將它的key視作ThreadLocal,value爲代碼中放入的值(實際上key並不是ThreadLocal本省,而是它的一個弱引用)。每個線程在往ThreadLocal裏set值的時候,都會往自己的ThreadLocalMap裏存,讀也是已某個ThreadLocal作爲引用,在自己的map裏找對應的key,從而實現了線程的隔離。如果想詳細瞭解,可以參考:ThreadLocal源碼解讀
數據庫相關
1、常見的數據庫優化手段
答:庫表優化,表設計合理化,符合三大範式;添加適當的索引(普通索引、主鍵索引、唯一索引、全文索引);分庫分表;讀寫分離等;sql語句優化,定位執行效率低,慢sql的語句,通過explain分析低效率的原因;
2、索引的優缺點,什麼字段上建立索引
答:優點方面:第一,通過創建唯一索引可以保證數據的唯一性;第二,可以大大加快數據的檢索速度,是主要目的;第三;在使用分組和排序子句進行數據檢索時,可以顯著減少查詢中分組和排序的時間;第四,可以在查詢中使用優化隱藏器,提高系統的性能;
缺點方面:第一,創建索引和維護索引要耗費時間,並且隨着數據量的增加而增加;第二,每一個索引需要佔用額外的物理空間,需要的磁盤開銷更大;第三,當對錶中的數據進行增加、刪除、修改操作時,索引也要動態維護,降低了數據的維護速度;
一般來說,在經常需要搜索的列上,強制該列的唯一性和組織表中數據的排列結構的列,在經常用在鏈接的列上,在經常需要排序的列上,在經常使用在where字句的列上可以添加索引,以提升查詢速度;同樣,對於一些甚少使用或者參考的列,只有很少數值的列(如性別),定義爲text,image,bit的列,修改性能遠遠大於檢索性能的列不適合添加索引;
3、數據庫連接池
答:數據庫連接池(Connection pooling)是程序啓動時建立足夠的數據庫連接,並將這些連接組成一個連接池,由程序動態的對池中的連接進行申請、使用、釋放;
(1)程序初始化時創建連接池
(2)使用時向連接池申請可用連接
(3)使用完畢,將連接返還給連接池
(4)程序退出時,斷開所有的連接,並釋放資源
計算機網絡
1、TCP和UDP的區別
答:TCP(傳輸控制協議),UDP(用戶數據報協議)
(1)TCP面向連接(如打電話先撥號建立連接);UDP是無連接的,即發送數據之前不需要建立連接;
(2)TCP提供可靠的服務。也就是說,通過TCP連接傳送的數據,無差錯,不丟失,不重複,且按序達到;UDP盡最大努力交付,即不保證可靠交付;
(3)TCP面向字節流,實際上是TCP把數據看成一連串無結構的字節流;UDP是面向報文,UDP沒有擁塞控制,因此網絡出現擁塞不會使源主機的發送速率降低(對實時應用很有用,如IP電話,實時視頻會議等)
(4)每一條TCP連接只能是點到點的,UDP支持一對一,一對多,多對一和多對多的交互通信;
(5)TCP首部開銷20字節,UDP首部開銷8字節;
(6)TCP的邏輯通信信道是全雙工的可靠信道,DUP則是不可靠信道;
2、三次握手,四次揮手,爲什麼要四次揮手。
答:三次握手的目的是建立可靠的通信信道,簡單來說就是數據的發送與接收,主要目的是雙方確認自己與對方的發送和接收機能正常;
第一次握手:Client什麼都不能確認,Server確認了對方發送正常;
第二次握手:Clent確認了,自己發送、接收正常,對方發送、接收正常;Server確認了自己接收正常,對方發送正常;
第三次握手:Clent確認了,自己發送、接收正常,對方發送、接收正常;Server確認了自己發送、接收正常,對方發送、接收正常;
所以,經過三次握手之後,就能確認雙方收發功能都正常;
四次揮手:
A:“喂,我不說了 (FIN)。”A->FIN_WAIT1
B:“我知道了(ACK)。等下,上一句還沒說完。Balabala…..(傳輸數據)”B->CLOSE_WAIT | A->FIN_WAIT2
B:”好了,說完了,我也不說了(FIN)。”B->LAST_ACK
A:”我知道了(ACK)。”A->TIME_WAIT | B->CLOSED
A等待2MSL,保證B收到了消息,否則重說一次”我知道了”,A->CLOSED
3、長連接和短連接。
短連接:連接=》傳輸數據=》關閉連接
HTTP是無狀態的,瀏覽器和服務器之間每進行一次http操作,就建立一次連接,但任務結束就中斷連接;也可以理解爲短連接是指socket連接後,發送接收完數據馬上斷開連接;
長連接:連接=》傳輸數據=》保持連接=》傳輸數據=》。。。=》關閉連接
長連接指建立socket連接後不管是否使用都保持連接,但安全性較差;
設計模式
此處推薦閱讀:java23種設計模式深入理解
1、單例模式的幾種寫法
懶漢模式
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static synchronized Singleton getInstance(){
//如果還沒有被實例化過,就實例化一個,然後返回
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
餓漢模式
public class Singleton {
//類加載的時候instance就已經指向了一個實例
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
雙重檢驗鎖
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
靜態內部類:因爲JAVA靜態內部類的特性,加載的時候不會加載內部靜態類,使用的時候纔會加載,而使用的時候類加載又是線程安全的,這就完美達到了效果;
public class Singleton {
private static class SingletonHolder{
private static Singleton instance = new Singleton();
}
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
枚舉:
public enum Singleton {
INSTANCE;
}
2、Spring使用了哪些設計模式
(1)工廠模式,在各種BeanFactory以及ApplicationContext創建中都用到了;
(2)模板模式,也是在各種BeanFactory以及ApplicationContext創建中都用到了;
(3)代理模式,在AOP實現中用到了JDK的動態代理;
(4)單例模式,比如創建bean的時候;
(5)策略模式,第一個地方,加載資源文件的地方,使用了不同的方法,比如:classPathResource,FileSystemResource,ServletContextResource,UrlResource但他們都有共同的接口Resource;第二個地方就是AOP的實現中,採用了不同的方式,JDK動態代理和CGLIB代理;
分佈式相關
1、分佈式事務的控制
可以參考分佈式系統事務一致性解決方案
2、分佈式鎖
答:一般使用zk瞬時有序節點實現的分佈式鎖,或者利用redis的setnx()封裝分佈式鎖;提供思路,具體的可以自行詳細理解;
3、分佈式session如何設計
答:一個比較成熟的方案是通過redis進行session共享。詳細的原理可以參考一種分佈式session實現方案
4、關於dubbo
可以參考博文:Dubbo學習總結(2)——Dubbo架構詳解
5、可以瞭解zk相關知識
緩存相關
1、redis和memcached的區別
(1)redis和memcache都是將數據放入內存中,都是內存數據庫。但是memcache可以緩存圖片、視頻等數據;
(2)redis不僅僅支持簡單的k/v數據,還提供list、set、hash等數據結構的存儲;
(3)虛擬內存--redis當物理內存用完時,可以將一些很久沒有用到的value交換到磁盤;
(4)過期策略--memcache在set時就指定,例如set key1008,即永不過期,redis通過expire設定;
(5)分佈式--設定memcache集羣,利用magent做一主多從;redis可以做一主多從或一主一從;
(6)存儲數據安全--memcache掛掉後,數據沒了,redis可以定期保存到磁盤進行持久化;
(7)災難恢復--memcache掛掉後,數據不可恢復。redis數據丟失後可以通過aof恢復;
(8)redis支持數據備份,即master-slave主備模式;
2、redis是單線程的麼(是的)
3、redis的持久化策略
答:rdb:快照形式是直接把內存中的數據保存到一個dump文件中,定時保存
aof:把所有的對redis的服務器進行修改的命令都存到一個文件裏,命令的集合
框架相關
1、SpringMvc工作原理
(1)用戶發送請求至前端控制器DispatcherServlet
(2)DispatcherServlet收到請求調用HandlerMapping處理映射器
(3)處理器映射器找到具體的處理器(可以根據xml配置、註解進行查找),生成處理器對象及處理器攔截器(如有則生成)一併返回給DispatcherServlet
(4)DispatcherServlet調用HandlerAdapter處理器映射器
(5)HandlerAdapter經過適配調用具體的處理器(Controller,也叫後端控制器)
(6)Controller執行完成返回ModelAndView
(7)HandlerAdapter將Controller執行結果ModelAndView返回給DispatcherServlet
(8)DispatcherServlet將ModelAndView傳給ViewResolver視圖解析器
(9)ViewResolver解析後返回具體的view
(10)DispatcherServlet根據view進行試圖渲染(即將模型數據填充至視圖中)
(11)DispatcherServlet響應用戶
以下組件通常使用框架提供實現:
DispatcherServlet:作爲前端控制器,整個流程控制的中心,控制其它組件執行,統一調度,降低組件之間的耦合性,提高每個組件的擴展性。
HandlerMapping:通過擴展處理器映射器實現不同的映射方式,例如:配置文件方式,實現接口方式,註解方式等。
HandlAdapter:通過擴展處理器適配器,支持更多類型的處理器。
ViewResolver:通過擴展視圖解析器,支持更多類型的視圖解析,例如:jsp、freemarker、pdf、excel等。
2、Quartz概念及原理
org.quartz.Job:它是一個抽象接口,表示一個工作,也是我們要執行的具體的內容,只定義了一個接口方法:void execute(JobExecutionContext context)
org.quartz.JobDetail:JobDetail表示一個具體的可執行的調度程序,Job是這個可執行調度程序所要執行的內容,它包含了這個調度任務的方案和策略
org.quartz.Trigger:Trigger是一個抽象接口,表示一個調度參數的配置,通過配置他,來告訴調度器什麼時候去調用JobDetail
org.quartz.Scheduler:一個調度容器,可以註冊多個Trigger和JobDetail。當Trigger和JobDetail組合,就可以被Scheduler容器調度了
3、Spring的IOC有什麼優勢
答:要了解IOC首先要明白依賴倒置原則(Dependency Inversion Principle),就是把原本的高層建築依賴底層建築倒置過來,變成底層建築依賴高層建築。高層建築決定需要什麼,底層去實現這樣的需求,但是高層並不用管底層的是怎麼實現的;而控制反轉(Inversion of Control)就是依賴倒置原則的一種代碼的設計思路;
IOC思想的核心,資源不由使用資源的雙方管理,由不適用資源的第三方管理。
優勢:資源集中管理,實現資源的可配置和易管理;降低了使用資源雙方的依賴程度,也就是降低了耦合度;
1. ZooKeeper是什麼?
ZooKeeper是一個開放源碼的分佈式協調服務,它是集羣的管理者,監視着集羣中各個節點的狀態根據節點提交的反饋進行下一步合理操作。最終,將簡單易用的接口和性能高效、功能穩定的系統提供給用戶。
分佈式應用程序可以基於Zookeeper實現諸如數據發佈/訂閱、負載均衡、命名服務、分佈式協調/通知、集羣管理、Master選舉、分佈式鎖和分佈式隊列等功能。
Zookeeper保證瞭如下分佈式一致性特性:
·順序一致性
·原子性
·單一視圖
·可靠性
·實時性(最終一致性)
客戶端的讀請求可以被集羣中的任意一臺機器處理,如果讀請求在節點上註冊了監聽器,這個監聽器也是由所連接的zookeeper機器來處理。對於寫請求,這些請求會同時發給其他zookeeper機器並且達成一致後,請求才會返回成功。因此,隨着zookeeper的集羣機器增多,讀請求的吞吐會提高但是寫請求的吞吐會下降。
有序性是zookeeper中非常重要的一個特性,所有的更新都是全局有序的,每個更新都有一個唯一的時間戳,這個時間戳稱爲zxid(Zookeeper Transaction Id)。而讀請求只會相對於更新有序,也就是讀請求的返回結果中會帶有這個zookeeper最新的zxid。
2. ZooKeeper提供了什麼?
1、文件系統
2、通知機制
3. Zookeeper文件系統
Zookeeper提供一個多層級的節點命名空間(節點稱爲znode)。與文件系統不同的是,這些節點都可以設置關聯的數據,而文件系統中只有文件節點可以存放數據而目錄節點不行。
Zookeeper爲了保證高吞吐和低延遲,在內存中維護了這個樹狀的目錄結構,這種特性使得Zookeeper不能用於存放大量的數據,每個節點的存放數據上限爲1M。
4. ZAB協議?
ZAB協議是爲分佈式協調服務Zookeeper專門設計的一種支持崩潰恢復的原子廣播協議。
ZAB協議包括兩種基本的模式:崩潰恢復和消息廣播。
當整個zookeeper集羣剛剛啓動或者Leader服務器宕機、重啓或者網絡故障導致不存在過半的服務器與Leader服務器保持正常通信時,所有進程(服務器)進入崩潰恢復模式,首先選舉產生新的Leader服務器,然後集羣中Follower服務器開始與新的Leader服務器進行數據同步,當集羣中超過半數機器與該Leader服務器完成數據同步之後,退出恢復模式進入消息廣播模式,Leader服務器開始接收客戶端的事務請求生成事物提案來進行事務請求處理。
5. 四種類型的數據節點 Znode
·PERSISTENT-持久節點
除非手動刪除,否則節點一直存在於Zookeeper上
·EPHEMERAL-臨時節點
臨時節點的生命週期與客戶端會話綁定,一旦客戶端會話失效(客戶端與zookeeper連接斷開不一定會話失效),那麼這個客戶端創建的所有臨時節點都會被移除。
·PERSISTENT_SEQUENTIAL-持久順序節點
基本特性同持久節點,只是增加了順序屬性,節點名後邊會追加一個由父節點維護的自增整型數字。
·EPHEMERAL_SEQUENTIAL-臨時順序節點
基本特性同臨時節點,增加了順序屬性,節點名後邊會追加一個由父節點維護的自增整型數字。
6. Zookeeper Watcher 機制 -- 數據變更通知
Zookeeper允許客戶端向服務端的某個Znode註冊一個Watcher監聽,當服務端的一些指定事件觸發了這個Watcher,服務端會向指定客戶端發送一個事件通知來實現分佈式的通知功能,然後客戶端根據Watcher通知狀態和事件類型做出業務上的改變。
工作機制:
·客戶端註冊watcher
·服務端處理watcher
·客戶端回調watcher
Watcher特性總結:
1. 一次性
無論是服務端還是客戶端,一旦一個Watcher被觸發,Zookeeper都會將其從相應的存儲中移除。這樣的設計有效的減輕了服務端的壓力,不然對於更新非常頻繁的節點,服務端會不斷的向客戶端發送事件通知,無論對於網絡還是服務端的壓力都非常大。
2. 客戶端串行執行
客戶端Watcher回調的過程是一個串行同步的過程。
3. 輕量
·Watcher通知非常簡單,只會告訴客戶端發生了事件,而不會說明事件的具體內容。
·客戶端向服務端註冊Watcher的時候,並不會把客戶端真實的Watcher對象實體傳遞到服務端,僅僅是在客戶端請求中使用boolean類型屬性進行了標記。
watcher event異步發送watcher的通知事件從server發送到client是異步的,這就存在一個問題,不同的客戶端和服務器之間通過socket進行通信,由於網絡延遲或其他因素導致客戶端在不通的時刻監聽到事件,由於Zookeeper本身提供了ordering guarantee,即客戶端監聽事件後,纔會感知它所監視znode發生了變化。所以我們使用Zookeeper不能期望能夠監控到節點每次的變化。Zookeeper只能保證最終的一致性,而無法保證強一致性。
註冊watcher getData、exists、getChildren
觸發watcher create、delete、setData
當一個客戶端連接到一個新的服務器上時,watch將會被以任意會話事件觸發。當與一個服務器失去連接的時候,是無法接收到watch的。而當client重新連接時,如果需要的話,所有先前註冊過的watch,都會被重新註冊。通常這是完全透明的。只有在一個特殊情況下,watch可能會丟失:對於一個未創建的znode的exist watch,如果在客戶端斷開連接期間被創建了,並且隨後在客戶端連接上之前又刪除了,這種情況下,這個watch事件可能會被丟失。
7. 客戶端註冊Watcher實現
1. 調用getData()/getChildren()/exist()三個API,傳入Watcher對象
2. 標記請求request,封裝Watcher到WatchRegistration
3. 封裝成Packet對象,發服務端發送request
4. 收到服務端響應後,將Watcher註冊到ZKWatcherManager中進行管理
5. 請求返回,完成註冊。
8. 服務端處理Watcher實現
1. 服務端接收Watcher並存儲
接收到客戶端請求,處理請求判斷是否需要註冊Watcher,需要的話將數據節點的節點路徑和ServerCnxn(ServerCnxn代表一個客戶端和服務端的連接,實現了Watcher的process接口,此時可以看成一個Watcher對象)存儲在WatcherManager的WatchTable和watch2Paths中去。
2. Watcher觸發
以服務端接收到 setData() 事務請求觸發NodeDataChanged事件爲例:
·封裝WatchedEvent
將通知狀態(SyncConnected)、事件類型(NodeDataChanged)以及節點路徑封裝成一個WatchedEvent對象
·查詢Watcher
從WatchTable中根據節點路徑查找Watcher
·沒找到;說明沒有客戶端在該數據節點上註冊過Watcher
·找到;提取並從WatchTable和Watch2Paths中刪除對應Watcher(從這裏可以看出Watcher在服務端是一次性的,觸發一次就失效了)
調用process方法來觸發Watcher
這裏process主要就是通過ServerCnxn對應的TCP連接發送Watcher事件通知。
9. 客戶端回調Watcher
客戶端SendThread線程接收事件通知,交由EventThread線程回調Watcher。客戶端的Watcher機制同樣是一次性的,一旦被觸發後,該Watcher就失效了。
10. ACL權限控制機制
UGO(User/Group/Others)
目前在Linux/Unix文件系統中使用,也是使用最廣泛的權限控制方式。是一種粗粒度的文件系統權限控制模式。
ACL(Access Control List)訪問控制列表
包括三個方面:
·權限模式(Scheme)
o IP:從IP地址粒度進行權限控制
o Digest:最常用,用類似於 username:password 的權限標識來進行權限配置,便於區分不同應用來進行權限控制
o World:最開放的權限控制方式,是一種特殊的digest模式,只有一個權限標識“world:anyone”
o Super:超級用戶
·授權對象
授權對象指的是權限賦予的用戶或一個指定實體,例如IP地址或是機器燈。
·權限 Permission
o CREATE:數據節點創建權限,允許授權對象在該Znode下創建子節點
o DELETE:子節點刪除權限,允許授權對象刪除該數據節點的子節點
o READ:數據節點的讀取權限,允許授權對象訪問該數據節點並讀取其數據內容或子節點列表等
o WRITE:數據節點更新權限,允許授權對象對該數據節點進行更新操作
o ADMIN:數據節點管理權限,允許授權對象對該數據節點進行ACL相關設置操作
11. Chroot特性
3.2.0版本後,添加了 Chroot特性,該特性允許每個客戶端爲自己設置一個命名空間。如果一個客戶端設置了Chroot,那麼該客戶端對服務器的任何操作,都將會被限制在其自己的命名空間下。
通過設置Chroot,能夠將一個客戶端應用於Zookeeper服務端的一顆子樹相對應,在那些多個應用公用一個Zookeeper進羣的場景下,對實現不同應用間的相互隔離非常有幫助。
12. 會話管理
分桶策略:將類似的會話放在同一區塊中進行管理,以便於Zookeeper對會話進行不同區塊的隔離處理以及同一區塊的統一處理。
分配原則:每個會話的“下次超時時間點”(ExpirationTime)
計算公式:
ExpirationTime_ = currentTime + sessionTimeout
ExpirationTime = (ExpirationTime_ / ExpirationInrerval +
1) * ExpirationInterval , ExpirationInterval
是指 Zookeeper 會話超時檢查時間間隔,默認 tickTime
13. 服務器角色
Leader
·事務請求的唯一調度和處理者,保證集羣事務處理的順序性
·集羣內部各服務的調度者
Follower
·處理客戶端的非事務請求,轉發事務請求給Leader服務器
·參與事務請求Proposal的投票
·參與Leader選舉投票
Observer
3.3.0版本以後引入的一個服務器角色,在不影響集羣事務處理能力的基礎上提升集羣的非事務處理能力
·處理客戶端的非事務請求,轉發事務請求給Leader服務器
·不參與任何形式的投票
14. Zookeeper 下 Server工作狀態
服務器具有四種狀態,分別是LOOKING、FOLLOWING、LEADING、OBSERVING。
·LOOKING:尋找Leader狀態。當服務器處於該狀態時,它會認爲當前集羣中沒有Leader,因此需要進入Leader選舉狀態。
·FOLLOWING:跟隨者狀態。表明當前服務器角色是Follower。
·LEADING:領導者狀態。表明當前服務器角色是Leader。
·OBSERVING:觀察者狀態。表明當前服務器角色是Observer。
15. Leader 選舉
Leader選舉是保證分佈式數據一致性的關鍵所在。當Zookeeper集羣中的一臺服務器出現以下兩種情況之一時,需要進入Leader選舉。
(1) 服務器初始化啓動。
(2) 服務器運行期間無法和Leader保持連接。
下面就兩種情況進行分析講解。
1. 服務器啓動時期的Leader選舉
若進行Leader選舉,則至少需要兩臺機器,這裏選取3臺機器組成的服務器集羣爲例。在集羣初始化階段,當有一臺服務器Server1啓動時,其單獨無法進行和完成Leader選舉,當第二臺服務器Server2啓動時,此時兩臺機器可以相互通信,每臺機器都試圖找到Leader,於是進入Leader選舉過程。選舉過程如下
(1) 每個Server發出一個投票。由於是初始情況,Server1和Server2都會將自己作爲Leader服務器來進行投票,每次投票會包含所推舉的服務器的myid和ZXID,使用(myid, ZXID)來表示,此時Server1的投票爲(1, 0),Server2的投票爲(2, 0),然後各自將這個投票發給集羣中其他機器。
(2) 接受來自各個服務器的投票。集羣的每個服務器收到投票後,首先判斷該投票的有效性,如檢查是否是本輪投票、是否來自LOOKING狀態的服務器。
(3) 處理投票。針對每一個投票,服務器都需要將別人的投票和自己的投票進行PK,PK規則如下
· 優先檢查ZXID。ZXID比較大的服務器優先作爲Leader。
· 如果ZXID相同,那麼就比較myid。myid較大的服務器作爲Leader服務器。
對於Server1而言,它的投票是(1, 0),接收Server2的投票爲(2, 0),首先會比較兩者的ZXID,均爲0,再比較myid,此時Server2的myid最大,於是更新自己的投票爲(2, 0),然後重新投票,對於Server2而言,其無須更新自己的投票,只是再次向集羣中所有機器發出上一次投票信息即可。
(4) 統計投票。每次投票後,服務器都會統計投票信息,判斷是否已經有過半機器接受到相同的投票信息,對於Server1、Server2而言,都統計出集羣中已經有兩臺機器接受了(2, 0)的投票信息,此時便認爲已經選出了Leader。
(5) 改變服務器狀態。一旦確定了Leader,每個服務器就會更新自己的狀態,如果是Follower,那麼就變更爲FOLLOWING,如果是Leader,就變更爲LEADING。
2. 服務器運行時期的Leader選舉
在Zookeeper運行期間,Leader與非Leader服務器各司其職,即便當有非Leader服務器宕機或新加入,此時也不會影響Leader,但是一旦Leader服務器掛了,那麼整個集羣將暫停對外服務,進入新一輪Leader選舉,其過程和啓動時期的Leader選舉過程基本一致。假設正在運行的有Server1、Server2、Server3三臺服務器,當前Leader是Server2,若某一時刻Leader掛了,此時便開始Leader選舉。選舉過程如下
(1) 變更狀態。Leader掛後,餘下的非Observer服務器都會講自己的服務器狀態變更爲LOOKING,然後開始進入Leader選舉過程。
(2) 每個Server會發出一個投票。在運行期間,每個服務器上的ZXID可能不同,此時假定Server1的ZXID爲123,Server3的ZXID爲122;在第一輪投票中,Server1和Server3都會投自己,產生投票(1, 123),(3, 122),然後各自將投票發送給集羣中所有機器。
(3) 接收來自各個服務器的投票。與啓動時過程相同。
(4) 處理投票。與啓動時過程相同,此時,Server1將會成爲Leader。
(5) 統計投票。與啓動時過程相同。
(6) 改變服務器的狀態。與啓動時過程相同。
2.2 Leader選舉算法分析
在3.4.0後的Zookeeper的版本只保留了TCP版本的FastLeaderElection選舉算法。當一臺機器進入Leader選舉時,當前集羣可能會處於以下兩種狀態
· 集羣中已經存在Leader。
· 集羣中不存在Leader。
對於集羣中已經存在Leader而言,此種情況一般都是某臺機器啓動得較晚,在其啓動之前,集羣已經在正常工作,對這種情況,該機器試圖去選舉Leader時,會被告知當前服務器的Leader信息,對於該機器而言,僅僅需要和Leader機器建立起連接,並進行狀態同步即可。而在集羣中不存在Leader情況下則會相對複雜,其步驟如下
(1) 第一次投票。無論哪種導致進行Leader選舉,集羣的所有機器都處於試圖選舉出一個Leader的狀態,即LOOKING狀態,LOOKING機器會向所有其他機器發送消息,該消息稱爲投票。投票中包含了SID(服務器的唯一標識)和ZXID(事務ID),(SID, ZXID)形式來標識一次投票信息。假定Zookeeper由5臺機器組成,SID分別爲1、2、3、4、5,ZXID分別爲9、9、9、8、8,並且此時SID爲2的機器是Leader機器,某一時刻,1、2所在機器出現故障,因此集羣開始進行Leader選舉。在第一次投票時,每臺機器都會將自己作爲投票對象,於是SID爲3、4、5的機器投票情況分別爲(3, 9),(4, 8), (5, 8)。
(2) 變更投票。每臺機器發出投票後,也會收到其他機器的投票,每臺機器會根據一定規則來處理收到的其他機器的投票,並以此來決定是否需要變更自己的投票,這個規則也是整個Leader選舉算法的核心所在,其中術語描述如下
· vote_sid:接收到的投票中所推舉Leader服務器的SID。
· vote_zxid:接收到的投票中所推舉Leader服務器的ZXID。
· self_sid:當前服務器自己的SID。
· self_zxid:當前服務器自己的ZXID。
每次對收到的投票的處理,都是對(vote_sid, vote_zxid)和(self_sid, self_zxid)對比的過程。
規則一:如果vote_zxid大於self_zxid,就認可當前收到的投票,並再次將該投票發送出去。
規則二:如果vote_zxid小於self_zxid,那麼堅持自己的投票,不做任何變更。
規則三:如果vote_zxid等於self_zxid,那麼就對比兩者的SID,如果vote_sid大於self_sid,那麼就認可當前收到的投票,並再次將該投票發送出去。
規則四:如果vote_zxid等於self_zxid,並且vote_sid小於self_sid,那麼堅持自己的投票,不做任何變更。
結合上面規則,給出下面的集羣變更過程。
(3) 確定Leader。經過第二輪投票後,集羣中的每臺機器都會再次接收到其他機器的投票,然後開始統計投票,如果一臺機器收到了超過半數的相同投票,那麼這個投票對應的SID機器即爲Leader。此時Server3將成爲Leader。
由上面規則可知,通常那臺服務器上的數據越新(ZXID會越大),其成爲Leader的可能性越大,也就越能夠保證數據的恢復。如果ZXID相同,則SID越大機會越大。
2.3 Leader選舉實現細節
1. 服務器狀態
服務器具有四種狀態,分別是LOOKING、FOLLOWING、LEADING、OBSERVING。
LOOKING:尋找Leader狀態。當服務器處於該狀態時,它會認爲當前集羣中沒有Leader,因此需要進入Leader選舉狀態。
FOLLOWING:跟隨者狀態。表明當前服務器角色是Follower。
LEADING:領導者狀態。表明當前服務器角色是Leader。
OBSERVING:觀察者狀態。表明當前服務器角色是Observer。
2. 投票數據結構
每個投票中包含了兩個最基本的信息,所推舉服務器的SID和ZXID,投票(Vote)在Zookeeper中包含字段如下
id:被推舉的Leader的SID。
zxid:被推舉的Leader事務ID。
electionEpoch:邏輯時鐘,用來判斷多個投票是否在同一輪選舉週期中,該值在服務端是一個自增序列,每次進入新一輪的投票後,都會對該值進行加1操作。
peerEpoch:被推舉的Leader的epoch。
state:當前服務器的狀態。
3. QuorumCnxManager:網絡I/O
每臺服務器在啓動的過程中,會啓動一個QuorumPeerManager,負責各臺服務器之間的底層Leader選舉過程中的網絡通信。
(1) 消息隊列。QuorumCnxManager內部維護了一系列的隊列,用來保存接收到的、待發送的消息以及消息的發送器,除接收隊列以外,其他隊列都按照SID分組形成隊列集合,如一個集羣中除了自身還有3臺機器,那麼就會爲這3臺機器分別創建一個發送隊列,互不干擾。
· recvQueue:消息接收隊列,用於存放那些從其他服務器接收到的消息。
· queueSendMap:消息發送隊列,用於保存那些待發送的消息,按照SID進行分組。
· senderWorkerMap:發送器集合,每個SenderWorker消息發送器,都對應一臺遠程Zookeeper服務器,負責消息的發送,也按照SID進行分組。
· lastMessageSent:最近發送過的消息,爲每個SID保留最近發送過的一個消息。
(2) 建立連接。爲了能夠相互投票,Zookeeper集羣中的所有機器都需要兩兩建立起網絡連接。QuorumCnxManager在啓動時會創建一個ServerSocket來監聽Leader選舉的通信端口(默認爲3888)。開啓監聽後,Zookeeper能夠不斷地接收到來自其他服務器的創建連接請求,在接收到其他服務器的TCP連接請求時,會進行處理。爲了避免兩臺機器之間重複地創建TCP連接,Zookeeper只允許SID大的服務器主動和其他機器建立連接,否則斷開連接。在接收到創建連接請求後,服務器通過對比自己和遠程服務器的SID值來判斷是否接收連接請求,如果當前服務器發現自己的SID更大,那麼會斷開當前連接,然後自己主動和遠程服務器建立連接。一旦連接建立,就會根據遠程服務器的SID來創建相應的消息發送器SendWorker和消息接收器RecvWorker,並啓動。
(3) 消息接收與發送。消息接收:由消息接收器RecvWorker負責,由於Zookeeper爲每個遠程服務器都分配一個單獨的RecvWorker,因此,每個RecvWorker只需要不斷地從這個TCP連接中讀取消息,並將其保存到recvQueue隊列中。消息發送:由於Zookeeper爲每個遠程服務器都分配一個單獨的SendWorker,因此,每個SendWorker只需要不斷地從對應的消息發送隊列中獲取出一個消息發送即可,同時將這個消息放入lastMessageSent中。在SendWorker中,一旦Zookeeper發現針對當前服務器的消息發送隊列爲空,那麼此時需要從lastMessageSent中取出一個最近發送過的消息來進行再次發送,這是爲了解決接收方在消息接收前或者接收到消息後服務器掛了,導致消息尚未被正確處理。同時,Zookeeper能夠保證接收方在處理消息時,會對重複消息進行正確的處理。
4. FastLeaderElection:選舉算法核心
· 外部投票:特指其他服務器發來的投票。
· 內部投票:服務器自身當前的投票。
· 選舉輪次:Zookeeper服務器Leader選舉的輪次,即logicalclock。
· PK:對內部投票和外部投票進行對比來確定是否需要變更內部投票。
(1) 選票管理
· sendqueue:選票發送隊列,用於保存待發送的選票。
· recvqueue:選票接收隊列,用於保存接收到的外部投票。
· WorkerReceiver:選票接收器。其會不斷地從QuorumCnxManager中獲取其他服務器發來的選舉消息,並將其轉換成一個選票,然後保存到recvqueue中,在選票接收過程中,如果發現該外部選票的選舉輪次小於當前服務器的,那麼忽略該外部投票,同時立即發送自己的內部投票。
· WorkerSender:選票發送器,不斷地從sendqueue中獲取待發送的選票,並將其傳遞到底層QuorumCnxManager中。
(2) 算法核心
上圖展示了FastLeaderElection模塊是如何與底層網絡I/O進行交互的。Leader選舉的基本流程如下
1. 自增選舉輪次。Zookeeper規定所有有效的投票都必須在同一輪次中,在開始新一輪投票時,會首先對logicalclock進行自增操作。
2. 初始化選票。在開始進行新一輪投票之前,每個服務器都會初始化自身的選票,並且在初始化階段,每臺服務器都會將自己推舉爲Leader。
3. 發送初始化選票。完成選票的初始化後,服務器就會發起第一次投票。Zookeeper會將剛剛初始化好的選票放入sendqueue中,由發送器WorkerSender負責發送出去。
4. 接收外部投票。每臺服務器會不斷地從recvqueue隊列中獲取外部選票。如果服務器發現無法獲取到任何外部投票,那麼就會立即確認自己是否和集羣中其他服務器保持着有效的連接,如果沒有連接,則馬上建立連接,如果已經建立了連接,則再次發送自己當前的內部投票。
5. 判斷選舉輪次。在發送完初始化選票之後,接着開始處理外部投票。在處理外部投票時,會根據選舉輪次來進行不同的處理。
· 外部投票的選舉輪次大於內部投票。若服務器自身的選舉輪次落後於該外部投票對應服務器的選舉輪次,那麼就會立即更新自己的選舉輪次(logicalclock),並且清空所有已經收到的投票,然後使用初始化的投票來進行PK以確定是否變更內部投票。最終再將內部投票發送出去。
· 外部投票的選舉輪次小於內部投票。若服務器接收的外選票的選舉輪次落後於自身的選舉輪次,那麼Zookeeper就會直接忽略該外部投票,不做任何處理,並返回步驟4。
· 外部投票的選舉輪次等於內部投票。此時可以開始進行選票PK。
6. 選票PK。在進行選票PK時,符合任意一個條件就需要變更投票。
· 若外部投票中推舉的Leader服務器的選舉輪次大於內部投票,那麼需要變更投票。
· 若選舉輪次一致,那麼就對比兩者的ZXID,若外部投票的ZXID大,那麼需要變更投票。
· 若兩者的ZXID一致,那麼就對比兩者的SID,若外部投票的SID大,那麼就需要變更投票。
7. 變更投票。經過PK後,若確定了外部投票優於內部投票,那麼就變更投票,即使用外部投票的選票信息來覆蓋內部投票,變更完成後,再次將這個變更後的內部投票發送出去。
8. 選票歸檔。無論是否變更了投票,都會將剛剛收到的那份外部投票放入選票集合recvset中進行歸檔。recvset用於記錄當前服務器在本輪次的Leader選舉中收到的所有外部投票(按照服務隊的SID區別,如{(1, vote1), (2, vote2)...})。
9. 統計投票。完成選票歸檔後,就可以開始統計投票,統計投票是爲了統計集羣中是否已經有過半的服務器認可了當前的內部投票,如果確定已經有過半服務器認可了該投票,則終止投票。否則返回步驟4。
10. 更新服務器狀態。若已經確定可以終止投票,那麼就開始更新服務器狀態,服務器首選判斷當前被過半服務器認可的投票所對應的Leader服務器是否是自己,若是自己,則將自己的服務器狀態更新爲LEADING,若不是,則根據具體情況來確定自己是FOLLOWING或是OBSERVING。
以上10個步驟就是FastLeaderElection的核心,其中步驟4-9會經過幾輪循環,直到有Leader選舉產生。
16. 數據同步
整個集羣完成Leader選舉之後,Learner(Follower和Observer的統稱)迴向Leader服務器進行註冊。當Learner服務器想Leader服務器完成註冊後,進入數據同步環節。
數據同步流程:(均以消息傳遞的方式進行)
i. Learner向Learder註冊
ii. 數據同步
iii. 同步確認
Zookeeper的數據同步通常分爲四類:
·直接差異化同步(DIFF同步)
·先回滾再差異化同步(TRUNC+DIFF同步)
·僅回滾同步(TRUNC同步)
·全量同步(SNAP同步)
在進行數據同步前,Leader服務器會完成數據同步初始化:
·peerLastZxid:從learner服務器註冊時發送的ACKEPOCH消息中提取lastZxid(該Learner服務器最後處理的ZXID)
·minCommittedLog:Leader服務器Proposal緩存隊列committedLog中最小ZXID
·maxCommittedLog:Leader服務器Proposal緩存隊列committedLog中最大ZXID
直接差異化同步(DIFF同步)
場景:peerLastZxid介於minCommittedLog和maxCommittedLog之間
先回滾再差異化同步(TRUNC+DIFF同步)
場景:當新的Leader服務器發現某個Learner服務器包含了一條自己沒有的事務記錄,那麼就需要讓該Learner服務器進行事務回滾--回滾到Leader服務器上存在的,同時也是最接近於peerLastZxid的ZXID
僅回滾同步(TRUNC同步)
場景:peerLastZxid 大於 maxCommittedLog
全量同步(SNAP同步)
場景一:peerLastZxid 小於 minCommittedLog
場景二:Leader服務器上沒有Proposal緩存隊列且peerLastZxid不等於lastProcessZxid
17. zookeeper是如何保證事務的順序一致性的?
zookeeper採用了全局遞增的事務Id來標識,所有的proposal(提議)都在被提出的時候加上了zxid,zxid實際上是一個64位的數字,高32位是epoch(時期; 紀元; 世; 新時代)用來標識leader週期,如果有新的leader產生出來,epoch會自增,低32位用來遞增計數。當新產生proposal的時候,會依據數據庫的兩階段過程,首先會向其他的server發出事務執行請求,如果超過半數的機器都能執行並且能夠成功,那麼就會開始執行。
18. 分佈式集羣中爲什麼會有Master?
在分佈式環境中,有些業務邏輯只需要集羣中的某一臺機器進行執行,其他的機器可以共享這個結果,這樣可以大大減少重複計算,提高性能,於是就需要進行leader選舉。
19. zk節點宕機如何處理?
Zookeeper本身也是集羣,推薦配置不少於3個服務器。Zookeeper自身也要保證當一個節點宕機時,其他節點會繼續提供服務。
如果是一個Follower宕機,還有2臺服務器提供訪問,因爲Zookeeper上的數據是有多個副本的,數據並不會丟失;
如果是一個Leader宕機,Zookeeper會選舉出新的Leader。
ZK集羣的機制是隻要超過半數的節點正常,集羣就能正常提供服務。只有在ZK節點掛得太多,只剩一半或不到一半節點能工作,集羣才失效。
所以
3個節點的cluster可以掛掉1個節點(leader可以得到2票>1.5)
2個節點的cluster就不能掛掉任何1個節點了(leader可以得到1票<=1)
20. zookeeper負載均衡和nginx負載均衡區別
zk的負載均衡是可以調控,nginx只是能調權重,其他需要可控的都需要自己寫插件;但是nginx的吞吐量比zk大很多,應該說按業務選擇用哪種方式。
21. Zookeeper有哪幾種幾種部署模式?
部署模式:單機模式、僞集羣模式、集羣模式。
22. 集羣最少要幾臺機器,集羣規則是怎樣的?
集羣規則爲2N+1臺,N>0,即3臺。
23. 集羣支持動態添加機器嗎?
其實就是水平擴容了,Zookeeper在這方面不太好。兩種方式:
·全部重啓:關閉所有Zookeeper服務,修改配置之後啓動。不影響之前客戶端的會話。
·逐個重啓:在過半存活即可用的原則下,一臺機器重啓不影響整個集羣對外提供服務。這是比較常用的方式。
3.5版本開始支持動態擴容。
24. Zookeeper對節點的watch監聽通知是永久的嗎?爲什麼不是永久的?
不是。官方聲明:一個Watch事件是一個一次性的觸發器,當被設置了Watch的數據發生了改變的時候,則服務器將這個改變發送給設置了Watch的客戶端,以便通知它們。
爲什麼不是永久的,舉個例子,如果服務端變動頻繁,而監聽的客戶端很多情況下,每次變動都要通知到所有的客戶端,給網絡和服務器造成很大壓力。
一般是客戶端執行getData(“/節點A”,true),如果節點A發生了變更或刪除,客戶端會得到它的watch事件,但是在之後節點A又發生了變更,而客戶端又沒有設置watch事件,就不再給客戶端發送。
在實際應用中,很多情況下,我們的客戶端不需要知道服務端的每一次變動,我只要最新的數據即可。
25. Zookeeper的java客戶端都有哪些?
java客戶端:zk自帶的zkclient及Apache開源的Curator。
26. chubby是什麼,和zookeeper比你怎麼看?
chubby是google的,完全實現paxos算法,不開源。zookeeper是chubby的開源實現,使用zab協議,paxos算法的變種。
27. 說幾個zookeeper常用的命令。
常用命令:ls get set create delete等。
28. ZAB和Paxos算法的聯繫與區別?
·相同點:
o 兩者都存在一個類似於Leader進程的角色,由其負責協調多個Follower進程的運行
o Leader進程都會等待超過半數的Follower做出正確的反饋後,纔會將一個提案進行提交
o ZAB協議中,每個Proposal中都包含一個 epoch 值來代表當前的Leader週期,Paxos中名字爲Ballot
·不同點:
ZAB用來構建高可用的分佈式數據主備系統(Zookeeper),Paxos是用來構建分佈式一致性狀態機系統。
29. Zookeeper的典型應用場景
Zookeeper是一個典型的發佈/訂閱模式的分佈式數據管理與協調框架,開發人員可以使用它來進行分佈式數據的發佈和訂閱。
通過對Zookeeper中豐富的數據節點進行交叉使用,配合Watcher事件通知機制,可以非常方便的構建一系列分佈式應用中年都會涉及的核心功能,如:
·數據發佈/訂閱
·負載均衡
·命名服務
·分佈式協調/通知
·集羣管理
·Master選舉
·分佈式鎖
·分佈式隊列
1. 數據發佈/訂閱
介紹
數據發佈/訂閱系統,即所謂的配置中心,顧名思義就是發佈者發佈數據供訂閱者進行數據訂閱。
目的
·動態獲取數據(配置信息)
·實現數據(配置信息)的集中式管理和數據的動態更新
設計模式
·Push 模式
·Pull 模式
數據(配置信息)特性:
·數據量通常比較小
·數據內容在運行時會發生動態更新
·集羣中各機器共享,配置一致
如:機器列表信息、運行時開關配置、數據庫配置信息等
基於Zookeeper的實現方式
1. 數據存儲:將數據(配置信息)存儲到Zookeeper上的一個數據節點
2. 數據獲取:應用在啓動初始化節點從Zookeeper數據節點讀取數據,並在該節點上註冊一個數據變更Watcher
3. 數據變更:當變更數據時,更新Zookeeper對應節點數據,Zookeeper會將數據變更通知發到各客戶端,客戶端接到通知後重新讀取變更後的數據即可。
2. 負載均衡
zk的命名服務
命名服務是指通過指定的名字來獲取資源或者服務的地址,利用zk創建一個全局的路徑,這個路徑就可以作爲一個名字,指向集羣中的集羣,提供的服務的地址,或者一個遠程的對象等等。
分佈式通知和協調
對於系統調度來說:操作人員發送通知實際是通過控制檯改變某個節點的狀態,然後zk將這些變化發送給註冊了這個節點的watcher的所有客戶端。
對於執行情況彙報:每個工作進程都在某個目錄下創建一個臨時節點。並攜帶工作的進度數據,這樣彙總的進程可以監控目錄子節點的變化獲得工作進度的實時的全局情況。
7.zk的命名服務(文件系統)
命名服務是指通過指定的名字來獲取資源或者服務的地址,利用zk創建一個全局的路徑,即是唯一的路徑,這個路徑就可以作爲一個名字,指向集羣中的集羣,提供的服務的地址,或者一個遠程的對象等等。
8.zk的配置管理(文件系統、通知機制)
程序分佈式的部署在不同的機器上,將程序的配置信息放在zk的znode下,當有配置發生改變時,也就是znode發生變化時,可以通過改變zk中某個目錄節點的內容,利用watcher通知給各個客戶端,從而更改配置。
9.Zookeeper集羣管理(文件系統、通知機制)
所謂集羣管理無在乎兩點:是否有機器退出和加入、選舉master。
對於第一點,所有機器約定在父目錄下創建臨時目錄節點,然後監聽父目錄節點的子節點變化消息。一旦有機器掛掉,該機器與 zookeeper的連接斷開,其所創建的臨時目錄節點被刪除,所有其他機器都收到通知:某個兄弟目錄被刪除,於是,所有人都知道:它上船了。
新機器加入也是類似,所有機器收到通知:新兄弟目錄加入,highcount又有了,對於第二點,我們稍微改變一下,所有機器創建臨時順序編號目錄節點,每次選取編號最小的機器作爲master就好。
10.Zookeeper分佈式鎖(文件系統、通知機制)
有了zookeeper的一致性文件系統,鎖的問題變得容易。鎖服務可以分爲兩類,一個是保持獨佔,另一個是控制時序。
對於第一類,我們將zookeeper上的一個znode看作是一把鎖,通過createznode的方式來實現。所有客戶端都去創建 /distribute_lock 節點,最終成功創建的那個客戶端也即擁有了這把鎖。用完刪除掉自己創建的distribute_lock 節點就釋放出鎖。
對於第二類, /distribute_lock 已經預先存在,所有客戶端在它下面創建臨時順序編號目錄節點,和選master一樣,編號最小的獲得鎖,用完刪除,依次方便。
11.獲取分佈式鎖的流程
clipboard.png
在獲取分佈式鎖的時候在locker節點下創建臨時順序節點,釋放鎖的時候刪除該臨時節點。客戶端調用createNode方法在locker下創建臨時順序節點,
然後調用getChildren(“locker”)來獲取locker下面的所有子節點,注意此時不用設置任何Watcher。客戶端獲取到所有的子節點path之後,如果發現自己創建的節點在所有創建的子節點序號最小,那麼就認爲該客戶端獲取到了鎖。如果發現自己創建的節點並非locker所有子節點中最小的,說明自己還沒有獲取到鎖,此時客戶端需要找到比自己小的那個節點,然後對其調用exist()方法,同時對其註冊事件監聽器。之後,讓這個被關注的節點刪除,則客戶端的Watcher會收到相應通知,此時再次判斷自己創建的節點是否是locker子節點中序號最小的,如果是則獲取到了鎖,如果不是則重複以上步驟繼續獲取到比自己小的一個節點並註冊監聽。當前這個過程中還需要許多的邏輯判斷。
clipboard.png
代碼的實現主要是基於互斥鎖,獲取分佈式鎖的重點邏輯在於BaseDistributedLock,實現了基於Zookeeper實現分佈式鎖的細節。
12.Zookeeper隊列管理(文件系統、通知機制)
兩種類型的隊列:
1、同步隊列,當一個隊列的成員都聚齊時,這個隊列纔可用,否則一直等待所有成員到達。
2、隊列按照 FIFO 方式進行入隊和出隊操作。
第一類,在約定目錄下創建臨時目錄節點,監聽節點數目是否是我們要求的數目。
第二類,和分佈式鎖服務中的控制時序場景基本原理一致,入列有編號,出列按編號。在特定的目錄下創建PERSISTENT_SEQUENTIAL節點,創建成功時Watcher通知等待的隊列,隊列刪除序列號最小的節點用以消費。此場景下Zookeeper的znode用於消息存儲,znode存儲的數據就是消息隊列中的消息內容,SEQUENTIAL序列號就是消息的編號,按序取出即可。由於創建的節點是持久化的,所以不必擔心隊列消息的丟失問題。