這又是一個(gè)新的系列啦,探究各大設(shè)計(jì)模式在開發(fā)中必須注意思考的一些問題,以及它們的多向使用。
文章結(jié)構(gòu):(1)單例模式概念以及優(yōu)缺點(diǎn)(2)各式各樣的單例及其線程安全問題。(3)使用推薦。
單例模式概念以及優(yōu)缺點(diǎn):
(1)定義:
要求一個(gè)類只能生成一個(gè)對(duì)象,所有對(duì)象對(duì)它的依賴相同。
(2)優(yōu)點(diǎn):
1. 只有一個(gè)實(shí)例,減少內(nèi)存開支。應(yīng)用在一個(gè)經(jīng)常被訪問的對(duì)象上
2. 減少系統(tǒng)的性能開銷,應(yīng)用啟動(dòng)時(shí),直接產(chǎn)生一單例對(duì)象,用永久駐留內(nèi)存的方式。
3.避免對(duì)資源的多重占用
4.可在系統(tǒng)設(shè)置全局的訪問點(diǎn),優(yōu)化和共享資源訪問。
(3)缺點(diǎn):
1.一般沒有接口,擴(kuò)展困難。原因:接口對(duì)單例模式?jīng)]有任何意義;要求“自行實(shí)例化”,并提供單一實(shí)例,接口或抽象類不可能被實(shí)例化。(當(dāng)然,單例模式可以實(shí)現(xiàn)接口、被繼承,但需要根據(jù)系統(tǒng)開發(fā)環(huán)境判斷)
2.單例模式對(duì)測(cè)試是不利的。如果單例模式?jīng)]完成,是不能進(jìn)行測(cè)試的。
3.單例模式與單一職責(zé)原則有沖突。原因:一個(gè)類應(yīng)該只實(shí)現(xiàn)一個(gè)邏輯,而不關(guān)心它是否是單例,是不是要單例取決于環(huán)境;單例模式把“要單例”和業(yè)務(wù)邏輯融合在一個(gè)類。
(4)使用場(chǎng)景:
1.要求生成唯一序列化的環(huán)境
2.項(xiàng)目需要的一個(gè)共享訪問點(diǎn)或共享的數(shù)據(jù)點(diǎn)
3.創(chuàng)建一個(gè)對(duì)象需要消耗資源過多的情況。如:要訪問IO和 數(shù)據(jù)庫等資源。
4.需要定義大量的靜態(tài)常量和靜態(tài)方法(如工具類)的環(huán)境。可以采用單例模式或者直接聲明static的方式。
(5)注意事項(xiàng):
1.類中其他方法,盡量是static
2.注意JVM的垃圾回收機(jī)制。
如果一個(gè)單例對(duì)象在內(nèi)存長(zhǎng)久不使用,JVM就認(rèn)為對(duì)象是一個(gè)垃圾。所以如果針對(duì)一些狀態(tài)值,如果回收的話,應(yīng)用就會(huì)出現(xiàn)故障。
3.采用單例模式來記錄狀態(tài)值的類的兩大方法:
(一)、由容器管理單例的生命周期。Java EE容器或者框架級(jí)容器,自行管理對(duì)象的生命周期。
(二)狀態(tài)隨時(shí)記錄。異步記錄的方式或者使用觀察者模式,記錄狀態(tài)變化,確保重新初始化也可從資源環(huán)境獲得銷毀前的數(shù)據(jù)。
二、各式各樣的單例及其線程安全問題:
(1)懶漢式單例:
意思:就是需要使用這個(gè)對(duì)象的時(shí)候才去創(chuàng)建這個(gè)對(duì)象。
//懶漢式單例
public class Singleton1 {
private static Singleton1 singleton1=null;
public Singleton1(){
}
public static Singleton1 getInstance(){
if (singleton1==null){
try {
Thread.sleep(200);//我們知道初始化一個(gè)對(duì)象需要一定時(shí)間的嘛,我們用sleep假設(shè)這個(gè)時(shí)間
singleton1 = new Singleton1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return singleton1;
}
}
//測(cè)試線程
public class SingleThread1 extends Thread {
//哈希值對(duì)應(yīng)的是唯一的嘛,如果不一樣了,就說明使用的不是同一個(gè)對(duì)象咯。
@Override
public void run() {
System.out.println(Singleton1.getInstance().hashCode());
}
}
//測(cè)試類
public class SingletonTest {
public static void main(String []args){
SingleThread1[] thread1s = new SingleThread1[10];
for (int i= 0;i<thread1s.length;i++){
thread1s[i] = new SingleThread1();
}
for (int j = 0; j < thread1s.length; j++) {
thread1s[j].start();
}
}
}
//打印的結(jié)果:
569219718
1259146238
565373737
732830316
679555294
1886445805
1557403724
635681435
622018771
1439317371
線程安全的懶漢式單例設(shè)計(jì):
1.鎖住獲取方法方式:
public class Singleton3 {
private static Singleton3 instance = null;
private Singleton3(){}
//鎖住獲取方法的方式
public synchronized static Singleton3 getInstance() {
try {
if(instance != null){
}else{
Thread.sleep(300);
instance = new Singleton3();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}
2.鎖住部分代碼塊的方式:
public class Singleton2 {
private static Singleton2 instance = null;
private Singleton2(){
}
public static Singleton2 getInstance() {
try {
//鎖住代碼塊的方式
synchronized (Singleton2.class) {
if(instance != null){
}else{
Thread.sleep(200);
instance = new Singleton2();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}
3.鎖住初始化對(duì)象操作的方式:但是!!!這不是線程安全的!!一會(huì)有這個(gè)方式的優(yōu)化從而實(shí)現(xiàn)線程安全。
為什么??
因?yàn)槎鄠€(gè)訪問已經(jīng)進(jìn)入到創(chuàng)建的那里了。
public class Singleton4 {
private static Singleton4 instance = null;
private Singleton4(){}
public static Singleton4 getInstance() {
try {
if(instance != null){
}else{
Thread.sleep(300);
//只鎖住初始化操作的方式
synchronized (Singleton4.class) {
instance = new Singleton4();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}
4.鎖住初始化對(duì)象操作的方式,但有個(gè)再檢查操作:
public class Singleton5 {
//使用volatile關(guān)鍵字保其可見性
volatile private static Singleton5 instance = null;
private Singleton5(){}
public static Singleton5 getInstance() {
try {
if(instance != null){
}else{
Thread.sleep(300);
//鎖住初始化操作的方式
synchronized (Singleton5.class) {
if(instance == null){//二次檢查
instance = new Singleton5();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}
使用了volatile關(guān)鍵字來保證其線程間的可見性;在同步代碼塊中使用二次檢查,以保證其不被重復(fù)實(shí)例化。集合其二者,這種實(shí)現(xiàn)方式既保證了其高效性,也保證了其線程安全性。
解析volatile在此的作用:
volatile(涉及java內(nèi)存模型的知識(shí))會(huì)禁止CPU對(duì)內(nèi)存訪問重排序(并不一定禁止指令重排),也就是CPU執(zhí)行初始化操作,那么他會(huì)保證其他CPU看到的操作順序是1.給 instance 分配內(nèi)存--2.調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量--3.將instance對(duì)象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 就為非 null 了),(雖然在CPU內(nèi)由于流水線多發(fā)射并不一定是這個(gè)順序)
不使用volatile的問題是什么呢??
在 JVM 的即時(shí)編譯器中存在指令重排序的優(yōu)化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被線程二搶占了,這時(shí) instance 已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會(huì)直接返回 instance,然后使用,然后順理成章地報(bào)錯(cuò)。
用volatile的意義并不在于其他線程一定要去內(nèi)存總讀取instance,而在于它限制了CPU對(duì)內(nèi)存操作的重拍序,使其他線程在看到3之前2一定是執(zhí)行過的。
(2)餓漢式單例:
意思是:類裝載時(shí)就實(shí)例化該單例類
public class Singleton6 {
//一初始化類就初始化這個(gè)單例了!!!
private static Singleton6 singleton6= new Singleton6();
private Singleton6(){
}
public static Singleton6 getInstance(){
return singleton6;
}
}
基于classloder機(jī)制避免了多線程的同步問題,不過,instance在類裝載時(shí)就實(shí)例化,雖然導(dǎo)致類裝載的原因有很多種,在單例模式中大多數(shù)都是調(diào)用getInstance方法, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時(shí)候初始化instance顯然沒有達(dá)到lazy loading的效果。這個(gè)是沒有懶加載的功能的!!!
餓漢式單例變種:
public class Singleton7 {
private static Singleton7 instance = null;
static {
instance = new Singleton7();
}
private Singleton7() {
}
public static Singleton7 getInstance() {
return instance;
}
}
(3)靜態(tài)內(nèi)部類實(shí)現(xiàn)懶加載:
//靜態(tài)內(nèi)部類單例
public class Singleton8 {
private static class SingletonHolder {
private static final Singleton8 INSTANCE = new Singleton8();
}
private Singleton8 (){}
public static final Singleton8 getInstance() {
return SingletonHolder.INSTANCE;
}
}
同樣利用了classloder的機(jī)制來保證初始化instance時(shí)只有一個(gè)線程,它跟餓漢式的兩種方式不同的是:餓漢式的兩種方式是只要Singleton類被裝載了,那么instance就會(huì)被實(shí)例化(沒有達(dá)到lazy loading效果),而這種方式是Singleton類被裝載了,instance還未被初始化。因?yàn)镾ingletonHolder類沒有被主動(dòng)使用,只有顯示通過調(diào)用getInstance方法時(shí),才會(huì)顯示裝載SingletonHolder類,從而實(shí)例化instance。想象一下,如果實(shí)例化instance很消耗資源,我想讓他延遲加載,另外一方面,我不希望在Singleton類加載時(shí)就實(shí)例化,因?yàn)槲也荒艽_保Singleton類還可能在其他的地方被主動(dòng)使用從而被加載,那么這個(gè)時(shí)候?qū)嵗痠nstance顯然是不合適的。
靜態(tài)內(nèi)部類方式單例再度研究:序列化和反序列化問題:
public class MySingleton implements Serializable {
private static final long serialVersionUID = 1L;
//內(nèi)部類
private static class MySingletonHandler{
private static MySingleton instance = new MySingleton();
}
private MySingleton(){}
public static MySingleton getInstance() {
return MySingletonHandler.instance;
}
}
public class SaveAndReadForSingleton {
public static void main(String[] args) {
MySingleton singleton = MySingleton.getInstance();
//創(chuàng)建個(gè)文件流
File file = new File("MySingleton.txt");
//使用節(jié)點(diǎn)流,直接與文件關(guān)聯(lián)
try {
//寫入文件
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton);
fos.close();
oos.close();
System.out.println(singleton.hashCode());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
try {
//讀取文件流
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
MySingleton rSingleton = (MySingleton) ois.readObject();
fis.close();
ois.close();
System.out.println(rSingleton.hashCode());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
這樣的單例測(cè)試出來時(shí),hash是不一樣的,因?yàn)闆]有同步到序列化與反序列化問題。說明反序列化后返回的對(duì)象是重新實(shí)例化的,單例被破壞了。
解決:當(dāng)JVM從內(nèi)存中反序列化地"組裝"一個(gè)新對(duì)象時(shí),就會(huì)自動(dòng)調(diào)用readResolve方法來返回我們指定好的對(duì)象,readResolve允許class在反序列化返回對(duì)象前替換、解析在流中讀出來的對(duì)象。實(shí)現(xiàn)readResolve方法,一個(gè)class可以直接控制反序化返回的類型和對(duì)象引用。
public class MySingleton1 implements Serializable {
private static final long serialVersionUID = 1L;
//內(nèi)部類
private static class MySingletonHandler{
private static MySingleton1 instance = new MySingleton1();
}
private MySingleton1(){}
public static MySingleton1 getInstance() {
return MySingletonHandler.instance;
}
//該方法在反序列化時(shí)會(huì)被調(diào)用,該方法不是接口定義的方法,有點(diǎn)兒約定俗成的感覺
protected Object readResolve() throws ObjectStreamException {
System.out.println("調(diào)用了readResolve方法!");
return MySingletonHandler.instance;
}
}
修改SaveAndReadForSingleton文件中的MySingleton,輸出
2133927002
調(diào)用了readResolve方法!解決序列化與反序列化問題!
2133927002
(4)枚舉:
//枚舉實(shí)現(xiàn)單例
public enum EnumSingletonFactory {
singletonFactory;
private EnumSingleton instance;
private EnumSingletonFactory(){//枚舉類的構(gòu)造方法在類加載是被實(shí)例化
instance = new EnumSingleton();
}
public EnumSingleton getInstance(){
return instance;
}
}
在thread中調(diào)用實(shí)現(xiàn):
@Override
public void run() { System.out.println(EnumFactory.singletonFactory.getInstance().hashCode());
}
但是此博客 引起我思考,是違反單一職責(zé)的,因?yàn)樗┞读嗣杜e的細(xì)節(jié),所以我們需要改造他。
//使用工廠來生成枚舉類
//通過工廠類的靜態(tài)方法去訪問枚舉類,然后通過枚舉類訪問它的單例。
public class ClassFactory {
private enum MyEnumSingleton{
singletonFactory;
private EnumSingleton instance;
private MyEnumSingleton(){//枚舉類的構(gòu)造方法在類加載是被實(shí)例化
instance = new EnumSingleton();
}
public EnumSingleton getInstance(){
return instance;
}
}
public static EnumSingleton getInstance(){
return MyEnumSingleton.singletonFactory.getInstance();
}
}
在thread中調(diào)用實(shí)現(xiàn):
@Override
public void run() {
System.out.println(ClassFactory.getInstance().hashCode());
}
枚舉類的方式不僅能避免多線程同步問題,而且還能防止反序列化重新創(chuàng)建新的對(duì)象。不過實(shí)際工程代碼中,很少去用此方式。
三、推薦使用:
上述的各種單例都講完了:基本是五種寫法。懶漢,惡漢,雙重校驗(yàn)鎖,枚舉和靜態(tài)內(nèi)部類。
(1)餓漢式單例。
原因:類的加載機(jī)制保證了,類初始化時(shí),只執(zhí)行一次靜態(tài)代碼塊以及類變量初始化。直接保證了唯一性,保證了線程安全。(一般使用非靜態(tài)代碼塊方式)
(2)靜態(tài)內(nèi)部類方式:
原因:懶加載唄!!!應(yīng)用在一些十分巨大的單例bean中。
參考博客:此博客讓我對(duì)單例加深了一大層,感謝感謝!!
好了,設(shè)計(jì)模式(一)--深入單例模式(涉及線程安全問題)講完了。本博客是我復(fù)習(xí)階段的一些筆記,拿來分享經(jīng)驗(yàn)給大家。歡迎在下面指出錯(cuò)誤,共同學(xué)習(xí)!!你的點(diǎn)贊是對(duì)我最好的支持!!