執行緒安全的單例模式也可以很精彩
這篇部落格以多種方式實現單例模式,包括非執行緒安全、執行緒安全的單例模式以及執行緒安全的優化。
餓漢式單例;
懶漢式單例(延遲初始化);(執行緒不安全)
執行緒安全的單例-synchronized方法
執行緒安全的單例-同步程式碼塊
執行緒安全的單例-顯式鎖ReentrantLock進一步減小鎖的粒度
執行緒安全的單例-DCL(double check lock)
首先可以參考一下我的另外一篇博文了解什麼是單例模式:
單例模式
多執行緒測試獲取單例例項
為了模擬在多執行緒的高併發下同時獲取單例中是例項的情況,我寫了一個測試用例,使用了J.U.C 併發包下的欄珊,使用欄珊的目的是為了讓100個執行緒到達欄珊後互相等待,知道全部到達欄珊然後開放出口,100個執行緒同時發起獲取單例。具體的程式碼如下:
package patterns.singleton_multi;
import java.util.Random;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Created by louyuting on 17/1/22.
* 多執行緒測試端-使用欄珊實現100各執行緒同時申請建立單例
*/
public class TestClient {
private static final Integer THREAD_NUM = 100;
static class NewInstance implements Runnable{
private String name;
private CyclicBarrier barrier;//欄珊
public NewInstance(CyclicBarrier barrier, String name) {
super();
this .barrier = barrier;
this.name = name;
}
@Override
public void run() {
try {
//做個延時
TimeUnit.MILLISECONDS.sleep(1000 * (new Random()).nextInt(8));
System.out.println(name + " 準備好了...");
//barrier的await方法,在所有參與者都已經在此 barrier 上呼叫 await 方法之前,將一直等待。
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(name + "的例項的hashcode:"+ Singleton1.getInstance().hashCode());
}
}
/**
* 測試類的主函式
* @param args
*/
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(THREAD_NUM, new Runnable() {
@Override
public void run() {
System.out.println("--------------提示:所有執行緒都已經到達.可以執行開始獲取單例了.--------------------");
}
});
ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUM);
for(int i=0; i<THREAD_NUM; i++){
if(i<10){
executor.submit(new NewInstance(barrier, "thread-0"+i));
}else{
executor.submit(new NewInstance(barrier, "thread-"+i));
}
}
executor.shutdown();
}
}
1.餓漢式單例
餓漢式單例是指在方法呼叫前,例項就已經建立好了,所以肯定是執行緒安全的。下面是實現程式碼:
package patterns.singleton_multi;
/**
* Created by louyuting on 17/1/22.
* 餓漢式單例模式
*/
public class Singleton1 {
//單例
private static Singleton1 instance = new Singleton1();
//構造器私有,不開放
private Singleton1(){
}
public static Singleton1 getInstance(){
return instance;
}
}
使用測試用例的測試結果如下:
從執行結果可以看出例項變數額hashCode值一致,這說明物件是同一個,餓漢式單例實現了。
2.懶漢式單例(延遲初始化);
懶漢式單例是指在方法呼叫獲取例項時才建立例項,因為相對餓漢式顯得“不急迫”,所以被叫做“懶漢模式”。下面是實現程式碼:
package patterns.singleton_multi;
import java.util.concurrent.TimeUnit;
/**
* Created by louyuting on 17/1/22.
* 懶漢式單例(延遲初始化); 多執行緒下完全不安全
*/
public class Singleton2 {
//單例
private static Singleton2 instance;
//構造器私有,不開放
private Singleton2(){
}
public static Singleton2 getInstance(){
if(instance == null){
//建立例項前一些耗時操作
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Singleton2();
}
return instance;
}
}
測試用例測試結果如下:
從執行結果可以看出例項變數的hashCode值完全一致,這說明物件不是同一個,所以是執行緒不安全的。
3.執行緒安全的單例-synchronized方法
上面的懶漢式單例明顯是執行緒不安全的,要想實現執行緒安全的實現,最簡單的方式就是使用同步原語實現synchronized方法。出現非執行緒安全問題,是由於多個執行緒可以同時進入getInstance()方法,那麼只需要對該方法進行synchronized的鎖同步即可:
原始碼如下:
package patterns.singleton_multi;
import java.util.concurrent.TimeUnit;
/**
* Created by louyuting on 17/1/22.
* synchronized 同步方法實現執行緒安全
*/
public class Singleton3 {
//單例
private static Singleton3 instance;
//構造器私有,不開放
private Singleton3(){
}
public synchronized static Singleton3 getInstance(){
if(instance == null){
//建立例項前一些耗時操作
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Singleton3();
}
return instance;
}
}
測試用例的結果是:
很顯然實現了執行緒安全。但是這種實現方式的執行效率會很低。同步方法效率低,那我們考慮使用同步程式碼塊來實現:
4.執行緒安全的單例-同步程式碼塊
原始碼如下:
package patterns.singleton_multi;
import java.util.concurrent.TimeUnit;
/**
* Created by louyuting on 17/1/22.
* synchronized 同步程式碼塊實現執行緒安全
*/
public class Singleton4 {
//單例
private static Singleton4 instance;
//構造器私有,不開放
private Singleton4(){
}
public static Singleton4 getInstance(){
//建立例項前一些耗時操作
try {
synchronized (Singleton4.class){
if(instance==null){
TimeUnit.MILLISECONDS.sleep(300);
instance = new Singleton4();
}else {
//instance非空
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}
執行結果如下:
同步程式碼塊雖然提交了效率,但是同步程式碼塊把方法內所有的程式碼全部包含在內,效率依然低,還可以優化,接下來我們繼續減小鎖的粒度,使用顯式鎖ReentrantLock進一步減小鎖的粒度
5.執行緒安全的單例-顯式鎖ReentrantLock
我們使用顯式鎖ReentrantLock只鎖住關鍵的程式碼行,原始碼如下:
package patterns.singleton_multi;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by louyuting on 17/1/22.
* synchronized 同步程式碼塊實現執行緒安全
*/
public class Singleton5 {
//單例
private static Singleton5 instance;
private static Lock lock = new ReentrantLock();
//構造器私有,不開放
private Singleton5(){
}
public static Singleton5 getInstance(){
//建立例項前一些耗時操作
try {
lock.lock();//加鎖
if(instance==null){
TimeUnit.MILLISECONDS.sleep(300);
instance = new Singleton5();
}else {
//instance非空
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
return instance;
}
}
測試結果如下:
在小粒度下同樣實現了執行緒安全,那麼還能不能優化呢?當然是可以的,我們知道volatile是最輕量級的鎖機制了,那能不能利用volatile來實現呢?接下來就提出了DCL方法。
6.執行緒安全的單例-DCL(double check lock)
為了達到執行緒安全,又能提高程式碼執行效率,我們這裡可以採用DCL的雙檢查鎖機制來完成,DCL的實現將鎖的粒度降低到了最小,僅僅加鎖new例項這一行程式碼,通過最輕量級的volatile實現多執行緒情況下的記憶體可見性。程式碼實現如下:
package patterns.singleton_multi;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by louyuting on 17/1/22.
* synchronized DCl
*/
public class Singleton6 {
//使用volatile關鍵字保其可見性
volatile private static Singleton6 instance;
private static Lock lock = new ReentrantLock();
//構造器私有,不開放
private Singleton6(){
}
public static Singleton6 getInstance(){
//建立例項前一些耗時操作
try {
if(instance==null){
lock.lock();//加鎖
if(instance == null){//二次檢查
TimeUnit.MILLISECONDS.sleep(300);
instance = new Singleton6();
}
lock.unlock();
}else {
//instance非空
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
}
return instance;
}
}
檢視測試結果:
從執行結果來看,該中方法保證了多執行緒併發下的執行緒安全性。
這裡在宣告變數時使用了volatile關鍵字來保證其執行緒間的可見性;在同步程式碼塊中使用二次檢查,以保證其不被重複例項化。集合其二者,這種實現方式既保證了其高效性,也保證了其執行緒安全性。
自此,優化過程結束了,如果有路過的大佬有更好的優化策略,歡迎私信交流。我今天也是被師兄問到了這個,被吊打才有了這篇博文,歡迎各路大神一起來學習。
本文全部程式碼均可通過github獲得,github地址。