memcached原始碼分析-----雜湊表基本操作以及擴容過程
轉載請註明出處:http://blog.csdn.net/luotuo44/article/details/42773231
溫馨提示:本文用到了一些可以在啟動memcached設定的全域性變數。關於這些全域性變數的含義可以參考《memcached啟動引數詳解》。對於這些全域性變數,處理方式就像《如何閱讀memcached原始碼》所說的那樣直接取其預設值。
assoc.c檔案裡面的程式碼是構造一個雜湊表。memcached快的一個原因是使用了雜湊表。現在就來看一下memcached是怎麼使用雜湊表的。
雜湊結構:
main函式會呼叫assoc_init函式申請並初始化雜湊表。為了減少雜湊表發生衝突的可能性,memcached的雜湊表是比較長的,並且雜湊表的長度為2的冪。全域性變數hashpower用來記錄2的冪次。main函式呼叫assoc_init函式時使用全域性變數settings.hashpower_init作為引數,用於指明雜湊表初始化時的冪次。settings.hashpower_init可以在啟動memcached的時候設定,具體可以參考《memcached啟動引數詳解以及關鍵配置的預設值》。
//memcached.h檔案
#define HASHPOWER_DEFAULT 16
//assoc.h檔案
unsigned int hashpower = HASHPOWER_DEFAULT;
#define hashsize(n) ((ub4)1<<(n))//這裡是1 左移 n次
//hashsize(n)為2的冪,所以hashmask的值的二進位制形式就是後面全為1的數。這就很像位操作裡面的 &
//value & hashmask(n)的結果肯定是比hashsize(n)小的一個數字.即結果在hash表裡面
//hashmask(n)也可以稱為雜湊掩碼
#define hashmask(n) (hashsize(n)-1)
//雜湊表陣列指標
static item** primary_hashtable = 0;
//預設引數值為0。本函式由main函式呼叫,引數的預設值為0
void assoc_init(const int hashtable_init) {
if (hashtable_init) {
hashpower = hashtable_init;
}
//因為雜湊表會慢慢增大,所以要使用動態記憶體分配。雜湊表儲存的資料是一個
//指標,這樣更省空間。
//hashsize(hashpower)就是雜湊表的長度了
primary_hashtable = calloc(hashsize(hashpower), sizeof(void *));
if (! primary_hashtable) {
fprintf(stderr, "Failed to init hashtable.\n");
exit(EXIT_FAILURE);//雜湊表是memcached工作的基礎,如果失敗只能退出執行
}
}
說到雜湊表,那麼就對應有兩個問題:雜湊演算法,怎麼解決衝突。
對於雜湊函式(演算法),memcached直接使用開源的MurmurHash3和jenkins_hash兩個中的一個。預設是使用jenkins,可以在啟動memcached的時候設定設定為MurmurHash3。memcached是直接把客戶端輸入的鍵值作為雜湊演算法的輸入,得到一個32位的無符號整型輸出(用變數hv儲存)。因為雜湊表的長度沒有2^32- 1這麼大,所以需要一個函式將hv對映在雜湊表的範圍之內。memcached採用了最簡單的取模運算作為對映函式,即hv%hashsize(hashpower)。對於CPU而言,取模運算是一個比較耗時的操作。所以memcached利用雜湊表的長度是2的冪的性質,採用位操作進行優化,即: hv & hashmask(hashpower)。因為對雜湊表進行增刪查操作都需要定位,所以經常本文的程式碼中經常會出現hv & hashmask(hashpower)。
memcached使用最常見的鏈地址法解決衝突問題。從前面的程式碼可以看到,primary_hashtable是一個的二級指標變數,它指向的是一個一維指標陣列,陣列的每一個元素指向一條連結串列(連結串列上的item節點具有相同的雜湊值)。陣列的每一個元素,在memcached裡面也稱為桶(bucket),所以後文的表述中會使用桶。下圖是一個雜湊表,其中第0號桶有2個item,第2、3、5號桶各有一個item。item就是用來儲存使用者資料的結構體。
基本操作:
插入item:
接著看一下怎麼在雜湊表中插入一個item。它是直接根據雜湊值找到雜湊表中的位置(即找到對應的桶),然後使用頭插法插入到桶的衝突鏈中。item結構體有一個專門的h_next指標成員變數用於連線雜湊衝突鏈。
static unsigned int hash_items = 0;//hash表中item的個數
/* Note: this isn't an assoc_update. The key must not already exist to call this */
//hv是這個item鍵值的雜湊值
int assoc_insert(item *it, const uint32_t hv) {
unsigned int oldbucket;
//使用頭插法 插入一個item
//第一次看本函式,直接看else部分
if (expanding &&
(oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
{
...
} else {
//使用頭插法插入雜湊表中
it->h_next = primary_hashtable[hv & hashmask(hashpower)];
primary_hashtable[hv & hashmask(hashpower)] = it;
}
hash_items++;//雜湊表的item數量加一
…
return 1;
}
查詢item:
往雜湊表插入item後,就可以開始查詢item了。下面看一下怎麼在雜湊表中查詢一個item。item的鍵值hv只能定位到雜湊表中的桶位置,但一個桶的衝突鏈上可能有多個item,所以除了查詢的時候除了需要hv外還需要item的鍵值。
//由於雜湊值只能確定是在雜湊表中的哪個桶(bucket),但一個桶裡面是有一條衝突鏈的
//此時需要用到具體的鍵值遍歷並一一比較衝突鏈上的所有節點。雖然key是以'\0'結尾
//的字串,但呼叫strlen還是有點耗時(需要遍歷鍵值字串)。所以需要另外一個引數
//nkey指明這個key的長度
item *assoc_find(const char *key, const size_t nkey, const uint32_t hv) {
item *it;
unsigned int oldbucket;
//直接看else部分
if (expanding &&
(oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
{
it = old_hashtable[oldbucket];
} else {
//由雜湊值判斷這個key是屬於那個桶(bucket)的
it = primary_hashtable[hv & hashmask(hashpower)];
}
//到這裡,已經確定這個key是屬於那個桶的。 遍歷對應桶的衝突鏈即可
item *ret = NULL;
while (it) {
//長度相同的情況下才呼叫memcmp比較,更高效
if ((nkey == it->nkey) && (memcmp(key, ITEM_key(it), nkey) == 0)) {
ret = it;
break;
}
it = it->h_next;
}
return ret;
}
刪除item:
下面看一下從雜湊表中刪除一個item是怎麼實現的。從連結串列中刪除一個節點的常規做法是:先找到這個節點的前驅節點,然後使用前驅節點的next指標進行刪除和拼接操作。memcached的做法差不多,實現如下:
void assoc_delete(const char *key, const size_t nkey, const uint32_t hv) {
item **before = _hashitem_before(key, nkey, hv);//得到前驅節點的h_next成員地址
if (*before) {//查詢成功
item *nxt;
hash_items--;
//因為before是一個二級指標,其值為所查詢item的前驅item的h_next成員地址.
//所以*before指向的是所查詢的item.因為before是一個二級指標,所以
//*before作為左值時,可以給h_next成員變數賦值。所以下面三行程式碼是
//使得刪除中間的item後,前後的item還能連得起來。
nxt = (*before)->h_next;
(*before)->h_next = 0; /* probably pointless, but whatever. */
*before = nxt;
return;
}
/* Note: we never actually get here. the callers don't delete things
they can't find. */
assert(*before != 0);
}
//查詢item。返回前驅節點的h_next成員地址,如果查詢失敗那麼就返回衝突鏈中最後
//一個節點的h_next成員地址。因為最後一個節點的h_next的值為NULL。通過對返回值
//使用 * 運算子即可知道有沒有查詢成功。
static item** _hashitem_before (const char *key, const size_t nkey, const uint32_t hv) {
item **pos;
unsigned int oldbucket;
//同樣,看的時候直接跳到else部分
if (expanding &&//正在擴充套件雜湊表
(oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
{
pos = &old_hashtable[oldbucket];
} else {
//找到雜湊表中對應的桶位置
pos = &primary_hashtable[hv & hashmask(hashpower)];
}
//遍歷桶的衝突鏈查詢item
while (*pos && ((nkey != (*pos)->nkey) || memcmp(key, ITEM_key(*pos), nkey))) {
pos = &(*pos)->h_next;
}
//*pos就可以知道有沒有查詢成功。如果*pos等於NULL那麼查詢失敗,否則查詢成功。
return pos;
}
擴充套件雜湊表:
當雜湊表中item的數量達到了雜湊表表長的1.5倍時,那麼就會擴充套件雜湊表增大雜湊表的表長。memcached在插入一個item時會檢查當前的item總數是否達到了雜湊表表長的1.5倍。由於item的雜湊值是比較均勻的,所以平均來說每個桶的衝突鏈長度大概就是1.5個節點。所以memcached的雜湊查詢還是很快的。
遷移執行緒:
擴充套件雜湊表有一個很大的問題:擴充套件後雜湊表的長度變了,item雜湊後的位置也是會跟著變化的(回憶一下memcached是怎麼根據鍵值的雜湊值確定桶的位置的)。所以如果要擴充套件雜湊表,那麼就需要對雜湊表中所有的item都要重新計算雜湊值得到新的雜湊位置(桶位置),然後把item遷移到新的桶上。對所有的item都要做這樣的處理,所以這必然是一個耗時的操作。後文會把這個操作稱為資料遷移。
因為資料遷移是一個耗時的操作,所以這個工作由一個專門的執行緒(姑且把這個執行緒叫做遷移執行緒吧)負責完成。這個遷移執行緒是由main函式呼叫一個函式建立的。看下面程式碼:
#define DEFAULT_HASH_BULK_MOVE 1
int hash_bulk_move = DEFAULT_HASH_BULK_MOVE;
//main函式會呼叫本函式,啟動資料遷移執行緒
int start_assoc_maintenance_thread() {
int ret;
char *env = getenv("MEMCACHED_HASH_BULK_MOVE");
if (env != NULL) {
//hash_bulk_move的作用在後面會說到。這裡是通過環境變數給hash_bulk_move賦值
hash_bulk_move = atoi(env);
if (hash_bulk_move == 0) {
hash_bulk_move = DEFAULT_HASH_BULK_MOVE;
}
}
if ((ret = pthread_create(&maintenance_tid, NULL,
assoc_maintenance_thread, NULL)) != 0) {
fprintf(stderr, "Can't create thread: %s\n", strerror(ret));
return -1;
}
return 0;
}
遷移執行緒被建立後會進入休眠狀態(通過等待條件變數),當worker執行緒插入item後,發現需要擴充套件雜湊表就會呼叫assoc_start_expand函式喚醒這個遷移執行緒。
static bool started_expanding = false;
//assoc_insert函式會呼叫本函式,當item數量到了雜湊表表長的1.5倍才會呼叫的
static void assoc_start_expand(void) {
if (started_expanding)
return;
started_expanding = true;
pthread_cond_signal(&maintenance_cond);
}
static bool expanding = false;//標明hash表是否處於擴充套件狀態
static volatile int do_run_maintenance_thread = 1;
static void *assoc_maintenance_thread(void *arg) {
//do_run_maintenance_thread是全域性變數,初始值為1,在stop_assoc_maintenance_thread
//函式中會被賦值0,終止遷移執行緒
while (do_run_maintenance_thread) {
int ii = 0;
//上鎖
item_lock_global();
mutex_lock(&cache_lock);
...//進行item遷移
//遍歷完就釋放鎖
mutex_unlock(&cache_lock);
item_unlock_global();
if (!expanding) {//不需要遷移資料(了)。
/* We are done expanding.. just wait for next invocation */
mutex_lock(&cache_lock);
started_expanding = false; //重置
//掛起遷移執行緒,直到worker執行緒插入資料後發現item數量已經到了1.5倍雜湊表大小,
//此時呼叫worker執行緒呼叫assoc_start_expand函式,該函式會呼叫pthread_cond_signal
//喚醒遷移執行緒
pthread_cond_wait(&maintenance_cond, &cache_lock);
mutex_unlock(&cache_lock);
...
mutex_lock(&cache_lock);
assoc_expand();//申請更大的雜湊表,並將expanding設定為true
mutex_unlock(&cache_lock);
}
}
return NULL;
}
逐步遷移資料:
為了避免在遷移的時候worker執行緒增刪雜湊表,所以要在資料遷移的時候加鎖,worker執行緒搶到了鎖才能增刪查詢雜湊表。memcached為了實現快速響應(即worker執行緒能夠快速完成增刪查詢操作),就不能讓遷移執行緒佔鎖太久。但資料遷移本身就是一個耗時的操作,這是一個矛盾。
memcached為了解決這個矛盾,就採用了逐步遷移的方法。其做法是,在一個迴圈裡面:加鎖-》只進行小部分資料的遷移-》解鎖。這樣做的效果是:雖然遷移執行緒會多次搶佔鎖,但每次佔有鎖的時間都是很短的,這就增加了worker執行緒搶到鎖的概率,使得worker執行緒能夠快速完成它的操作。一小部分是多少個item呢?前面說到的全域性變數hash_bulk_move就指明是多少個桶的item,預設值是1個桶,後面為了方便敘述也就認為hash_bulk_move的值為1。
逐步遷移的具體做法是,呼叫assoc_expand函式申請一個新的更大的雜湊表,每次只遷移舊雜湊表一個桶的item到新雜湊表,遷移完一桶就釋放鎖。此時就要求有一箇舊雜湊表和新雜湊表。在memcached實現裡面,用primary_hashtable表示新表(也有一些博文稱之為主表),old_hashtable表示舊錶(副表)。
前面說到,遷移執行緒被建立後就會休眠直到被worker執行緒喚醒。當遷移執行緒醒來後,就會呼叫assoc_expand函式擴大雜湊表的表長。assoc_expand函式如下:
static void assoc_expand(void) {
old_hashtable = primary_hashtable;
//申請一個新雜湊表,並用old_hashtable指向舊雜湊表
primary_hashtable = calloc(hashsize(hashpower + 1), sizeof(void *));
if (primary_hashtable) {
hashpower++;
expanding = true;//標明已經進入擴充套件狀態
expand_bucket = 0;//從0號桶開始資料遷移
} else {
primary_hashtable = old_hashtable;
/* Bad news, but we can keep running. */
}
}
現在看一下完整一點的assoc_maintenance_thread執行緒函式,體會遷移執行緒是怎麼逐步資料遷移的。為什麼說完整一點呢?因為該函式裡面還是有一些東西本篇博文是沒有解釋的,但這並不妨礙我們閱讀該函式。後面還會有其他博文對這個執行緒函式進行講解的。
static unsigned int expand_bucket = 0;//指向待遷移的桶
#define DEFAULT_HASH_BULK_MOVE 1
int hash_bulk_move = DEFAULT_HASH_BULK_MOVE;
static volatile int do_run_maintenance_thread = 1;
static void *assoc_maintenance_thread(void *arg) {
//do_run_maintenance_thread是全域性變數,初始值為1,在stop_assoc_maintenance_thread
//函式中會被賦值0,終止遷移執行緒
while (do_run_maintenance_thread) {
int ii = 0;
//上鎖
item_lock_global();
mutex_lock(&cache_lock);
//hash_bulk_move用來控制每次遷移,移動多少個桶的item。預設是一個.
//如果expanding為true才會進入迴圈體,所以遷移執行緒剛建立的時候,並不會進入迴圈體
for (ii = 0; ii < hash_bulk_move && expanding; ++ii) {
item *it, *next;
int bucket;
//在assoc_expand函式中expand_bucket會被賦值0
//遍歷舊雜湊表中由expand_bucket指明的桶,將該桶的所有item
//遷移到新雜湊表中。
for (it = old_hashtable[expand_bucket]; NULL != it; it = next) {
next = it->h_next;
//重新計算新的雜湊值,得到其在新雜湊表的位置
bucket = hash(ITEM_key(it), it->nkey) & hashmask(hashpower);
//將這個item插入到新雜湊表中
it->h_next = primary_hashtable[bucket];
primary_hashtable[bucket] = it;
}
//不需要清空舊桶。直接將衝突鏈的鏈頭賦值為NULL即可
old_hashtable[expand_bucket] = NULL;
//遷移完一個桶,接著把expand_bucket指向下一個待遷移的桶
expand_bucket++;
if (expand_bucket == hashsize(hashpower - 1)) {//全部資料遷移完畢
expanding = false; //將擴充套件標誌設定為false
free(old_hashtable);
}
}
//遍歷完hash_bulk_move個桶的所有item後,就釋放鎖
mutex_unlock(&cache_lock);
item_unlock_global();
if (!expanding) {//不再需要遷移資料了。
/* finished expanding. tell all threads to use fine-grained(細粒度的) locks */
//進入到這裡,說明已經不需要遷移資料(停止擴充套件了)。
...
mutex_lock(&cache_lock);
started_expanding = false; //重置
//掛起遷移執行緒,直到worker執行緒插入資料後發現item數量已經到了1.5倍雜湊表大小,
//此時呼叫worker執行緒呼叫assoc_start_expand函式,該函式會呼叫pthread_cond_signal
//喚醒遷移執行緒
pthread_cond_wait(&maintenance_cond, &cache_lock);
/* Before doing anything, tell threads to use a global lock */
mutex_unlock(&cache_lock);
...
mutex_lock(&cache_lock);
assoc_expand();//申請更大的雜湊表,並將expanding設定為true
mutex_unlock(&cache_lock);
}
}
return NULL;
}
回馬槍:
現在再回過頭來再看一下雜湊表的插入、刪除和查詢操作,因為這些操作可能發生在雜湊表遷移階段。有一點要注意,在assoc.c檔案裡面的插入、刪除和查詢操作,是看不到加鎖操作的。但前面已經說了,需要和遷移執行緒搶佔鎖,搶到了鎖才能進行對應的操作。其實,這鎖是由插入、刪除和查詢的呼叫者(主調函式)負責加的,所以在程式碼裡面看不到。
因為插入的時候可能雜湊表正在擴充套件,所以插入的時候要面臨一個選擇:插入到新表還是舊錶?memcached的做法是:當item對應在舊錶中的桶還沒被遷移到新表的話,就插入到舊錶,否則插入到新表。下面是插入部分的程式碼。
/* Note: this isn't an assoc_update. The key must not already exist to call this */
//hv是這個item鍵值的雜湊值
int assoc_insert(item *it, const uint32_t hv) {
unsigned int oldbucket;
//使用頭插法 插入一個item
if (expanding &&//目前處於擴充套件hash表狀態
(oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)//資料遷移時還沒遷移到這個桶
{
//插入到舊錶
it->h_next = old_hashtable[oldbucket];
old_hashtable[oldbucket] = it;
} else {
//插入到新表
it->h_next = primary_hashtable[hv & hashmask(hashpower)];
primary_hashtable[hv & hashmask(hashpower)] = it;
}
hash_items++;//雜湊表的item數量加一
//當hash表的item數量到達了hash表容量的1.5倍時,就會進行擴充套件
//當然如果現在正處於擴充套件狀態,是不會再擴充套件的
if (! expanding && hash_items > (hashsize(hashpower) * 3) / 2) {
assoc_start_expand();//喚醒遷移執行緒,擴充套件雜湊表
}
return 1;
}
這裡有一個疑問,為什麼不直接插入到新表呢?直接插入到新表對於資料一致性來說完全是沒有問題的啊。網上有人說是為了保證同一個桶item的順序,但由於遷移執行緒和插入執行緒對於鎖搶佔的不確定性,任何順序都不能通過assoc_insert函式來保證。本文認為是為了快速查詢。如果是直接插入到新表,那麼在查詢的時候就可能要同時查詢新舊兩個表才能找到item。查詢完一個表,發現沒有,然後再去查詢另外一個表,這樣的查詢被認為是不夠快速的。
如果按照assoc_insert函式那樣的實現,不用查詢兩個表就能找到item。看下面的查詢函式。
//由於雜湊值只能確定是在雜湊表中的哪個桶(bucket),但一個桶裡面是有一條衝突鏈的
//此時需要用到具體的鍵值遍歷並一一比較衝突鏈上的所有節點。因為key並不是以'\0'結尾
//的字串,所以需要另外一個引數nkey指明這個key的長度
item *assoc_find(const char *key, const size_t nkey, const uint32_t hv) {
item *it;
unsigned int oldbucket;
if (expanding &&//正在擴充套件雜湊表
(oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)//該item還在舊錶裡面
{
it = old_hashtable[oldbucket];
} else {
//由雜湊值判斷這個key是屬於那個桶(bucket)的
it = primary_hashtable[hv & hashmask(hashpower)];
}
//到這裡已經確定了要查詢的item是屬於哪個表的了,並且也確定了桶位置。遍歷對應桶的衝突鏈即可
item *ret = NULL;
while (it) {
//長度相同的情況下才呼叫memcmp比較,更高效
if ((nkey == it->nkey) && (memcmp(key, ITEM_key(it), nkey) == 0)) {
ret = it;
break;
}
it = it->h_next;
}
return ret;
}
刪除操作和查詢操作差不多,這裡直接貼出,不多說了。刪除操作也是要進行查詢操作的。
void assoc_delete(const char *key, const size_t nkey, const uint32_t hv) {
item **before = _hashitem_before(key, nkey, hv);//得到前驅節點的h_next成員地址
if (*before) {//查詢成功
item *nxt;
hash_items--;
//因為before是一個二級指標,其值為所查詢item的前驅item的h_next成員地址.
//所以*before指向的是所查詢的item.因為before是一個二級指標,所以
//*before作為左值時,可以給h_next成員變數賦值。所以下面三行程式碼是
//使得刪除中間的item後,前後的item還能連得起來。
nxt = (*before)->h_next;
(*before)->h_next = 0; /* probably pointless, but whatever. */
*before = nxt;
return;
}
/* Note: we never actually get here. the callers don't delete things
they can't find. */
assert(*before != 0);
}
//查詢item。返回前驅節點的h_next成員地址,如果查詢失敗那麼就返回衝突鏈中最後
//一個節點的h_next成員地址。因為最後一個節點的h_next的值為NULL。通過對返回值
//使用 * 運算子即可知道有沒有查詢成功。
static item** _hashitem_before (const char *key, const size_t nkey, const uint32_t hv) {
item **pos;
unsigned int oldbucket;
if (expanding &&//正在擴充套件雜湊表
(oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
{
pos = &old_hashtable[oldbucket];
} else {
//找到雜湊表中對應的桶位置
pos = &primary_hashtable[hv & hashmask(hashpower)];
}
//到這裡已經確定了要查詢的item是屬於哪個表的了,並且也確定了桶位置。遍歷對應桶的衝突鏈即可
//遍歷桶的衝突鏈查詢item
while (*pos && ((nkey != (*pos)->nkey) || memcmp(key, ITEM_key(*pos), nkey))) {
pos = &(*pos)->h_next;
}
//*pos就可以知道有沒有查詢成功。如果*pos等於NULL那麼查詢失敗,否則查詢成功。
return pos;
}
由上面的討論可以知道,插入和刪除一個item都必須知道這個item對應的桶有沒有被遷移到新表上了。
相關文章
- Java集合原始碼分析之基礎(二):雜湊表Java原始碼
- 從Dictionary原始碼看雜湊表原始碼
- 《閒扯Redis八》Redis字典的雜湊表執行Rehash過程分析Redis
- Spring啟動過程原始碼分析基本概念Spring原始碼
- 雜湊表擴充套件—布隆過濾器(Bloom Filter)套件過濾器OOMFilter
- 雜湊表擴充套件—點陣圖套件
- 超詳細的ArrayList擴容過程(配合原始碼詳解)原始碼
- HashMap擴容機制原始碼分析HashMap原始碼
- 雜湊表(雜湊表)原理詳解
- external-resizer 原始碼分析/pvc 擴容分析原始碼
- 雜湊表
- Ubuntu 磁碟擴容過程Ubuntu
- 【尋跡#3】 雜湊與雜湊表
- Java HashMap原始碼分析(含雜湊表、紅黑樹、擾動函式等重點問題分析)JavaHashMap原始碼函式
- Memcached記憶體管理原始碼分析記憶體原始碼
- 雜湊表2
- 字串雜湊表字串
- 6.7雜湊表
- 雜湊遊戲原始碼開發搭建丨hash雜湊遊戲競猜原始碼搭建丨雜湊遊戲系統開發遊戲原始碼
- HASH雜湊遊戲原始碼丨HASH雜湊遊戲系統開發丨HASH雜湊遊戲開發成品原始碼部署原始碼遊戲開發
- 通過原始碼一步一步分析 ArrayList 擴容機制原始碼
- Spring啟動過程——原始碼分析Spring原始碼
- Netty NioEventLoop 建立過程原始碼分析NettyOOP原始碼
- Glide的load()過程原始碼分析IDE原始碼
- 線性表 & 雜湊表
- 雜湊表的程式碼實現(Java)Java
- 十二、雜湊表(二)
- 十一、雜湊表(一)
- 雜湊表應用
- 手寫雜湊表
- 雜湊表的原理
- 雜湊擴充套件攻擊套件
- memcached的學習過程
- 曹工說JDK原始碼(1)--ConcurrentHashMap,擴容前大家同在一個雜湊桶,為啥擴容後,你去新陣列的高位,我只能去低位?JDK原始碼HashMap陣列
- 從原始碼解析 Go 的切片型別以及擴容機制原始碼Go型別
- 原始碼分析OKHttp的執行過程原始碼HTTP
- Netty NioEventLoop 啟動過程原始碼分析NettyOOP原始碼
- Spring Boot原始碼分析-啟動過程Spring Boot原始碼
- Spring MVC 啟動過程原始碼分析SpringMVC原始碼