場景:java
在一個app中,我須要爲訪問者提供某種信息的存儲,因爲架構上已經肯定的方式,因此能夠確保每個app上存儲的用戶不會太多,因而就放在了內存中,而不是緩存。mysql
這些信息須要按期清理掉,就像會話同樣,每一個用戶都會有一個惟一的key標識符,用一個ConcurrentHashMap存放,長時間不使用就須要刪除掉了。ajax
可是它與會話不一樣的是,在清空的同時會清空掉許多用戶級別的網絡通訊對象,例如Socket或數據庫鏈接對象等。所以它的清理將與傳統的清理方法有一些區別,爲什麼?sql
由於當清理程序發現須要清理該對象的時候,這個對象正好被一個有效請求所使用,在清理對象的時候,須要將內部的Socket等資源關閉,就會致使問題。數據庫
所以我不得不在這個用戶級別的對象上去作一個狀態:瀏覽器
簡單來講有一個FREE、USE、DELETE三種狀態,FREE是能夠修改成任意狀態的,USE是使用狀態的,DELETE是刪除狀態的。USE狀態的不能被刪除,DELETE狀態的不能再被使用。緩存
簡單邏輯是:服務器
一、若是經過ConcurrentHashMap獲取到相應的對象後,須要斷定狀態不能是DELETE,再嘗試在對象上修改狀態爲USE才能使用,若是修改失敗則不能被使用,固然是用後會更新下最新的時間,這個時間將用volatile來保證可見性,以便於最近不會被清理掉,使用完後會講對象的狀態從新修改成FREE。僞代碼以下所示:網絡
[java] view plain copy架構
二、在刪除操做前也必須先獲經過ConcurrentHashMap取到對象,須要斷定狀態不能是USE,而後嘗試將狀態修改成DELELE才能真正開始作刪除操做。代碼與上面相似。
這個邏輯彷佛看似完美,我當時暈頭轉向的也認爲CAS就能夠簡單搞定這個問題,作幾個狀態嘛,簡單事情,呵呵。
結果之外發生了,外部程序偶然狀況下獲取不到這個對象,可是在獲取不到這個對象的斷點中,我使用表達式再執行一次又能獲取到,這尼瑪是什麼問題發生了呢?
剛開始我也跑偏了,由於外層有一個ConcurrentHashMap,思惟凝固在是否是這有併發可見性問題,不過這樣的猜想連我本身都沒有相信,由於我對這個組件的內在的源碼是比較瞭解的,若是它有問題,就完全顛覆可見性的問題了。
在不斷加班到半夜的迷糊中,迷迷糊糊地跟蹤代碼,發現裏頭還有一層,就看到點但願,看到了剛纔的代碼。咋一看,代碼沒有啥問題,由於這個就是狀態轉換,並且這個是在一個用戶下的操做,一個用戶併發的機率原本就很低,並且有CAS來保證原子性,能有什麼問題呢?
後來一個哥們問我可不能夠用synchronzied一會兒提醒了我,個人第一反應是不到萬不得已不用這個,這個若是放在內部作就是全部的狀態轉換所有要加上,悲觀鎖就很差了,放在外面更不靠譜,那就是一個全局的ConcurrentHashMap,那用它來控制個毛的併發啊,我就是要把鎖打散。
可是這個提示讓我在迷迷糊糊中醒了一下,我發現可能真的有併發問題,或者說假設一個用戶的客戶端同時發送多個請求上來,此時因爲是同一個用戶的請求是同一個,因此KEY確定是同樣的,緩存用戶對象也應該是同樣的,此時若是兩個請求都運行到代碼:
int old = status.get();
那麼兩個請求在此時獲取到的狀態值就是同樣的,當發生CAS的時候,只有一個會成功,另外一個不成功的就返回null了,代碼看了好久,雖然很簡單,可是隻可能這裏有問題。
考慮實際場景,還真的可能有一個客戶端的瀏覽器同時發起多個請求的狀況,由於客戶端並非簡單的頁面跳轉(簡單頁面用戶手點擊再快也有時差),而是與服務器端不少ajax交互,當一個選項發生變化的時候,確實有可能同時發起多個ajax請求。
不過怎麼改呢?用syncrhonezized,顯示我不是那麼容易放棄本身的人,哈哈,迷迷糊糊中終於纔想起來,CAS也須要考慮下嘗試,確實是這樣,那麼就改成循環來作。
可是一旦改成循環大夥第一個擔憂的問題就是可否退出循環,Java的裏面有許多死循環方式,可是這種代碼不退出就是一個大問題,可是限制次數的話,多少爲好?這很差說,由於樂觀鎖在這個階段是很差講清楚具體的次數的,或許在許多人眼中這算是小問題,可是我認爲在這些問題上是關鍵的關鍵,若是不注重就會出大問題。
後來考慮來考慮去發現這樣寫沒問題:
[java] view plain copy
這個while循環的條件是狀態沒有被刪除,狀態只要有被刪除,這個請求就應該有機會去獲取使用機會,只要有機會就應該去嘗試,你們會想會不會一直不成功呢?那不會,樂觀鎖的道理就是咱們足夠樂觀,由於咱們發生到這個點上的問題都是偶然,並且是用戶級內部發生,因此它嘗試的機率很是低,在這樣作的方式下,咱們採用樂觀機制避開了悲觀鎖帶來的巨大開銷,同時又能保證原子性。
而對於刪除就沒有必要循環了,刪除操做發現狀態是USE就不能刪除,狀態爲FREE在作CAS的時候若是CAS徵用失敗也沒有必要再去徵用,爲什麼?假若有兩個線程在徵用DELETE,另外一個成功了就OK了,若是有一個USE在與之徵用,它自己就沒有再徵用的必要。
到這裏問題基本解決,可是這個程序是否是就沒有問題了呢?
未必然也,由於最初咱們寫代碼的時候沒有考慮到多個請求同時發起的過程,因此也天然不會考慮到多個請求將狀態改成FREE的過程,假若有2個請求,其中1個請求釋放掉了將狀態修改成FREE,而另外一個還在使用中,此時有線程想將它DELETE掉,發現FREE狀態,是能夠刪除的,因而將相應的Socket關閉掉,就出大事了。
若是要徹底解決這種問題,還須要一個條件變量來使用和釋放的次數,使用時加1,釋放時候減掉1,這就有點像Lock機制了,只是可控性上更強,可是對於代碼複雜性更大,你本身也須要承擔更大的責任。
若是在應用中,出現這種問題的機率極低,那麼能夠暫時用狀態也能夠,或者爲了簡單處理也能夠直接換成Lock。爲什麼說機率低呢?由於這種數據的清理理論上不會到秒級別,例如10分鐘,一個請求來的時候,會刷新最近的操做時間,後臺操做即便一長一短,只要誤差不是10分鐘以上,在理論上就不會有問題。
你們可能一想,通常要求系統響應3s,不會有那種狀況發生。真的是這樣嘛?我認爲未必,所謂3s只是常規系統,有的系統就未必了,例如WEB版本的數據庫軟件,經過UI上輸入SQL獲取結果,WEB版本的安裝系統給上千的服務器安裝相應的軟件等等,這些操做的響應都是能夠很長的,這個值是有可能超過咱們的清理時間的,因此一切皆有可能,當你真正遇到的時候,但願這些小思路能幫助到你。