Java高併發情況下的鎖機制優化

本文主要講並行優化的幾種方式, 其結構如下:

 

鎖優化

減少鎖的持有時間

例如避免給整個方法加鎖

1     public synchronized void syncMethod(){ 
2         othercode1(); 
3         mutextMethod(); 
4         othercode2(); 
5     }

改進後

複製代碼

複製代碼

1     public void syncMethod2(){ 
2         othercode1(); 
3         synchronized(this){ 
4             mutextMethod(); 
5         } 
6         othercode2(); 
7     }

複製代碼

複製代碼

減小鎖的粒度

將大對象,拆成小對象,大大增加並行度,降低鎖競爭. 如此一來偏向鎖,輕量級鎖成功率提高. 

一個簡單的例子就是jdk內置的ConcurrentHashMap與SynchronizedMap.

Collections.synchronizedMap

其本質是在讀寫map操作上都加了鎖, 在高併發下性能一般.

ConcurrentHashMap

內部使用分區Segment來表示不同的部分, 每個分區其實就是一個小的hashtable. 各自有自己的鎖. 

只要多個修改發生在不同的分區, 他們就可以併發的進行. 把一個整體分成了16個Segment, 最高支持16個線程併發修改. 

代碼中運用了很多volatile聲明共享變量, 第一時間獲取修改的內容, 性能較好.

讀寫分離鎖替代獨佔鎖

顧名思義, 用ReadWriteLock將讀寫的鎖分離開來, 尤其在讀多寫少的場合, 可以有效提升系統的併發能力.

  • 讀-讀不互斥:讀讀之間不阻塞。
  • 讀-寫互斥:讀阻塞寫,寫也會阻塞讀。
  • 寫-寫互斥:寫寫阻塞。

鎖分離

在讀寫鎖的思想上做進一步的延伸, 根據不同的功能拆分不同的鎖, 進行有效的鎖分離.

一個典型的示例便是LinkedBlockingQueue,在它內部, take和put操作本身是隔離的, 

有若干個元素的時候, 一個在queue的頭部操作, 一個在queue的尾部操作, 因此分別持有一把獨立的鎖.

複製代碼

複製代碼

 1     /** Lock held by take, poll, etc */
 2     private final ReentrantLock takeLock = new ReentrantLock();
 3 
 4     /** Wait queue for waiting takes */
 5     private final Condition notEmpty = takeLock.newCondition();
 6 
 7     /** Lock held by put, offer, etc */
 8     private final ReentrantLock putLock = new ReentrantLock();
 9 
10     /** Wait queue for waiting puts */
11     private final Condition notFull = putLock.newCondition();

複製代碼

複製代碼

鎖粗化

通常情況下, 爲了保證多線程間的有效併發, 會要求每個線程持有鎖的時間儘量短, 

即在使用完公共資源後, 應該立即釋放鎖. 只有這樣, 等待在這個鎖上的其他線程才能儘早的獲得資源執行任務.

而凡事都有一個度, 如果對同一個鎖不停的進行請求 同步和釋放, 其本身也會消耗系統寶貴的資源, 反而不利於性能的優化

一個極端的例子如下, 在一個循環中不停的請求同一個鎖.

複製代碼

複製代碼

 1     for(int i = 0; i < 1000; i++){
 2         synchronized(lock){
 3             
 4         }
 5     }
 6 
 7     // 優化後
 8     synchronized(lock){
 9         for(int i = 0;i < 1000; i++){
10             
11         }
12     }

複製代碼

複製代碼

鎖粗化與減少鎖的持有時間, 兩者是截然相反的, 需要在實際應用中根據不同的場合權衡使用.

JDK中各種涉及鎖優化的併發類可以看之前的博文: 併發包總結

ThreadLocal

除了控制有限資源訪問外, 我們還可以增加資源來保證對象線程安全.

對於一些線程不安全的對象, 例如SimpleDateFormat, 與其加鎖讓100個線程來競爭獲取, 

不如準備100個SimpleDateFormat, 每個線程各自爲營, 很快的完成format工作.

示例

複製代碼

複製代碼

 1 public class ThreadLocalDemo {
 2 
 3     public static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal();
 4 
 5     public static void main(String[] args){
 6         ExecutorService service = Executors.newFixedThreadPool(10);
 7         for (int i = 0; i < 100; i++) {
 8             service.submit(new Runnable() {
 9                 @Override
10                 public void run() {
11                     if (threadLocal.get() == null) {
12                         threadLocal.set(new SimpleDateFormat("yyyy-MM-dd"));
13                     }
14 
15                     System.out.println(threadLocal.get().format(new Date()));
16                 }
17             });
18         }
19     }
20 }

複製代碼

複製代碼

原理

對於set方法, 先獲取當前線程對象, 然後getMap()獲取線程的ThreadLocalMap, 並將值放入map中.

該map是線程Thread的內部變量, 其key爲threadlocal, vaule爲我們set進去的值.

複製代碼

複製代碼

1     public void set(T value) {
2         Thread t = Thread.currentThread();
3         ThreadLocalMap map = getMap(t);
4         if (map != null)
5             map.set(this, value);
6         else
7             createMap(t, value);
8     }

複製代碼

複製代碼

對於get方法, 自然是先拿到map, 然後從map中獲取數據.

複製代碼

複製代碼

 1     public T get() {
 2         Thread t = Thread.currentThread();
 3         ThreadLocalMap map = getMap(t);
 4         if (map != null) {
 5             ThreadLocalMap.Entry e = map.getEntry(this);
 6             if (e != null)
 7                 return (T)e.value;
 8         }
 9         return setInitialValue();
10     }

複製代碼

複製代碼

內存釋放

  • 手動釋放: 調用threadlocal.set(null)或者threadlocal.remove()即可
  • 自動釋放: 關閉線程池, 線程結束後, 自動釋放threadlocalmap.

複製代碼

複製代碼

 1 public class StaticThreadLocalTest {
 2 
 3     private static ThreadLocal tt = new ThreadLocal();
 4     public static void main(String[] args) throws InterruptedException {
 5         ExecutorService service = Executors.newFixedThreadPool(1);
 6         for (int i = 0; i < 3; i++) {
 7             service.submit(new Runnable() {
 8                 @Override
 9                 public void run() {
10                     BigMemoryObject oo = new BigMemoryObject();
11                     tt.set(oo);
12                     // 做些其他事情
13                     // 釋放方式一: 手動置null
14 //                    tt.set(null);
15                     // 釋放方式二: 手動remove
16 //                    tt.remove();
17                 }
18             });
19         }
24         // 釋放方式三: 關閉線程或者線程池
25         // 直接new Thread().start()的場景, 會在run結束後自動銷燬線程
26 //        service.shutdown();
27 
28         while (true) {
29             Thread.sleep(24 * 3600 * 1000);
30         }
31     }
32 
33 }
34 // 構建一個大內存對象, 便於觀察內存波動.
35 class BigMemoryObject{
36 
37     List<Integer> list = new ArrayList<>();
38 
39     BigMemoryObject() {
40         for (int i = 0; i < 10000000; i++) {
41             list.add(i);
42         }
43     }
44 }

複製代碼

複製代碼

內存泄露

內存泄露主要出現在無法關閉的線程中, 例如web容器提供的併發線程池, 線程都是複用的.

由於ThreadLocalMap生命週期和線程生命週期一樣長. 對於一些被強引用持有的ThreadLocal, 如定義爲static.

如果在使用結束後, 沒有手動釋放ThreadLocal, 由於線程會被重複使用, 那麼會出現之前的線程對象殘留問題,

造成內存泄露, 甚至業務邏輯紊亂.

對於沒有強引用持有的ThreadLocal, 如方法內變量, 是不是就萬事大吉了呢? 答案是否定的.

雖然ThreadLocalMap會在get和set等操作裏刪除key 爲 null的對象, 但是這個方法並不是100%會執行到.

看ThreadLocalMap源碼即可發現, 只有調用了getEntryAfterMiss後纔會執行清除操作, 

如果後續線程沒滿足條件或者都沒執行get set操作, 那麼依然存在內存殘留問題.

複製代碼

複製代碼

 1     private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal key) {
 2         int i = key.threadLocalHashCode & (table.length - 1);
 3         ThreadLocal.ThreadLocalMap.Entry e = table[i];
 4         if (e != null && e.get() == key)
 5             return e;
 6         else
 7             // 並不是一定會執行
 8             return getEntryAfterMiss(key, i, e);
 9     }
10 
11     private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal key, int i, ThreadLocal.ThreadLocalMap.Entry e) {
12         ThreadLocal.ThreadLocalMap.Entry[] tab = table;
13         int len = tab.length;
14 
15         while (e != null) {
16             ThreadLocal k = e.get();
17             if (k == key)
18                 return e;
19             // 刪除key爲null的value
20             if (k == null)
21                 expungeStaleEntry(i);
22             else
23                 i = nextIndex(i, len);
24             e = tab[i];
25         }
26         return null;
27     }

複製代碼

複製代碼

最佳實踐

不管threadlocal是static還是非static的, 都要像加鎖解鎖一樣, 每次用完後, 手動清理, 釋放對象.

無鎖

與鎖相比, 使用CAS操作, 由於其非阻塞性, 因此不存在死鎖問題, 同時線程之間的相互影響, 

也遠小於鎖的方式. 使用無鎖的方案, 可以減少鎖競爭以及線程頻繁調度帶來的系統開銷.

例如生產消費者模型中, 可以使用BlockingQueue來作爲內存緩衝區, 但他是基於鎖和阻塞實現的線程同步.

如果想要在高併發場合下獲取更好的性能, 則可以使用基於CAS的ConcurrentLinkedQueue. 

同理, 如果可以使用CAS方式實現整個生產消費者模型, 那麼也將獲得可觀的性能提升, 如Disruptor框架.

關於無鎖, 這邊不再贅述, 之前博文已經有所介紹, 具體見: Java高併發之無鎖與Atomic源碼分析

原文鏈接:https://www.cnblogs.com/xdecode/p/9137804.html

 

 


 -END-

標籤: 鎖機制

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章