Java多線程核心技術(五)單例模式與多線程
本文只需要考慮一件事:如何使單例模式遇到多線程是安全的、正確的
1.立即加載 / "餓漢模式"
什麽是立即加載?立即加載就是使用類的時候已經將對象創建完畢,常見的實現辦法就是直接 new 實例化。
public class MyObject { private static MyObject myObject = new MyObject(); public MyObject(){ } public static MyObject getInstance(){ return myObject; } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } }
打印結果:
985396398
985396398
985396398
控制臺打印的 hashCode 是同一個值,說明對象是同一個,也就實現了立即加載型單例設計模式。
此版本的缺點是不能有其他其他實例變量,因為getInstance()方法沒有同步,所以有可能出現非線程安全問題。
2.延遲加載 / "懶漢模式"
什麽是延遲加載?延遲加載就是在調用 get() 方法時實例才被創建,常見的實現方法就是在 get() 方法中進行 new() 實例化。
測試代碼:
public class MyObject { private static MyObject myObject; public MyObject() { } public static MyObject getInstance() { try { if (myObject == null) { //模擬對象在創建之前做的一些準備工作 Thread.sleep(3000); myObject = new MyObject(); } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } }
打印結果:
985396398
610025186
21895028
從運行結果來看,創建了三個對象,並不是真正的單例模式。原因顯而易見,3個線程同時進入了if (myObject == null)
判斷語句中,最後各自都創建了對象。
3.延遲加載解決方案
3.1 聲明synchronized關鍵字
既然多個線程可以同時進入getInstance() 方法,那麽只需要對其進行同步synchronized處理即可。
public class MyObject { private static MyObject myObject; public MyObject() { } synchronized public static MyObject getInstance() { try { if (myObject == null) { //模擬對象在創建之前做的一些準備工作 Thread.sleep(3000); myObject = new MyObject(); } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } }
打印結果:
961745937
961745937
961745937
雖然運行結果表明,成功實現了單例,但這種給整個方法上鎖的解決方法效率太低。
3.2 嘗試同步 synchronized 代碼塊
同步方法是對方法的整體加鎖,這對運行效率來講很不利的。改成同步代碼塊後:
public class MyObject {
private static MyObject myObject;
public MyObject() {
}
public static MyObject getInstance() {
try {
synchronized (MyObject.class) {
if (myObject == null) {
//模擬對象在創建之前做的一些準備工作
Thread.sleep(3000);
myObject = new MyObject();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
}
}
打印結果:
355159803
355159803
355159803
運行結果雖然表明是正確的,但同步synchronized語句塊依舊把整個 getInstance()方法代碼包括在內,和synchronize 同步方法效率是一樣低下。
3.3 針對某些重要的代碼進行單獨同步
所以,我們可以針對某些重要的代碼進行單獨的同步,而其他的代碼則不需要同步。這樣在運行時,效率完全可以得到大幅度提升。
public class MyObject {
private static MyObject myObject;
public MyObject() {
}
public static MyObject getInstance() {
try {
if (myObject == null) {
//模擬對象在創建之前做的一些準備工作
Thread.sleep(3000);
synchronized (MyObject.class) {
myObject = new MyObject();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
}
}
運行結果:
985396398
21895028
610025186
此方法只對實例化對象的關鍵代碼進行同步,從語句的結構上來說,運行的效率的確得到的提升。但是在多線程的情況下依舊無法解決得到一個單例對象的結果。
3.4 使用DCL雙檢查鎖機制
在最後的步驟中,使用DCL雙檢查所=鎖機制來實現多線程環境中的延遲加載單例設計模式。
public class MyObject {
private volatile static MyObject myObject;
public MyObject() {
}
public static MyObject getInstance() {
try {
if (myObject == null) {
//模擬對象在創建之前做的一些準備工作
Thread.sleep(3000);
synchronized (MyObject.class) {
if (myObject == null) {
myObject = new MyObject();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
}
}
運行結果:
860826410
860826410
860826410
使用DCL雙重檢查鎖功能,成功地解決了“懶漢模式”遇到多線程的問題。DCL也是大多數多線程結合單例模式使用的解決方案。
4.使用靜態內置類實現單例模式
DCL可以解決多線程單例模式的非線程安全問題。當然,還有許多其它的方法也能達到同樣的效果。
public class MyObject {
public static class MyObjectHandle{
private static MyObject myObject = new MyObject();
public static MyObject getInstance() {
return myObject;
}
}
public static MyObject getInstance(){
return MyObjectHandle.getInstance();
}
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
}
}
打印結果:
1035057739
1035057739
1035057739
靜態內置類可以達到線程安全問題,但如果遇到序列化對象時,使用默認的方式運行得到的結果還是多例的。
解決方法就是在反序列化中使用readResolve()方法:
public class MyObject implements Serializable {
//靜態內部類
public static class MyObjectHandle{
private static final MyObject myObject = new MyObject();
}
public static MyObject getInstance(){
return MyObjectHandle.myObject;
}
protected Object readResolve(){
System.out.println("調用了readResolve方法");
return MyObjectHandle.myObject;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
MyObject myObject = MyObject.getInstance();
FileOutputStream outputStream = new FileOutputStream(new File("myObject.txt"));
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(myObject);
objectOutputStream.close();
System.out.println(myObject.hashCode());
FileInputStream inputStream = new FileInputStream(new File("myObject.txt"));
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
MyObject object = (MyObject) objectInputStream.readObject();
objectInputStream.close();
System.out.println(object.hashCode());
}
}
運行結果:
621009875
調用了readResolve方法
621009875
5.使用static代碼塊實現單例模式
靜態代碼塊中的代碼在使用類的時候就已經執行了,所以可以應用靜態代碼塊的這個特點來實現單例設計模式。
public class MyObject {
private static MyObject myObject = null;
static {
myObject = new MyObject();
}
public static MyObject getInstance(){
return myObject;
}
public static void main(String[] args){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
}
}
運行結果:
355159803
355159803
355159803
6.使用enum枚舉數據類型實現單例模式
枚舉enum 和靜態代碼塊的特性相似,在使用枚舉類時,構造方法會被自動調用,也可以應用其這個特性實現單例設計模式。
public enum Singleton {
INSTANCE;
private MyObject myObject = null;
Singleton() {
myObject = new MyObject();
}
public MyObject getInstance(){
return myObject;
}
public static void main(String[] args){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Singleton.INSTANCE.getInstance().hashCode());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Singleton.INSTANCE.getInstance().hashCode());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Singleton.INSTANCE.getInstance().hashCode());
}
}).start();
}
}
運行結果:
1516133987
1516133987
1516133987
這樣實現的一個弊端就是違反了“職責單一原則”,完善後的代碼如下:
public class MyObject {
public enum Singleton {
INSTANCE;
private MyObject myObject = null;
Singleton() {
myObject = new MyObject();
}
public MyObject getInstance() {
return myObject;
}
}
public static MyObject getInstance(){
return Singleton.INSTANCE.getInstance();
}
public static void main(String[] args){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
}
}
運行結果:
610025186
610025186
610025186
7.文末總結
本文使用若幹案例來闡述單例模式與多線程結合遇到的情況與解決方案。
參看
《Java多線程編程核心技術》高洪巖著
擴展
Java多線程編程核心技術(一)Java多線程技能
Java多線程編程核心技術(二)對象及變量的並發訪問
Java多線程編程核心技術(三)多線程通信
Java多線程核心技術(四)Lock的使用
Java多線程核心技術(五)單例模式與多線程