数据映射--跳表(skiplist)
http://blog.sina.com.cn/s/blog_693f08470101n2lv.html
本周我要介紹的數(shù)據(jù)結(jié)構(gòu),是我非常非常喜歡的一個(gè)數(shù)據(jù)結(jié)構(gòu),因?yàn)樵垡彩浅赃^平衡二叉樹的苦的人啊T_T?,神馬左旋,右旋,上旋,下旋,看原理的時(shí)候就已經(jīng)暈暈乎乎的了,再看源碼,發(fā)現(xiàn)比原理還復(fù)雜,心理就想,這東西是不是就是為了讓我掛科給學(xué)校交重修費(fèi)來(lái)拯救學(xué)校財(cái)政的東西啊?!。。
?
當(dāng)然,現(xiàn)在再來(lái)看,這些東西有其非常重要的作用,只是確實(shí)有點(diǎn)復(fù)雜了,不過,突然有一天,有個(gè)結(jié)構(gòu)進(jìn)入了我的視野,第一次聽說是在redis上面看到,后來(lái)發(fā)現(xiàn)原來(lái)java的并發(fā)包里也早就實(shí)現(xiàn)了這個(gè)結(jié)構(gòu),可以做到很高的并發(fā)度,幾乎是全部無(wú)鎖的結(jié)構(gòu),而最重要的是,這個(gè)結(jié)構(gòu)的原理非常的簡(jiǎn)單!?這么漂亮的結(jié)構(gòu)才是拯救地球的大舅星啊~~~
?
沒錯(cuò),今天我們來(lái)介紹skiplist跳表。
?
說起跳表,我們還是要從二分查找開始。二分查找的關(guān)鍵要求有兩個(gè),1,數(shù)據(jù)能夠按照某種條件進(jìn)行排序。2,可以通過某種方式,取出該數(shù)據(jù)集中任意子集的中間值。
能夠滿足的數(shù)據(jù)結(jié)構(gòu)主要是有序數(shù)組,但對(duì)于數(shù)據(jù)量不斷變化的場(chǎng)景來(lái)說,有序數(shù)組很難能夠高效的進(jìn)行寫入。鏈表是一種最容易處理數(shù)據(jù)不斷增加結(jié)構(gòu)的有序數(shù)據(jù)結(jié)構(gòu),并且因?yàn)橐呀?jīng)有了無(wú)鎖完成多線程鏈表寫入的算法,因此鏈表對(duì)于并發(fā)的支持度是非常好的(我們后面會(huì)介紹這個(gè)算法),然而鏈表卻不能夠進(jìn)行二分查找,因?yàn)闊o(wú)法取到任意子集的中值。
所以人們又去想辦法基于樹來(lái)做能夠既支持寫入,又能夠通過“預(yù)先找到中值并寫到父節(jié)點(diǎn)”的方式來(lái)提前將中值準(zhǔn)備好,這就是平衡有序二叉樹。不過,無(wú)論是AVL還是紅黑樹,這個(gè)預(yù)先找到中值并寫入到父節(jié)點(diǎn)的操作的都是非常復(fù)雜的,對(duì)于復(fù)雜的操作來(lái)說,想使用常見的無(wú)鎖操作就幾乎不可能了。
最后,綜合一下,鏈表結(jié)構(gòu)能夠做到并發(fā)無(wú)鎖的增加新節(jié)點(diǎn),但不能很容易的訪問到中值(因?yàn)殒湵碇荒軓念^部遍歷或尾部遍歷)。平衡有序二叉樹則相反,雖然很容易可以訪問到全部數(shù)據(jù)的中值,但無(wú)法做到并發(fā)無(wú)鎖的增加新節(jié)點(diǎn)。
在90年代之前,人們一直以“這就是生活”?來(lái)安慰自己,認(rèn)為魚與熊掌不可兼得。但在90年代,William Pugh在他的論文中提出了一種新的數(shù)據(jù)結(jié)構(gòu),很巧妙的解決了這個(gè)矛盾,另外也八卦一下,其實(shí)目前Java領(lǐng)域很流行的find bugs靜態(tài)代碼分析工具也是william發(fā)明的~
首先我們先定義一個(gè)概念,叫層(level) ,為了方便理解,大家可以直接對(duì)應(yīng)到平衡有序二叉樹里面的樹的高度。
?
每一層在邏輯上都是一個(gè)鏈表,既然是鏈表,那么自然也就只能從頭部遍歷或從尾部遍歷咯。
一個(gè)標(biāo)準(zhǔn)的skiplist在內(nèi)存中可能是這樣的:
Level2:0,4
Level1:0,2,4,6,9
Level0:0,1,2,3,4,5,6,7,8,9
?
可以看到,層級(jí)越高,數(shù)據(jù)量越小,并且,高層級(jí)的元素都有一個(gè)到低層級(jí)元素的指針,這樣他可以很容易的通過指針跳轉(zhuǎn)到更底層的元素上面。
?
下面讓我們來(lái)看看讀取的邏輯,比如如果要讀取6,那么從最高層級(jí)的鏈表的頭部(從左向右)依次讀取數(shù)據(jù),發(fā)現(xiàn)6>4,于是在通過Level2?的4?這個(gè)元素到level1的4這個(gè)元素的指針,跳躍到Level1,然后從Level1的4這個(gè)元素繼續(xù)往右面找發(fā)現(xiàn)下一個(gè)元素就是6,于是將整個(gè)6所對(duì)應(yīng)的元素返回。
那么要找3的話應(yīng)該怎么操作呢?
仍然是從最高層級(jí)level2的頭部開始遍歷,發(fā)現(xiàn)0<3<4 .?于是利用level2的0這個(gè)元素到level1的0這個(gè)元素的指針,跳躍到level1的0元素,繼續(xù)向右遍歷,發(fā)現(xiàn)2<3<4。于是利用Level1的2這個(gè)元素到level0的2這個(gè)元素的指針,跳躍到level0的2這個(gè)元素上,繼續(xù)向右遍歷找到元素3,于是將整個(gè)3所對(duì)應(yīng)的元素返回。
?
可以看到,利用這種結(jié)構(gòu)如果我們能夠比較準(zhǔn)確的在鏈表里將數(shù)據(jù)排好序,并且level0中每?jī)蓚€(gè)元素中拿出一個(gè)元素推送到更高的層級(jí)level1中,然后在level1中也按照每?jī)蓚€(gè)元素拿出一個(gè)元素推送到更高層級(jí)的level2中…依此類推,就可以構(gòu)建出一個(gè)查詢時(shí)間復(fù)雜度為O(log2n)的查找數(shù)據(jù)結(jié)構(gòu)了。
?
但這里有個(gè)關(guān)鍵的難在于:如何能夠知道,當(dāng)前寫入的元素是否應(yīng)該被推送到更高的層級(jí)呢?這也就對(duì)應(yīng)了原來(lái)avl,紅黑里面為什么要做如此復(fù)雜的旋轉(zhuǎn)的原因。而在william的解決方案里,他選擇了一條完全不相同的路來(lái)做到這一點(diǎn)。
?
這也是skiplist里面一個(gè)最大的創(chuàng)新點(diǎn),就是引入了一個(gè)新條件:概率。與傳統(tǒng)的根據(jù)臨近元素的來(lái)決定是否上推的avl或紅黑樹相比。Skiplist則使用概率這個(gè)完全不需要依托集合內(nèi)其他元素的因素來(lái)決定這個(gè)元素是否要上推。這種方式的最大好處,就是可以讓每一次的插入都變得更“獨(dú)立”,而不需要依托于其他元素插入的結(jié)果。?這樣就能夠讓沖突只發(fā)生在數(shù)據(jù)真正寫入的那一步操作上,而我們已經(jīng)在前面的文章里面知道了,對(duì)于鏈表來(lái)說,數(shù)據(jù)的寫入是能夠做到無(wú)鎖的寫入新數(shù)據(jù)的,于是,利用skiplist,就能成功的做到無(wú)鎖的有序平衡“樹”(多層級(jí))結(jié)構(gòu)。
?
下面我們就來(lái)看看如何利用概率來(lái)決定某個(gè)元素是否需要上推的。
讓我們先用一個(gè)簡(jiǎn)單的模式來(lái)說明解決問題的思路,然后再探討如何進(jìn)行優(yōu)化。
我們可以把skiplist的寫入分為兩個(gè)步驟,第一個(gè)步驟是找到元素在整個(gè)順序列表中要寫入的位置,這個(gè)步驟與我們上面講到的讀取過程是一致的。
然后下一個(gè)步驟是決定這個(gè)數(shù)據(jù)是否需要從當(dāng)前層級(jí)上推到上一個(gè)層級(jí),具體的做法是從最低層級(jí)level0開始,寫入用戶需要寫入的值,并計(jì)算一個(gè)隨機(jī)數(shù),如果是0,則不上推到高一層,而如果是1,則上推到高一個(gè)層,然后指針跳躍到高一個(gè)層級(jí),重復(fù)進(jìn)行隨機(jī)數(shù)計(jì)算來(lái)決定是否需要推到更高的層級(jí),如果最高層中只有自己這個(gè)元素的時(shí)候,則也停止計(jì)算隨機(jī)數(shù)(因?yàn)椴恍枰偻频礁邔恿?#xff09;。
最后,還有個(gè)問題就是如何解決并發(fā)寫入的問題,為了闡述清楚如何能夠做到并發(fā)寫,我們需要先對(duì)什么叫”一致性的寫”,進(jìn)行一下說明。
一般的人理解數(shù)據(jù)的一致性寫的定義可能是:如果寫成功了你就讓我看到,而如果沒寫成功,你就不讓我看到唄。
但實(shí)際上這個(gè)定義在計(jì)算機(jī)里面是無(wú)法操作的,因?yàn)槲覀冎耙蔡岬竭^,計(jì)算機(jī)其實(shí)就是個(gè)打字機(jī),一次只能進(jìn)行一個(gè)操作,針對(duì)復(fù)雜的操作,只能通過加鎖來(lái)實(shí)現(xiàn)一致性。但加鎖本身的代價(jià)又很大,這就形成了個(gè)悖論,如何能夠既保證性能,又能夠?qū)崿F(xiàn)一致性呢?
這時(shí)候就需要我們對(duì)一致性的定義針對(duì)多線程環(huán)境進(jìn)行一下完善:在之前的定義,我們是把寫入的過程分為兩個(gè)時(shí)間點(diǎn)的,一個(gè)時(shí)間點(diǎn)是調(diào)用寫入接口前,另一個(gè)時(shí)間點(diǎn)是調(diào)用寫入接口后。但其實(shí)在多線程環(huán)境下,應(yīng)該分為三個(gè)時(shí)間點(diǎn),第一個(gè)是調(diào)用寫入接口前,第二個(gè)是調(diào)用寫入接口,但還未返回結(jié)果的那段時(shí)間,第三個(gè)是調(diào)用寫入接口,返回結(jié)果后。
然后我們來(lái)看看,針對(duì)這三個(gè)時(shí)間點(diǎn)應(yīng)該如何選擇,才能保證數(shù)據(jù)的一致性:
對(duì)于第一個(gè)時(shí)間點(diǎn),因?yàn)檫€沒有調(diào)用寫入接口,所以所有線程(包含調(diào)用寫入的線程)都不應(yīng)該能夠從這個(gè)映射中讀取到待寫入的數(shù)據(jù)。
第二個(gè)時(shí)間點(diǎn),也就是寫入操作過程中,我們需要能夠保證:如果數(shù)據(jù)已經(jīng)被其他線程看到過了,那么再這個(gè)時(shí)間點(diǎn)之后的所有時(shí)間點(diǎn),數(shù)據(jù)應(yīng)該都能夠被其他線程看到,也就是說不能出現(xiàn)先被看到但又被刪掉的情況。
第三個(gè)時(shí)間點(diǎn),這個(gè)寫入的操作應(yīng)該能夠被所有人看到。
已經(jīng)定義好了一致性的規(guī)范,下面就來(lái)看看這個(gè)無(wú)鎖并發(fā)的skiplist是如何處理好并發(fā)一致性的。
首先我們需要先了解一下鏈表是如何能夠做到無(wú)鎖寫入的:
對(duì)于鏈表類的數(shù)據(jù)結(jié)構(gòu)來(lái)說,如果想做到無(wú)鎖,主要就是解決以下的問題,如何能夠讓當(dāng)前線程知道,目前要插入新元素的位置,是否有其他人正在插入? 如果有的話,那么就自旋等待,如果沒有,那么就插入。利用這個(gè)原理,把原來(lái)的多步指針變更操作利用compare and set的方式轉(zhuǎn)換為一個(gè)偽原子操作。這樣就可以有效的減少鎖導(dǎo)致的上下文切換開銷,在爭(zhēng)用不頻繁的情況下,極大的提升性能。(這只是思路,關(guān)于linkedlist的無(wú)鎖編程細(xì)節(jié),可以參照A pragmatic implementation of non-blocking linked lists,這篇文章)
利用上面鏈表的無(wú)鎖寫入,我們就能夠保證,數(shù)據(jù)在每一個(gè)level內(nèi)的寫是保證無(wú)鎖寫入的。并且,因?yàn)槊恳淮斡行碌臄?shù)據(jù)寫入的時(shí)候其他嘗試寫入的線程也都能感知的到,所以這些并行寫入的數(shù)據(jù)可以通過不斷相互比較的方式來(lái)了解到,自己這個(gè)要寫入的數(shù)據(jù)與其他并行寫入的數(shù)據(jù)之間的大小關(guān)系,從而可以動(dòng)態(tài)的進(jìn)行調(diào)整以保證在每一層內(nèi),數(shù)據(jù)都是絕對(duì)有序的。
同一個(gè)level的一致性解決了,那么不同level之間的一致性是如何得到解決的呢?這就與我們剛才定義的一致性規(guī)范緊密相關(guān)了。因?yàn)閿?shù)據(jù)的寫入是從低層級(jí)開始,一層一層的往更高的層級(jí)推送的。而數(shù)據(jù)讀取的時(shí)候,則是從最高層級(jí)往下讀取的。又因?yàn)閿?shù)據(jù)是絕對(duì)有序的,那么我們就一定可以認(rèn)為,只要最低層級(jí)(level0)內(nèi)存在了的數(shù)據(jù),那么他就一定能夠被所有線程看到。而如果在上推的過程中出現(xiàn)了任何異常,其實(shí)都是沒關(guān)系的,因?yàn)樯贤频奈ㄒ荒康脑谟诩涌鞕z索速度,所以就算因?yàn)楫惓]有上推,也只是降低了查詢的效率,對(duì)數(shù)據(jù)的可見性完全沒有影響。
這個(gè)設(shè)計(jì)確實(shí)是非常的巧妙~
這樣,雖然每個(gè)元素的具體能夠到達(dá)哪個(gè)層級(jí)是隨機(jī)的,但從宏觀上來(lái)看,低層元素的個(gè)數(shù)基本上是高層元素個(gè)數(shù)的一倍。從宏觀上來(lái)看,如果按照我們上面定義的自最高層級(jí)依次往下遍歷的讀取模式,那么整個(gè)查詢的時(shí)間復(fù)雜度就是O(log2n)。
?
下面來(lái)介紹一些優(yōu)化的思路,因?yàn)檫M(jìn)行隨機(jī)數(shù)的運(yùn)算本身也是個(gè)很消耗cpu的操作,所以,一種最常見的優(yōu)化就是,如果在插入的時(shí)候就能直接算出這個(gè)數(shù)據(jù)應(yīng)該往高層推的總次數(shù),那么就不需要算那么多次隨機(jī)數(shù)了,每次寫入只需要算一次就行了。
第二個(gè)優(yōu)化的思路是如何能夠?qū)崿F(xiàn)一個(gè)高性能的隨機(jī)數(shù)算法,這個(gè)各位可以自行搜索。
?
Skiplist是一個(gè)我個(gè)人很喜歡的數(shù)據(jù)結(jié)構(gòu),因?yàn)樗銐蚝?jiǎn)單,性能又好,除了運(yùn)氣非常差的時(shí)候效率很低,其他時(shí)候都能做到很好的查詢效率,賭博什么的最喜歡了~~~最重要的是,它還足夠簡(jiǎn)單和容易理解!
下面照例,我們使用一些通用的標(biāo)準(zhǔn)對(duì)skiplis進(jìn)行一下簡(jiǎn)單的評(píng)價(jià):
1.???????是否支持范圍查找
?
因?yàn)槭怯行蚪Y(jié)構(gòu),所以能夠很好的支持范圍查找。
?
2.???????集合是否能夠隨著數(shù)據(jù)的增長(zhǎng)而自動(dòng)擴(kuò)展
?
可以,因?yàn)楹诵臄?shù)據(jù)結(jié)構(gòu)是鏈表,所以是可以很好的支持?jǐn)?shù)據(jù)的不斷增長(zhǎng)的
?
3.???????讀寫性能如何
?
因?yàn)閺暮暧^上可以做到一次排除一半的數(shù)據(jù),并且在寫入時(shí)也沒有進(jìn)行其他額外的數(shù)據(jù)查找性工作,所以對(duì)于skiplist來(lái)說,其讀寫的時(shí)間復(fù)雜度都是O(log2n)
?
4.???????是否面向磁盤結(jié)構(gòu)
?
磁盤要求順序?qū)?#xff0c;順序讀,一次讀寫必須是一整塊的數(shù)據(jù)。而對(duì)于skiplist來(lái)說,查詢中每一次從高層跳躍到底層的操作,都會(huì)對(duì)應(yīng)一次磁盤隨機(jī)讀,而skiplist的層數(shù)從宏觀上來(lái)看一定是O(log2n)層。因此也就對(duì)應(yīng)了O(log2n)次磁盤隨機(jī)讀。
因此這個(gè)數(shù)據(jù)結(jié)構(gòu)不適合于磁盤結(jié)構(gòu)。
?
并行指標(biāo)
終于來(lái)到這個(gè)指標(biāo)了,?skiplist的并行指標(biāo)是非常好的,只要不是在同一個(gè)目標(biāo)插入點(diǎn)插入數(shù)據(jù),所有插入都可以并行進(jìn)行,而就算在同一個(gè)插入點(diǎn),插入本身也可以使用無(wú)鎖自旋來(lái)提升寫入效率。
因此skiplist是個(gè)并行度非常高的數(shù)據(jù)結(jié)構(gòu)。
?
內(nèi)存占用
與平衡二叉樹的內(nèi)存消耗基本一致。
unsafe_skiplist
#include <iostream> #include <stdlib.h> #include <time.h> using namespace std;typedef int key_t; typedef int value_t; #define MAX_LEVEL 16 #define SKIPLIST_P 0.25struct node_t {key_t key;value_t val;node_t *forward[]; };class SkipList {protected:int m_level;int m_length;node_t *header;node_t *creatNode(int level, key_t key, value_t val) {node_t *node = (node_t*)malloc(sizeof(node_t)+level*sizeof(node_t*));if (node == NULL) {return NULL;}node->key = key;node->val = val;srand(time(NULL));return node;}public:SkipList() {header = creatNode(MAX_LEVEL, 0, 0);if (header == NULL)exit(-1);m_length = 0;m_level = 0;for (int i = 0; i < MAX_LEVEL; ++i) {header->forward[i] = NULL;}}value_t *getValue(key_t key) {int beg = m_level - 1;node_t *p = header;for (; beg >=0; --beg) {while (p->forward[beg] && p->forward[beg]->key <= key) {if (p->forward[beg]->key == key)return &p->forward[beg]->val;p = p->forward[beg];}//p = p->forward[beg-1];}return NULL;}int randomLevel() {int level = 1;while ((rand()&0xffff) < 0xffff*SKIPLIST_P)++level;if(level > MAX_LEVEL) level = MAX_LEVEL;return level;}void insert(key_t key, value_t val) {node_t *update[MAX_LEVEL];int beg = m_level - 1;node_t *p = header;node_t *last = NULL;for (; beg >=0; --beg) {while( (last = p->forward[beg]) && last->key < key) {p = p->forward[beg];}update[beg] = p;}if (last && last->key == key) {last->val = val;return;}m_length++;int level = randomLevel();if (level > m_level) {for(int i = m_level; i < level; ++i)update[i] = header;m_level = level;}node_t *node = creatNode(level, key, val);for (beg = level - 1; beg >=0; --beg) {node->forward[beg] = update[beg]->forward[beg];update[beg]->forward[beg] = node;}}void erase(key_t key) {node_t *update[MAX_LEVEL];int beg = m_level - 1;node_t *p = header;node_t *last = NULL;for (; beg >=0; --beg) {while ((last = p->forward[beg]) && last->key < key) {p = p->forward[beg];}}if (last && last->key != key)return;for (beg = m_level; beg >=0; --beg) {if (update[beg]->forward[beg] == last){update[beg]->forward[beg] = last->forward[beg];if (header->forward[beg] == NULL)m_level--;}}free(last);m_length--;}void display() {node_t *p = header->forward[0];while (p) {cout << p->key << ":"<<p->val<<" ";p = p->forward[0];}cout <<endl;}~SkipList() {node_t *p = header;while (p){node_t *next = p->forward[0];free(p);p = next;} }};int main() {SkipList sl;/*for (int i = 0; i < 1000; ++i){cout << i <<endl;sl.insert(rand(),i);}*/sl.insert(0,10);sl.insert(5, 50);sl.insert(6, 60);sl.insert(0, 11);sl.insert(5, 51);sl.insert(7,70);sl.insert(3, 30);sl.insert(4,40);sl.insert(3,31);sl.display();return 1; }
總結(jié)
以上是生活随笔為你收集整理的数据映射--跳表(skiplist)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据映射--平衡二叉有序树
- 下一篇: Eclipse中查看源码