本文主要講並行優化的幾種方式, 其結構以下:html
例如避免給整個方法加鎖web
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操做上都加了鎖, 在高併發下性能通常.ide
ConcurrentHashMap高併發
內部使用分區Segment來表示不一樣的部分, 每一個分區其實就是一個小的hashtable. 各自有本身的鎖. 源碼分析
只要多個修改發生在不一樣的分區, 他們就能夠併發的進行. 把一個總體分紅了16個Segment, 最高支持16個線程併發修改. post
代碼中運用了不少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中各類涉及鎖優化的併發類能夠看以前的博文: 併發包總結
除了控制有限資源訪問外, 咱們還能夠增長資源來保證對象線程安全.
對於一些線程不安全的對象, 例如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 }
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源碼分析