Java高並發編程(三),java並發編程
目錄:
1.安全執行緒單例模式的幾種實現方式
2.同步容器
3.並發容器
一、安全執行緒單例模式的幾種實現方式
1.餓漢式(不使用同步鎖,典型的用空間換時間)
public class Singleton1 { private static Singleton1 mySingleton = new Singleton1(); private Singleton1(){ System.out.println("single"); } public static Singleton1 getSingle(){ return mySingleton; } public static void main(String[] args) { Thread[] ths = new Thread[200]; for(int i=0; i<ths.length; i++) { ths[i] = new Thread(()->{ Singleton1.getSingle(); }); } Arrays.asList(ths).forEach(o->o.start()); }}
運行結果:
2.懶漢式(使用同步鎖,延時載入,典型的時間換空間)
public class Singleton2 { private static Singleton2 mySingleton; private Singleton2 (){ System.out.println("single"); } public static synchronized Singleton2 getSingle(){ //對擷取執行個體的方法進行同步 if (mySingleton == null) mySingleton = new Singleton2(); return mySingleton; } public static void main(String[] args) { Thread[] ths = new Thread[200]; for(int i=0; i<ths.length; i++) { ths[i] = new Thread(()->{ Singleton2.getSingle(); }); } Arrays.asList(ths).forEach(o->o.start()); }}
運行結果:
3.雙重同步鎖(縮小粒度,雙重檢查
public class Singleton3 { private volatile static Singleton3 mySingleton; private Singleton3 (){ System.out.println("single"); } public static Singleton3 getSingle(){ //對擷取執行個體的方法進行同步 if (mySingleton == null){ synchronized(Singleton3.class){ if (mySingleton == null) mySingleton = new Singleton3(); } } return mySingleton; } public static void main(String[] args) { Thread[] ths = new Thread[200]; for(int i=0; i<ths.length; i++) { ths[i] = new Thread(()->{ Singleton3.getSingle(); }); } Arrays.asList(ths).forEach(o->o.start()); }}
運行結果:
為mySingleton加上volatile關鍵字,以確保能先行發生關係(happens-before relationship),使所有的寫操作都發生於讀操作之前,避免出現在另一個線程中看到一個初始化一半的mySingleton的情況,同時雙重同步鎖的效率也很高
4.使用內部類的單例模式(既不用加鎖,也能實現懶載入)
public class Singleton4 { private Singleton4() { System.out.println("single"); } private static class Inner { private static Singleton4 mySingleton = new Singleton4(); } public static Singleton4 getSingle() { return Inner.mySingleton; } public static void main(String[] args) { Thread[] ths = new Thread[200]; for(int i=0; i<ths.length; i++) { ths[i] = new Thread(()->{ Singleton4.getSingle(); }); } Arrays.asList(ths).forEach(o->o.start()); } }
運行結果:
二、同步容器
1.vector、stack、hashtable等,保證了方法的原子性
2.小例子程式示範同步容器
/** * 有N張火車票,每張票都有一個編號 * 同時有10個視窗對外售票 * 請寫一個類比程式 * * 分析下面的程式可能會產生哪些問題? * 重複銷售?超量銷售? * */package yxxy.c_024;import java.util.ArrayList;import java.util.List;public class TicketSeller1 { static List<String> tickets = new ArrayList<>(); static { for(int i=0; i<10000; i++) tickets.add("票編號:" + i); } public static void main(String[] args) { for(int i=0; i<10; i++) { new Thread(()->{ while(tickets.size() > 0) { System.out.println("銷售了--" + tickets.remove(0)); } }).start(); } }}
運行結果:
出現了越界情況,說明發生了超量銷售與重複銷售的情況
下面使用同步容器,以vector為例再做一遍售票操作
/** * 有N張火車票,每張票都有一個編號 * 同時有10個視窗對外售票 * 請寫一個類比程式 * * 分析下面的程式可能會產生哪些問題? * * 使用Vector或者Collections.synchronizedXXX * 分析一下,這樣能解決問題嗎? * */package yxxy.c_024;import java.util.Vector;import java.util.concurrent.TimeUnit;public class TicketSeller2 { static Vector<String> tickets = new Vector<>(); static { for(int i=0; i<1000; i++) tickets.add("票 編號:" + i); } public static void main(String[] args) { for(int i=0; i<10; i++) { new Thread(()->{ while(tickets.size() > 0) { /* try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }*/ System.out.println("銷售了--" + tickets.remove(0)); } }).start(); } }}
運行結果:
...
從運行結果來看,同步容器似乎已經達到了我們想要的目的,但其中還有一些問題
當我們使用了同步容器時,雖然容器的方法是具有原子性的,但當判斷與操作分離時,中間的部分還是可能會被其他線程所打斷,下面我們類比一下判斷與remove之間有其他的邏輯
/** * 有N張火車票,每張票都有一個編號 * 同時有10個視窗對外售票 * 請寫一個類比程式 * * 分析下面的程式可能會產生哪些問題? * * 使用Vector或者Collections.synchronizedXXX * 分析一下,這樣能解決問題嗎? * */package yxxy.c_024;import java.util.Vector;import java.util.concurrent.TimeUnit;public class TicketSeller2 { static Vector<String> tickets = new Vector<>(); static { for(int i=0; i<1000; i++) tickets.add("票 編號:" + i); } public static void main(String[] args) { for(int i=0; i<10; i++) { new Thread(()->{ while(tickets.size() > 0) { try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("銷售了--" + tickets.remove(0)); } }).start(); } }}
運行結果:
我們看到程式出現了錯誤,所以雖然同步容器保證了方法的原子性,但可能會發生判斷與操作分離,整體操作不具備原子性
三、並發容器
1.並發容器支援多線程
ConcurrentSkipListMap跳錶結構,他是排好序的容器執行插入操作較快
ConcurrentHashMap將容器分成了16段,每次執行插入操作的時候只執行其中的一段,而HashTable為鎖定一整段
ConcurrentLinkedQueue無界隊列,記憶體耗不完的情況下可以一直加(offer,add),拿(poll拿出來刪掉,peek拿出來不刪)
BolckingQueue阻塞式隊列,加(put如果滿了就會等待),拿(take如果空了就會等待)
幾個特殊的阻塞式隊列:
1)TransferQueue轉移隊列,相較於其他隊列,多了一個transfer方法生產者將任務直接發給消費者,不進入結束隊列,在transfer找不到消費者情形下阻塞,即時處理用的較多,隊列還有一定容量
2)DelayQueue計時任務隊列
3)synchronusQueue同步隊列,特殊的transferQueue,沒有容量的隊列,不能調用add方法,只能調用put方法阻塞等待消費者消費,生產者生產的任務直接給消費者消費
還是之前的售票程式,現在改用並發容器以ConcurrentLinkedQueue來重新實驗一遍
/** * 有N張火車票,每張票都有一個編號 * 同時有10個視窗對外售票 * 請寫一個類比程式 * * 使用ConcurrentQueue提高並發性 */package yxxy.c_024;import java.util.Queue;import java.util.concurrent.ConcurrentLinkedQueue;import java.util.concurrent.TimeUnit;public class TicketSeller4 { static Queue<String> tickets = new ConcurrentLinkedQueue<>(); static { for(int i=0; i<1000; i++) tickets.add("票 編號:" + i); } public static void main(String[] args) { for(int i=0; i<10; i++) { new Thread(()->{ while(true) { String s = tickets.poll(); if(s == null) break; else System.out.println("銷售了--" + s); } }).start(); } }}
運行結果:
當我們改用並發容器ConcurrentLinkedQueue時,我們調用poll方法售票直至票數為0時,字串s會變為null,所以在接下來的if判斷後會跳出
當我們在此例中使用了並發容器時,就具有高的效率同時也不會出現問題
四、其他常用同步容器
1.Collocations.synchronizedXXX()將未加鎖的容器加鎖,在並發度不高的情況下可以使用此方法,在並發度較高且需排序的情況下使用concurrentSkipListMap
2.CopyOnWriteList寫時複製連結資料表,在讀取時不加鎖,因此讀取的時候效率高,但寫的效率特別低,應用情境:在需要大量讀取少量寫入時使用
五、總結
1.對於map/set的使用
無線程安全要求:hashmap、treemap、linkedhashmap
並發度低: hashtable、Collections.synchronizedXXX()
並發度高: concurrentHashMap、concurrentSkipListMap
2.list
無線程安全要求:ArrayList、LinkedList
並發度低: Collections.synchronizedXXX()
並發度高: Queue
concurrentLinkedQueue
BlockingQueue
LinkedBQ
ArrayBQ
TransferQueue
synchronusQueue
DelayQueue