java併發 構建高效且可伸縮的結果快取

2021-07-30 07:47:24 字數 3134 閱讀 4878

幾乎所有的伺服器應用都會使用某種形式的快取。重用之前的計算結果能降低延遲,提高吞吐量,但卻要消耗更多記憶體。看上去簡單的快取,可能會將效能瓶頸轉變成伸縮性瓶頸,即使快取是用來提高單執行緒效能的。本文將開發乙個高效且可伸縮的快取,用於改進乙個高計算開銷的計算,我們會從hashmap開始,逐步完善功能,分析它們的併發問題,並討論如何修改它們。

下面基於乙個計算任務開始快取的設計

public

inte***ce

computable

public

class

function

implements

computable

}

第一階段 hashmap

public

class

memorizer1

implements

computable

@override

public synchronized v compute(a a) throws interruptedexception

return result;

}}

如上所示,memorizer1將computable實現類的計算結果快取在mapcache。因為hashmap不是執行緒安全的,為了保證併發性,memorizer1用了個很保守的方法,對整個compute方法進行同步。這導致了memorizer1會有很明顯的可伸縮性問題,當有很多執行緒呼叫compute方法,將排一列很長的隊,考慮到這麼多執行緒的阻塞,執行緒狀態切換,記憶體占用,這種方式甚至不如不使用快取。

第二階段 concurrenthashmap

public

class

memorizer2

implements

computable

@override

public v compute(a a) throws interruptedexception

return result;

}}

memorizer2比memorizer1擁有更好的併發性,並且具有良好的伸縮性。但它仍然有一些不足——當兩個執行緒同時計算同乙個值,它們並不知道有其它執行緒在做同一的事,存在著資源被浪費的可能。這個不足,對於快取的物件只提供單次初始化,會帶來安全性問題。

第三階段 concurrenthashmap+futuretask

事實上,第二階段的功能已經符合大部分情況的功能,但是當計算時間很長導致很多執行緒進行同乙個運算,或者快取的物件只提供單次初始化,問題就會很棘手,在這裡,我們引入futuretask來讓進行運算的執行緒獲知是否已經有其它正在,或已經進行該運算的執行緒。

public

class

memorizer3

implements

computable

@override

public v compute(a a) throws interruptedexception

}futuretaskft = new futuretask(eval);

f = ft;

cache.put(a, ft);

ft.run();

}trycache(executionexception e)

}}

memorizer3快取的不是計算的結果,而是進行運算的futuretask。因此memorizer3首先檢查有沒有執行該任務的futuretask。如果有,則直接獲得futuretask,如果計算已經完成,futuretask.get()方法可以立刻獲得結果,如果計算未完成,後進入的執行緒阻塞直到get()返回結果;如果沒有,則建立乙個futuretask進行運算,後續進了的同樣的運算可以直接拿到結果或者等待運算完成獲得結果。

memorizer3的實現近乎完美,但是仍然存在乙個問題,當a執行緒判斷沒有快取是,進入到cache.put(a, ft);這一步前,b執行緒恰好判斷快取為空,b執行緒建立的futuretask會把a建立的futuretask覆蓋掉。雖然這相比memorizer2已經是小概率事件,但是問題還是沒根本解決。

第四階段 concurrenthashmap + futuretask + map原子操作

第三階段的concurrenthashmap + futuretask由於存在「先檢查再執行「的操作,會有併發問題,我們給cache使用復合操作(「若沒有則新增「),避免該問題。

public

class

memorizer4

implements

computable

@override

public v compute(a a) throws interruptedexception

}futuretaskft = new futuretask(eval);

f = cache.putifabsent(a, ft);

if(f == null)

}trycatch(cancellationexception e)catch(executionexception e)}}

}

memorizer4做了兩點改進:

1. 插入時會再次檢查是否有快取,並且這是個復合操作

f =

cache

.putifabsent(a, ft);

if(f ==

null){

f =ft; ft

.run();

這裡考慮到了一種情況,如果正在執行的futuretask被終止,那進行該運算的所有請求都會出問題,始料未及的遭遇cancellationexception異常。memorizer4的compute操作是乙個迴圈,當在get()阻塞的執行緒catch到cancellationexception異常,則會再一次申請乙個建立futuretask的機會。

至此,整個設計過程就結束了。我們得到了乙個在極端環境下依然能夠保證高效且可伸縮執行的結果快取。

java併發程式設計實戰 構建高效且可伸縮的結果快取

構建高效且可伸縮的結果快取 1,快取在伺服器應用程式中是乙個非常重要的元件。2,以下講解乙個高效且可伸縮的快取示例 如下 public class cachesample futuretaskft new futuretask eval 通過putifabsent完成 先檢查再新增 的原子操作 pu...

構建高效且可伸縮的結果快取引申的併發測試規範化

有些東西其實就是一層紙,當你偶然穿透的時候,就會豁然開朗,眼前一亮。以前總是對一些併發測試不感冒,有時候覺得我從下手的感覺。但是不自己親自測試一把又覺得不放心,於是總是在thread裡面來來回回的修改,在run方法中做這個中修改,總是亂亂的感覺。今天突然從別人的 中頓悟了,體系終於明朗了。我以構建高...

學習如何構建乙個高效且可伸縮的快取

我們平時處理高併發的請求處理時,伺服器的壓力會很大。我們常做的就是構建乙個高效的可伸縮的快取來減輕伺服器的壓力。1.第一次嘗試 分析 使用 hashmap充當cache。public inte ce computable public class memoizer1implements comput...