本文的知識(shí)點(diǎn)非常重要窟社,通過(guò)單例模式與多線程技術(shù)相結(jié)合,在這個(gè)過(guò)程中能發(fā)現(xiàn)很多以前未考慮過(guò)的情況手幢,一些不良的程序設(shè)計(jì)方法如果應(yīng)用在商業(yè)項(xiàng)目中,將會(huì)遇到非常大的麻煩娃闲。本文的案例也將充分說(shuō)明虚汛,線程與某些技術(shù)相結(jié)合時(shí)要考慮的事情有很多。在學(xué)習(xí)本文時(shí)只需要考慮一件事情皇帮,那就是:如何使單例模式遇到多線程是安全的卷哩、正確的。
在標(biāo)準(zhǔn)的23個(gè)設(shè)計(jì)模式中属拾,單例設(shè)計(jì)模式在應(yīng)用中是比較常見(jiàn)的将谊。但在常規(guī)的該模式教學(xué)資料介紹中,多數(shù)并沒(méi)有結(jié)合多線程技術(shù)作為參考渐白,這就造成在使用多線程技術(shù)的單例模式時(shí)會(huì)出現(xiàn)一些意想不到的情況尊浓,這樣的代碼如果在生產(chǎn)環(huán)境中出現(xiàn)異常,有可能造成災(zāi)難性的后果纯衍。本文將介紹單例模式結(jié)合多線程技術(shù)在使用時(shí)的相關(guān)知識(shí)栋齿。
1. 立即加載/“餓漢模式”
什么是立即加載?立即加載就是使用類(lèi)的時(shí)候已經(jīng)將對(duì)象創(chuàng)建完畢襟诸,常見(jiàn)的實(shí)現(xiàn)辦法就是直接new實(shí)例化瓦堵。而立即加載從中文的語(yǔ)境來(lái)看,有“著急”歌亲、“急迫”的含義菇用,所以也稱(chēng)為“餓漢模式”。
立即加載/“餓漢模式”是在調(diào)用方法前陷揪,實(shí)例已經(jīng)被創(chuàng)建了惋鸥,來(lái)看一下實(shí)現(xiàn)代碼。
創(chuàng)建測(cè)試用的項(xiàng)目鹅龄,創(chuàng)建類(lèi)MyObject.java代碼如下:
public class MyObject {
// 立即加載方式==餓漢模式
private static MyObject myObject = new MyObject();
private MyObject() {
}
public static MyObject getInstance() {
// 此代碼版本為立即加載
// 此版本代碼的缺點(diǎn)是不能有其它實(shí)例變量
// 因?yàn)間etInstance()方法沒(méi)有同步
// 所以有可能出現(xiàn)非線程安全問(wèn)題
return myObject;
}
}
創(chuàng)建線程類(lèi)MyThread.java代碼如下:
import test.MyObject;
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
創(chuàng)建運(yùn)行類(lèi)Run.java代碼如下:
import extthread.MyThread;
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
程序運(yùn)行后的結(jié)果如下:
1031548957
1031548957
1031548957
控制臺(tái)打印的hashCode是同一個(gè)只揩慕,說(shuō)明對(duì)象是同一個(gè),也就實(shí)現(xiàn)了立即加載型單例設(shè)計(jì)模式扮休。
2. 延遲加載/“懶漢模式”
什么是延遲加載?延遲加載就是在調(diào)用get()方法時(shí)實(shí)例才被創(chuàng)建拴鸵,常見(jiàn)的實(shí)現(xiàn)辦法就是在get()方法中進(jìn)行new實(shí)例化玷坠。而延遲加載從中文的語(yǔ)境來(lái)看,是“緩慢”劲藐、“不急迫”的含義八堡,所以也稱(chēng)為“懶漢模式”。
2.1 延遲加載/“懶漢模式”解析
延遲加載/“懶漢模式”是在調(diào)用方法時(shí)實(shí)例才被創(chuàng)建聘芜。一起來(lái)看一下實(shí)現(xiàn)代碼兄渺。
創(chuàng)建類(lèi)MyObject.java代碼如下:
public class MyObject {
private static MyObject myObject;
private MyObject() {
}
public static MyObject getInstance() {
// 延遲加載
if (myObject != null) {
} else {
myObject = new MyObject();
}
return myObject;
}
}
創(chuàng)建線程類(lèi)MyThread.java代碼如下:
import test.MyObject;
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
創(chuàng)建運(yùn)行類(lèi)Run.java代碼如下:
import extthread.MyThread;
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
}
}
程序運(yùn)行后的效果如下:
1031548957
此實(shí)驗(yàn)雖然取得一個(gè)對(duì)象的實(shí)例,但如果是在多線程的環(huán)境中汰现,就會(huì)出現(xiàn)去除多個(gè)實(shí)例的情況挂谍,與單例模式的初衷是相背離的叔壤。
2.2 延遲加載/“懶漢模式”的缺點(diǎn)
前面兩個(gè)實(shí)驗(yàn)雖然使用“立即加載”和“延遲加載”實(shí)現(xiàn)了單例設(shè)計(jì)模式,但在多線程的環(huán)境中口叙,前面“延遲加載”示例中的代碼完全就是錯(cuò)誤的炼绘,根本補(bǔ)鞥呢實(shí)現(xiàn)保持單例的狀態(tài)。來(lái)看一下如何在多線程環(huán)境中結(jié)合“錯(cuò)誤的單例模式”創(chuàng)建出“多例”妄田。
創(chuàng)建類(lèi)MyObject.java代碼如下:
public class MyObject {
private static MyObject myObject;
private MyObject() {
}
public static MyObject getInstance() {
try {
if (myObject != null) {
} else {
// 模擬在創(chuàng)建對(duì)象之前做一些準(zhǔn)備性的工作
Thread.sleep(3000);
myObject = new MyObject();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
}
創(chuàng)建線程類(lèi)MyThread.java代碼如下:
import test.MyObject;
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
創(chuàng)建運(yùn)行類(lèi)Run.java代碼如下:
import extthread.MyThread;
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
程序運(yùn)行后的效果如下:
186668687
903341402
1770731361
控制臺(tái)打印出了3中hashCode俺亮,說(shuō)明創(chuàng)建了3個(gè)對(duì)象,并不是單例的疟呐,這就是“錯(cuò)誤的單例模式”脚曾。如何解決呢?先看一下解決方案启具。
2.3 延遲加載/“懶漢模式”的解決方案
2.3.1 聲明synchronized關(guān)鍵字
既然多個(gè)線程可以同時(shí)進(jìn)入getInstance()方法斟珊,那么只需要對(duì)getInstance()方法聲明synchronized關(guān)鍵字即可。
創(chuàng)建類(lèi)MyObject.java代碼如下:
public class MyObject {
private static MyObject myObject;
private MyObject() {
}
// 設(shè)置同步方法效率太低了
// 整個(gè)方法被上鎖
synchronized public static MyObject getInstance() {
try {
if (myObject != null) {
} else {
// 模擬在創(chuàng)建對(duì)象之前做一些準(zhǔn)備性的工作
Thread.sleep(3000);
myObject = new MyObject();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
}
創(chuàng)建線程類(lèi)MyThread.java代碼如下:
import test.MyObject;
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
創(chuàng)建運(yùn)行類(lèi)Run.java代碼如下:
import extthread.MyThread;
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
程序運(yùn)行后的結(jié)果如下:
531436928
531436928
531436928
此方法加入同步synchronized關(guān)鍵字得到相同實(shí)例的對(duì)象富纸,但此種方法的運(yùn)行效率非常低下囤踩,是同步運(yùn)行的,下一個(gè)線程想要取得對(duì)象晓褪,則必須等上一個(gè)線程釋放鎖之后堵漱,才可以繼續(xù)執(zhí)行。
2.3.2 嘗試同步代碼塊
同步方法是對(duì)方法的整體進(jìn)行持鎖涣仿,這對(duì)運(yùn)行效率來(lái)講是不利的勤庐。改成同步代碼塊能解決嗎?
創(chuàng)建類(lèi)MyObject.java代碼如下:
public class MyObject {
private static MyObject myObject;
private MyObject() {
}
public static MyObject getInstance() {
try {
// 此種寫(xiě)法等同于:
// synchronized public static MyObject getInstance()
// 的寫(xiě)法好港,效率一樣很低愉镰,全部代碼被上鎖
synchronized (MyObject.class) {
if (myObject != null) {
} else {
// 模擬在創(chuàng)建對(duì)象之前做一些準(zhǔn)備性的工作
Thread.sleep(3000);
myObject = new MyObject();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
}
創(chuàng)建線程類(lèi)MyThread.java代碼如下:
import test.MyObject;
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
創(chuàng)建運(yùn)行類(lèi)Run.java代碼如下:
import extthread.MyThread;
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
// 此版本代碼雖然是正確的
// 但public static MyObject getInstance()方法
// 中的全部代碼都是同步的了,這樣做有損效率
}
}
程序運(yùn)行后的結(jié)果如下:
903341402
903341402
903341402
此方法加入同步synchronized語(yǔ)句塊得到相同實(shí)例的對(duì)象钧汹,但此種方法的運(yùn)行效率也是非常低的丈探,和synchronized同步方法一樣是同步運(yùn)行的。繼續(xù)更改代碼嘗試解決這個(gè)缺點(diǎn)拔莱。
2.3.3 針對(duì)某些重要的代碼進(jìn)行單獨(dú)的同步
同步代碼塊可以針對(duì)某些重要的代碼進(jìn)行單獨(dú)的同步碗降,而其他的代碼則不需要同步。這樣在運(yùn)行時(shí)塘秦,效率完全可以得到大幅提升讼渊。
創(chuàng)建MyObject.java代碼如下:
public class MyObject {
private static MyObject myObject;
private MyObject() {
}
public static MyObject getInstance() {
try {
if (myObject != null) {
} else {
// 模擬在創(chuàng)建對(duì)象之前做一些準(zhǔn)備性的工作
Thread.sleep(3000);
// 使用synchronized (MyObject.class)
// 雖然部分代碼被上鎖
// 但還是有非線程安全問(wèn)題
synchronized (MyObject.class) {
myObject = new MyObject();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
}
創(chuàng)建線程類(lèi)MyThread.java代碼如下:
import test.MyObject;
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
創(chuàng)建運(yùn)行類(lèi)Run.java代碼如下:
import extthread.MyThread;
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
程序運(yùn)行后的結(jié)果如下:
903341402
1770731361
1684444739
此方法使同步synchronized語(yǔ)句塊,只對(duì)實(shí)例化對(duì)象的關(guān)鍵代碼進(jìn)行同步尊剔,從語(yǔ)句的結(jié)構(gòu)上來(lái)講爪幻,運(yùn)行的效率的確得到了提升。但如果是遇到多線程的情況下無(wú)法解決得到同一個(gè)實(shí)例對(duì)象的結(jié)果。到底如何解決“懶漢模式”遇到多線程的情況呢挨稿?
2.3.4 使用DCL雙檢查鎖機(jī)制
在最后的步驟中仇轻,使用的是DCL雙檢查鎖機(jī)制來(lái)實(shí)現(xiàn)多線程環(huán)境中的延遲加載單例設(shè)計(jì)模式。
創(chuàng)建類(lèi)MyObject.java代碼如下:
public class MyObject {
private volatile static MyObject myObject;
private MyObject() {
}
// 使用雙檢測(cè)機(jī)制來(lái)解決問(wèn)題
// 即保證了不需要同步代碼的異步
// 又保證了單例的效果
public static MyObject getInstance() {
try {
if (myObject != null) {
} else {
// 模擬在創(chuàng)建對(duì)象之前做一些準(zhǔn)備性的工作
Thread.sleep(3000);
synchronized (MyObject.class) {
if (myObject == null) {
myObject = new MyObject();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
// 此版本的代碼稱(chēng)為:
// 雙重檢查Double-Check Locking
}
創(chuàng)建線程類(lèi)MyThread.java代碼如下:
import test.MyObject;
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
創(chuàng)建運(yùn)行類(lèi)Run.java代碼如下:
import extthread.MyThread;
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
程序運(yùn)行后的結(jié)果如下:
186668687
186668687
186668687
使用雙重檢查鎖功能叶组,成功地解決了“懶漢模式”遇到多線程的問(wèn)題拯田。DCL也是大多數(shù)多線程結(jié)合單例模式使用的解決方案。
3. 使用靜態(tài)內(nèi)置類(lèi)實(shí)現(xiàn)單例模式
DCL可以解決多線程單例模式的非線程安全問(wèn)題甩十。當(dāng)然船庇,使用其他的辦法也能達(dá)到同樣的效果。
創(chuàng)建類(lèi)MyObject.java代碼如下:
public class MyObject {
// 內(nèi)部類(lèi)方式
private static class MyObjectHandler {
private static MyObject myObject = new MyObject();
}
private MyObject() {
}
public static MyObject getInstance() {
return MyObjectHandler.myObject;
}
}
創(chuàng)建線程類(lèi)MyThread.java代碼如下:
import test.MyObject;
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
創(chuàng)建運(yùn)行類(lèi)Run.java代碼如下:
import extthread.MyThread;
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
程序運(yùn)行后的結(jié)果如下:
531436928
531436928
531436928
4. 序列化與反序列化的單例模式實(shí)現(xiàn)
靜態(tài)內(nèi)置類(lèi)可以達(dá)到線程安全問(wèn)題侣监,但如果遇到序列化對(duì)象時(shí)鸭轮,使用默認(rèn)的方式運(yùn)行得到的結(jié)果還是多例的。
創(chuàng)建MyObject.java代碼如下:
import java.io.ObjectStreamException;
import java.io.Serializable;
public class MyObject implements Serializable {
private static final long serialVersionUID = 888L;
// 內(nèi)部類(lèi)方式
private static class MyObjectHandler {
private static final MyObject myObject = new MyObject();
}
private MyObject() {
}
public static MyObject getInstance() {
return MyObjectHandler.myObject;
}
protected Object readResolve() throws ObjectStreamException {
System.out.println("調(diào)用了readResolve方法橄霉!");
return MyObjectHandler.myObject;
}
}
創(chuàng)建業(yè)務(wù)類(lèi)SaveAndRead.java代碼如下:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import test.MyObject;
public class SaveAndRead {
public static void main(String[] args) {
try {
MyObject myObject = MyObject.getInstance();
FileOutputStream fosRef = new FileOutputStream(new File(
"myObjectFile.txt"));
ObjectOutputStream oosRef = new ObjectOutputStream(fosRef);
oosRef.writeObject(myObject);
oosRef.close();
fosRef.close();
System.out.println(myObject.hashCode());
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
FileInputStream fisRef = new FileInputStream(new File(
"myObjectFile.txt"));
ObjectInputStream iosRef = new ObjectInputStream(fisRef);
MyObject myObject = (MyObject) iosRef.readObject();
iosRef.close();
fisRef.close();
System.out.println(myObject.hashCode());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
程序運(yùn)行后的效果如下:
1836019240
81628611
解決辦法就是在反序列化中使用readResolve()方法窃爷。
去掉如下代碼的注釋?zhuān)?/p>
protected Object readResolve() throws ObjectStreamException {
System.out.println("調(diào)用了readResolve方法!");
return MyObjectHandler.myObject;
}
程序運(yùn)行后的結(jié)果如下:
1836019240
調(diào)用了readResolve方法姓蜂!
1836019240
5. 使用static代碼塊實(shí)現(xiàn)單例模式
靜態(tài)代碼塊中的代碼在使用類(lèi)的時(shí)候就已經(jīng)執(zhí)行了按厘,所以可以應(yīng)用靜態(tài)代碼塊的這個(gè)特性來(lái)實(shí)現(xiàn)單例設(shè)計(jì)模式。
創(chuàng)建MyObject.java代碼如下:
public class MyObject {
private static MyObject instance = null;
private MyObject() {
}
static {
instance = new MyObject();
}
public static MyObject getInstance() {
return instance;
}
}
創(chuàng)建線程類(lèi)MyThread.java代碼如下:
import test.MyObject;
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(MyObject.getInstance().hashCode());
}
}
}
創(chuàng)建運(yùn)行類(lèi)Run.java代碼如下:
import extthread.MyThread;
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
程序運(yùn)行后的結(jié)果如下:
186668687
186668687
186668687
186668687
186668687
186668687
186668687
186668687
186668687
186668687
186668687
186668687
186668687
186668687
186668687
6. 使用enum枚舉數(shù)據(jù)類(lèi)型實(shí)現(xiàn)單例模式
枚舉enum和靜態(tài)代碼塊的特性相似钱慢,在使用枚舉類(lèi)時(shí)逮京,構(gòu)造方法會(huì)被自動(dòng)調(diào)用,也可以應(yīng)用其這個(gè)特性實(shí)現(xiàn)單例設(shè)計(jì)模式束莫。
創(chuàng)建類(lèi)MyObject.java代碼如下:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public enum MyObject {
connectionFactory;
private Connection connection;
private MyObject() {
try {
System.out.println("調(diào)用了MyObject的構(gòu)造");
String url = "jdbc:mysql://localhost:3306/test";
String username = "root";
String password = "root";
String driverName = "com.mysql.jdbc.Driver";
Class.forName(driverName);
connection = DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
public Connection getConnection() {
return connection;
}
}
創(chuàng)建線程類(lèi)MyThread.java代碼如下:
import test.MyObject;
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(MyObject.connectionFactory.getConnection()
.hashCode());
}
}
}
創(chuàng)建運(yùn)行類(lèi)Run.java代碼如下:
import extthread.MyThread;
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
程序運(yùn)行后的結(jié)果如下:
調(diào)用了MyObject的構(gòu)造
482793510
482793510
482793510
482793510
482793510
482793510
482793510
482793510
482793510
482793510
482793510
482793510
482793510
482793510
482793510
7. 完善使用enum枚舉實(shí)現(xiàn)單例模式
前面一節(jié)將枚舉類(lèi)進(jìn)行暴露懒棉,違反了“職責(zé)單一原則”,在本節(jié)中進(jìn)行完善览绿。
更改類(lèi)MyObject.java代碼如下:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class MyObject {
public enum MyEnumSingleton {
connectionFactory;
private Connection connection;
private MyEnumSingleton() {
try {
System.out.println("創(chuàng)建MyObject對(duì)象");
String url = "jdbc:mysql://localhost:3306/test";
String username = "root";
String password = "root";
String driverName = "com.mysql.jdbc.Driver";
Class.forName(driverName);
connection = DriverManager.getConnection(url, username,
password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
public Connection getConnection() {
return connection;
}
}
public static Connection getConnection() {
return MyEnumSingleton.connectionFactory.getConnection();
}
}
更改MyThread.java類(lèi)代碼如下:
import test.MyObject;
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(MyObject.getConnection().hashCode());
}
}
}
程序運(yùn)行的結(jié)果如下:
創(chuàng)建MyObject對(duì)象
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269