1 設計模式
(1) 單例模式
保證一個類只能一個對象實現。正常的單例模式分為懶漢式和餓漢式,餓漢式就是把單例聲明稱static a=new A(),系統第一次調用的時候生成(包括調用該類的其他靜態資源也會生成),懶漢式就是系統調用get函數的時候,加個鎖判斷單例對象是否存在,存在就返回不存在就聲明一個。好一點的懶漢式應該把單例加一個靜態內部類,第一次訪問的類的時候靜態內部類不會初始化,當調用的get方法的時候再實例化,這樣不用加鎖效率高一些,
public class StaticSingleton {
private StaticSingleton(){
System.out.println("StaticSingleton is create");
}
private static class SingletonHolder {
private static StaticSingleton instance = new StaticSingleton();
}
public static StaticSingleton getInstance() {
return SingletonHolder.instance;
}
(2)不變模式 類和變量都聲明為final,只要創建就不可變,常見的string Integer Double等都是不可變。
(3) future模式 客戶端請求服務端數據,如果服務端處理時間較長,可以返回一個空值(類似代理),啟動一個線程專門設值。客戶端可以先干別的,當想要試用這個值時,可以從代理里拿,如果代理值已經設置好直接返回,如果沒設置好則wait,等設置好了的時候notify。 可以向excutor里提交一個實現了Collable的對象,會返回一個Future,然后使用這個future.get()拿值。
(4) 生產者消費者模式 專門有生產者生產數據,消費者消費數據,中間靠線程安全的隊列作為公共區域,各線程都從這個區域里寫值和讀值。各個線程無需了解對存在,只要負責自己的事情即可,也符合開閉原則。
2 鎖優化
具體思路:減少鎖持有時間,減小鎖粒度,鎖分離,鎖粗化,鎖消除。
(1)減少鎖持有時間 盡量少的加鎖代碼,例如用具體代碼段代替方法加鎖。
(2)減小鎖粒度 把大對象盡量改成小對象,增加並行度減少鎖競爭。同時有利於偏向鎖,輕量級鎖。例如ConcurrentHashMap
(3)鎖分離 讀寫分離,讀讀可重入,讀寫互斥,寫寫互斥。另一種分離,例如 LinkedBlockingQueue ,存數據和取數據從隊列兩端操作,兩端各自加鎖控制即可,兩端的鎖互不影響。
(4)鎖粗化 如果一段程序要多次請求鎖,鎖之間的代碼執行時間比較少,就應該整合成一個鎖,前提是不用同步的部分執行時間短。例如for循環里面申請鎖,如果for循環時間不長,可以在for外面加鎖。
(5)鎖消除 編譯器級別的操作,如果jdk發現鎖不可能被共享,會擦除這個鎖。原理是逃逸分析,例如stringbuffer,本身操作是加鎖的,如果只在局部使用不存在並發訪問,那么會擦除鎖,如果對象逃逸出去例如賦值給全局變量等,面臨並發訪問,就不會擦除鎖。可以通過jvm參數來指定是否使用鎖消除。
3 jdk的鎖優化 sychronized的優化,由虛擬機完成
(1)偏向鎖 在競爭比較少的情況下,會使用偏向鎖來提高性能。
*對象頭 markword,共32位,存hash,鎖信息(指向鎖的指針),垃圾回收標志(偏向鎖id),年齡信息,偏向鎖線程id,monitor信息等。
一個線程爭取到對象資源時,對象會在對象頭中標記為偏向,並且將線程id寫入到對象頭中,下次如果這個線程再來可以不通過鎖競爭直接進入同步塊。當其他線程訪問的時候,偏向結束,升級為輕量級鎖。所以在競爭激烈的場景下偏向鎖會增加系統負擔,jvm默認是開啟偏向鎖的,可以通過jvm參數設置取消偏向鎖
*偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令,在只有一個線程執行同步塊時進一步提高性能。
(2)輕量級鎖 輕量級鎖所適應的場景是線程交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖。
輕量級鎖的加鎖過程 :
1)在代碼進入同步塊的時候,如果同步對象鎖狀態為無鎖狀態(偏向鎖也是無鎖),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝。
2)拷貝對象頭中的Mark Word復制到鎖記錄中。
3)拷貝成功后,虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,並將Lock record里的owner指針指向object mark word。
4)如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標志位設置為處於輕量級鎖定狀態。
5)如果這個更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹為重量級鎖,鎖標志的狀態值變為“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態。 而當前線程便嘗試使用自旋來獲取鎖,自旋就是為了不讓線程阻塞,而采用循環去獲取鎖的過程。
輕量級鎖解鎖時,把復制的對象頭替換回去(cas)如果替換成功,鎖結束,如果失敗,說明有競爭,升級為重量級鎖(先會自旋一下等等看),notify 喚醒其他等待線程。
* 輕量級鎖是為了在線程交替執行同步塊時提高性能。
(3)自旋鎖
輕量級鎖加鎖失敗以后,可能先自旋一段時間,嘗試獲得輕量級鎖,不會着急升級為重量級鎖掛起。如果自旋過多,會造成cpu資源浪費,JDK采用了適應性自旋,簡單來說就是一開始設置固定自旋次數,線程如果自旋成功了,則下次自旋的次數會更多,如果自旋失敗了,則自旋的次數就會減少。
*自旋如果成功,可以省略線程掛起的時間。jdk7以后默認使用。
3 jdk8新特性
(1)LongAdder 類似automicLong, 但是提供了“熱點分離”。過程如下:如果並發不激烈,則與automicLong 一樣,cas賦值。如果出現並發操作,則使用數組,數組的各元素之和為真實value,讓操作分散在數組各個元素上,把並發操作壓力分散,一遇到並發就擴容數組,最后達到高效率。一般cas如果遇到高並發,可能一直賦值失敗導致不斷循環,熱點分離可以解決這個問題。有點類似concurrenthashmap,分而治之。
(2)completableFuture 對Future進行增強,支持函數式編程的流式調用。提供更多功能,壓縮編碼量。
(3)stampedLock 改進讀寫鎖,讀不阻塞寫。如果讀的時候,發生了寫,應該重新讀,不是阻塞寫。解決了一般讀寫鎖讀太多導致寫一直阻塞的問題,讀線程發現數據不一致時觸發重新讀操作。 原理是維護了一個stamp標記,在添加寫鎖的釋放寫鎖的時候,stamp都會改變(比如++),代碼在加讀鎖的時候,可以先得到stamp,讀完數據釋放讀鎖的時候,調用validate方法,檢驗剛才stamp和現在stamp是否相同,如果相同,說明讀的過程中沒有修改,讀取成功,如果不相同,則說明讀的時候發生了寫,那么接下來兩種策略,一個是繼續用當前stamp為初試,繼續讀,讀完比較stamp,是樂觀的辦法;另一種直接調用readlock(),升級為正常的讀鎖,是悲觀辦法。