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
鎖粗化與減少鎖的持有時間, 兩者是截然相反的, 需要在實際應用中根據不同的場合權衡使用.
JDK中各種涉及鎖優化的并發類可以看之前的博文:并發包總結
ThreadLocal
除了控制有限資源訪問外, 我們還可以增加資源來保證對象線程安全.
對于一些線程不安全的對象, 例如SimpleDateFormat, 與其加鎖讓100個線程來競爭獲取,
不如準備100個SimpleDateFormat, 每個線程各自為營, 很快的完成format工作.
示例
1 public class ThreadLocalDemo { 2 3 public static ThreadLocal threadLocal = new ThreadLocal(); 4 5 public static void main(String[] args){ 6 ExecutorService service = Executors.newFixedThreadPool(10); 7 for (int i = 0; i原理對于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 else7 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 list = new ArrayList();38 39 BigMemoryObject() {40 for (int i = 0; i內存泄露內存泄露主要出現在無法關閉的線程中, 例如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的value20 if (k == null)21 expungeStaleEntry(i);22 else23 i = nextIndex(i, len);24 e = tab[i];25 }26 return null;27 }最佳實踐不管threadlocal是static還是非static的, 都要像加鎖解鎖一樣, 每次用完后, 手動清理, 釋放對象.無鎖與鎖相比, 使用CAS操作, 由于其非阻塞性, 因此不存在死鎖問題, 同時線程之間的相互影響,也遠小于鎖的方式. 使用無鎖的方案, 可以減少鎖競爭以及線程頻繁調度帶來的系統開銷.例如生產消費者模型中, 可以使用BlockingQueue來作為內存緩沖區, 但他是基于鎖和阻塞實現的線程同步.如果想要在高并發場合下獲取更好的性能, 則可以使用基于CAS的ConcurrentLinkedQueue.同理, 如果可以使用CAS方式實現整個生產消費者模型, 那么也將獲得可觀的性能提升, 如Disruptor框架.
總結
以上是生活随笔為你收集整理的Java高并发之锁优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JAVA并发编程: CAS和AQS
- 下一篇: 当我们在谈论HTTP缓存时我们在谈论什么