這是閱讀《Java編髮編程實戰》這本Java多線程領域的寶典書籍的自我總結與融匯貫通的過程。現在看到了第二部分的第七章,我自己先在我們幾個人中,做一個開頭,把自己學習到的分享出來。現在只是多線程原子性總結了出來,今天陸續吧可見性和不變性都總結出來,貼上來。這些學習,都算是基礎夯實的過程,再多的框架,... ...
混混噩噩看了很多多線程的書籍,一直認為自己還不夠資格去閱讀這本書。有種要高登大堂的感覺,被各種網路上、朋友、同事一頓外加一頓的宣傳與傳頌,多多少少再自我內心中產生了一種敬畏感。2月28好開始看了之後,發現,其實完全沒這個必要。除了翻譯的爛之外(一大段中文下來,有時候你就會罵娘:這tm想說的是個shen me gui),所有的,多線程所必須掌握的知識點,深入點,全部涵蓋其中,只能說,一書在手,萬線程不愁那種!當然,你必須要全部讀懂,並融匯貫通之後,才能有的效果。我推薦,看這本書的中文版本,不要和哪一段的冗長的字句在那過多的糾纏,儘量一段一段的讀,然後獲取這一段中最重要的那句話,否則你會陷入中文閱讀理解的怪圈,而懷疑你的高中語文老師是不是體育老師客串的!!我舉個例子:13頁第八段,我整段讀了三遍硬是沒想明白前面那麼多的文字,是乾什麼用的,就是最後一句話才是核心:告訴你,線程安全性,最正規的定義應該是什麼!(情允許我,向上交的幾個翻譯此書的,所謂的“教授”致敬,在你們的引領下,使我們的意志與忍受力更上了一個臺階,人生更加完美!)
一、多線程開發所要平衡的幾個點
看了很多次的目錄,外加看了第一部分,發現,要想做好多線程的開發,無非就是平衡好以下的幾點
- 安全性
- 活躍性
- 無限迴圈問題
- 死鎖問題
- 饑餓問題
- 活鎖問題(這個還沒具體的瞭解到)
- 性能要求
- 吞吐量的問題
- 可伸縮性的問題
二、多線程開發所要關註的開發點
要想平很好以上幾點,書中循序漸進的將多線程開發最應該修煉的幾個點,娓娓道來:
- 原子性
- 先檢查後執行
- 原子類
- 加鎖機制
- 可見性
- 重排
- 非64位寫入問題
- 對象的發佈
- 對象的封閉
- 不變性
在一本國人自己寫的,介紹線程工具api的書中,看到了這麼一句話:外練原子,內練可見。感覺這幾點如果在多線程中尤為重要。我在有贊,去年還記得上線多門店的那天凌晨,最後項目啟動報一個類載入的錯誤,一堆人過來看問題,基德大神站在攀哥的後面,最後淡淡的說了句:已經很明顯是可見性問題了,加上volatile,不行的話,我把代碼吃了!!可以見得,多線程這幾個點,在“居家旅行”,生活工作中是多麼的常見與重要!不出問題不要緊,只要一齣,就會是頭痛的大問題,因為你根本不好排查根本原因在這。所以我們需要平時就練好功底,儘量避免多線程問題的出現!而不是一味的用框架啊用框架、摞代碼啊摞代碼!
三、原子性下麵的安全問題
1. 下麵代碼有什麼問題呢?
public class UnsafeConuntingFactorizer implements Servlet{
private long count = 0;
private long getCount(){
return count;
}
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp,factor);
}
}
思考:如何讓一個普普通通的類變得線程安全呢?一個類什麼叫做有狀態,而什麼又叫做無狀態呢?
2. 解答上面代碼
- 一個請求的方法,實例都是一個,所以每次請求都會訪問同一個對象
- 每個請求,使用一個線程,這就是典型的多線程模型
- count是一個對象狀態屬性,被多個線程共用
++count
並非一次原子操作(分成:複製count->對複製體修改->使用複製體回寫count,三個步奏)- 多個線程有可能多次修改count值,而結果卻相同
3. 使用原子類解決上面代碼問題
public class UnsafeConuntingFactorizer implements Servlet{
private final AtomicLong count = new AtomicLong(0);
private long getCount(){
return count.get();
}
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();//使用了新的原子類的原子方法
encodeIntoResponse(resp,factor);
}
}
4. 原子類也不是萬能的
//在複雜的場景下,使用多個原子類的對象
public class UnsafeConuntingFactorizer implements Servlet{
private final AtomicReference<BigInteger> lastNumber
= new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors
= new AtomicReference<BigInteger[]>();
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
if(i.equals(lastNumber.get())){//先判斷再處理,並沒有進行同步,not safe!
encodeIntoResponse(resp,lastFactors.get());
}else{
BigInteger[] factors = factor(i);
lastNumer.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}
思考:什麼叫做複合型操作?
5. 先列舉一個我們常見的複合型操作
public class LazyInitRace {
private ExpensiveObject instace = null;
public ExpensiveObject getInstace(){
if(instace == null){
instace = new ExpensiveObject();
}
return instace;
}
}
看好了,這就是我們深惡痛絕的一段代碼!如果這段代碼還分析不了的,對不起,出門左轉~
6. 提高“先判斷再處理”的警覺性
- 如果沒有同步措施,直接對一個狀態進行判斷,然後設值的,都是不安全的
- if操作和下麵代碼快中的代碼,遠遠不是原子的
- 如果if判斷完之後,接下來線程掛起,其他線程進入判斷流程,又是同樣的狀態,同樣進入if語句塊
- 當然,只有一個線程執行的程式,請忽略(那還叫能用的程式嗎?)
7. 性能的問題來了
//在複雜的場景下,使用多個原子類的對象
public class UnsafeConuntingFactorizer implements Servlet{
private final AtomicReference<BigInteger> lastNumber
= new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors
= new AtomicReference<BigInteger[]>();
//這下子總算同步了!
public synchronized void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
if(i.equals(lastNumber.get())){//先判斷再處理,並沒有進行同步,not safe!
encodeIntoResponse(resp,lastFactors.get());
}else{
BigInteger[] factors = factor(i);
lastNumer.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}
思考:有沒有種“關公揮大刀,一砍一大片”的感覺?
8. 上訴代碼解析
- 加上了
synchronized
關鍵字的確解決了多線程訪問,類安全性問題 - 可是每次都是一個線程進行計算,所有請求變成了串列
- 請求量低於100/s其實都還能接受,可是再高的話,這就完全有問題的代碼了
- 性能問題,再網路裡面,是永痕的心病~
9. 一段針對原子性、性能問題的解決方案
//在複雜的場景下,使用多個原子類的對象
public class CacheFactorizer implements Servlet{
private BigInteger lastNumber;
private BigInteger[] lastFactors ;
private long hits;
private long cacheHits;
public synchronized long getHits(){
return hits;
}
public synchronized double getCacheHitRadio(){
return (double) cacheHits / (double) hits;
}
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = null;
synchronized (this){
++hits;
if(i.equals(lastNumber)){
++cacheHits;
factors = lastFactors.clone();
}
}
if (factors == null){
factors = factor(i);
synchronized (this){
lastNumer = i;
lastFactors = factors.clone();
}
}
encodeIntoResponse(resp, factors);
}
}
在修改狀態值的時候,才進行加鎖,平時對狀態值的讀操作可以不用加鎖,當然,最耗時的計算過程,也是要同步的,這種情況下,才會進一步提高性能。