這是我的學習筆記,純手打,本想寫在紙質(zhì)筆記本上的,但時間一久就容易丟,所以還是記在網(wǎng)絡(luò)上吧:
意圖:保證每個類,只有一個實例,并且提供一個全局的訪問點
場景:需要嚴格的控制全局變量時,如線程池對象、數(shù)據(jù)庫連接池對象,不需要多個實例的對象,如工具類。
雙重檢測加鎖的單例實現(xiàn):
class LazySingleton{ private volatile static LazySingleton instance; private LazySingleton(){ } public static LazySingleton getInstance(){ if (null == instance){ synchronized(LazySingleton.class){ if(null == instance){ instance = new LazySingleton(); } } } return instance; }}
這里用了synchronized,先擴展一下筆記synchronized的四種用法
>>>
一、synchronized同步鎖為普通對象:
public void function(){ synchronized (object) { //doSomething… }}
當某個線程要訪問如上代碼塊中的內(nèi)容時,若該線程獲得object對象的鎖,那么就獲得了執(zhí)行權(quán),否則此線程被阻塞,直到其他線程釋放了object對象的鎖。
舉個例子:
public class SyncTest { public static void main(String args[]){ Sync[] syncs = new Sync[5]; for (int i = 0; i < syncs.length; i++) { syncs[i] = new Sync(new Object()); // new了5個新對象到數(shù)組種 } for(Sync sync : syncs){ sync.start(); } }}class Sync extends Thread{ Object syncObj; public Sync(Object syncObj) { this.syncObj = syncObj; } public void run(){ synchronized (syncObj) { System.out.println(Thread.currentThread().getName()+"運行中…"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"結(jié)束…"); } }}
運行結(jié)果:
Thread-0運行中…Thread-1運行中…Thread-2運行中…Thread-3運行中…Thread-4運行中…Thread-1結(jié)束…Thread-0結(jié)束…Thread-3結(jié)束…Thread-4結(jié)束…Thread-2結(jié)束…
看到這5個線程并沒有按順序執(zhí)行,他們之間不是同步的。這是因為這5個線程的syncObj并不是指向同一個對象,他們之間不存在同步鎖的競爭,所以是非同步的。將程序改為:
public class SyncTest { public static void main(String args[]){ Sync[] syncs = new Sync[5]; Object object = new Object(); //改了這里 for (int i = 0; i < syncs.length; i++) { syncs[i] = new Sync(object); //數(shù)組里的對象都是同一個,前面是5個不一樣的 } for(Sync sync : syncs){ sync.start(); } }}class Sync extends Thread{ Object syncObj; public Sync(Object syncObj) { this.syncObj = syncObj; } public void run(){ synchronized (syncObj) { System.out.println(Thread.currentThread().getName()+"運行中…"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"結(jié)束…"); } }}
運行結(jié)果:
Thread-0運行中…Thread-0結(jié)束…Thread-4運行中…Thread-4結(jié)束…Thread-2運行中…Thread-2結(jié)束…Thread-3運行中…Thread-3結(jié)束…Thread-1運行中…Thread-1結(jié)束…
這5個線程都達到了效果,但是5個線程的執(zhí)行順序并不是固定的,這是編譯是重排序造成的。
所以,若想要多個線程同步,則這些線程必須競爭同一個同步鎖。
二、synchronized同步鎖為類
類其實也是一個對象,可以去按照普通對象的方式去理解。不同之處在于,普通對象作用于某個實例,而類對象作用于整個類。
上面的例子我改改:
public class SyncTest { public static void main(String args[]){ Sync[] syncs = new Sync[5]; for (int i = 0; i < syncs.length; i++) { syncs[i] = new Sync(); } for(Sync sync : syncs){ sync.start(); } }}class Sync extends Thread{ public void run(){ synchronized (SyncTest.class) { // 改了這里 System.out.println(Thread.currentThread().getName()+"運行中…"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"結(jié)束…"); } }}
運行結(jié)果:
Thread-0運行中…Thread-0結(jié)束…Thread-3運行中…Thread-3結(jié)束…Thread-1運行中…Thread-1結(jié)束…Thread-4運行中…Thread-4結(jié)束…Thread-2運行中…Thread-2結(jié)束…
這些線程同樣實現(xiàn)了同步,因為他們的同步鎖是同一個對象SyncTest類對象。
需要注意的是,類對象鎖和普通對象鎖是不同的兩個鎖(即使這個對象是這個類的實例),他們之間互不干擾。
public class SyncTest { public static void main(String args[]){ Sync[] syncs = new Sync[5]; for (int i = 0; i < syncs.length; i++) { syncs[i] = new Sync(); } Sync1 sync1 = new Sync1(new SyncTest()); for(Sync sync : syncs){ sync.start(); //類對象鎖跑5個線程 } sync1.start(); //普通對象鎖跑一個線程 }}class Sync extends Thread{ public void run(){ synchronized (SyncTest.class) { //類對象鎖 System.out.println(Thread.currentThread().getName()+"運行中…"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"結(jié)束…"); } }} class Sync1 extends Thread{ SyncTest syncTest; public Sync1(SyncTest syncTest){ this.syncTest = syncTest; } public void run(){ synchronized (syncTest) { //普通對象鎖 System.out.println(Thread.currentThread().getName()+"運行中…"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"結(jié)束…"); } }}
運行結(jié)果:
Thread-0運行中…Thread-5運行中…Thread-0結(jié)束…Thread-5結(jié)束…Thread-4運行中…Thread-4結(jié)束…Thread-2運行中…Thread-2結(jié)束…Thread-3運行中…Thread-3結(jié)束…Thread-1運行中…Thread-1結(jié)束…
可以看到,雖然Sync1中的對象鎖是SyncTest的實例,但是Sync1與Sync的run方法中的synchronized代碼塊并沒有實現(xiàn)同步,他們可以同時訪問這段代碼。驗證了互相不干擾。
三、synchronized修飾普通方法
synchronized修飾方法在本質(zhì)上和修飾代碼塊是一樣的,他們都是通過同步鎖來實現(xiàn)同步的。
public synchronized void syncFunction(){ //doSomething…}
synchronized修飾普通方法中的同步鎖就是這個對象本身,即 this
看代碼:
class Sync extends Thread{ public synchronized void syncFunction(){ doSomething(); } //syncFunction() 和 syncFunction2() 實現(xiàn)的同步效果是一樣的。 public void syncFunction2(){ synchronized (this) { doSomething(); } } private void doSomething(){ //doSomething… }}
當類中某個方法test()被synchronized關(guān)鍵字所修飾時,所有不同的線程訪問這個類的同一個實例的test()方法都會實現(xiàn)同步的效果。不同的實例之間不存在同步鎖的競爭,也就是說,不同的線程訪問這個類不同實例的test()方法并不會實現(xiàn)同步。這很容易理解,因為不同的實例同步鎖不同,每個實例都有自己的”this”。
四、synchronized修飾靜態(tài)方法
public static synchronized void syncFunction(){ //doSomething…}
同樣的,synchronized作用于靜態(tài)方法時,跟使用類對象作為靜態(tài)鎖的效果是一樣的,此時的類對象就是靜態(tài)方法所屬的類。
不同的線程訪問某個類不同實例的syncFunction()方法(被synchronized修飾的靜態(tài)方法,如上)時,他們之間實現(xiàn)了同步效果。結(jié)合上面的解釋,這種情況也很好理解:此時不同線程競爭同一把同步鎖,這就是這個類的類對象鎖。
總結(jié)
理解synchronized的關(guān)鍵就在于:若想要多個線程同步,則這些線程必須競爭同一個同步鎖。這個同步鎖,可以理解為一個對象。
>>>
擴展完畢,繼續(xù)說單例模式:
我們知道,創(chuàng)建一個對象大致分為三步:
step1 開辟空間
step2 初始化空間
step3 賦值
但是編譯器、即時編譯,CPU可能對字節(jié)碼進行優(yōu)化,對編譯后的字節(jié)碼進行指令的重新排序。比如將第2步和第3步置換位置、那就會出現(xiàn)有一個狀況:
線程A賦值完畢,但未完成初始化時,線程B在走第一個IF檢查時,發(fā)現(xiàn)instance實例不為null,直接return未初始化的對象,出現(xiàn)異常。
加入volatile關(guān)鍵字給實例變量,是為了防止JVM進行指令重排而出現(xiàn)的問題。
private volatile static LazySingleton instance;
上述時懶漢式實現(xiàn)單例,意為在需要時才去實例化。
那么對應(yīng)的有餓漢式:
class HungrySingleton{ //靜態(tài)屬性public static String name=”JJ”;private static HungrySingleton instance = new HungrySingleton(); //構(gòu)造私有化private HungrySingleton(){ } //獲取實例的方法public static HungrySingleton getInstance(){ return instance; }}
只有在主動使用對應(yīng)的類時,才會觸發(fā)實例的初始化,如當前類是啟動類即main函數(shù)所在的類,直接進行new操作,訪問靜態(tài)屬性、訪問靜態(tài)方法、用反射訪問、初始化一個類的子類等,都會觸發(fā)實例的初始化操作。
類加載的初始化階段就完成了實例的初始化。本質(zhì)上是借助了JVM類加載機制,保證實例的唯一性(初始化過程只會執(zhí)行一次)和線程安全(JVM以同步的形式來完成類加載的整個過程)。
類加載過程:
1.加載二進制數(shù)據(jù)到內(nèi)存中,生成對應(yīng)的Class數(shù)據(jù)結(jié)構(gòu)
2.連接:驗證是否符合規(guī)范,準備給類的靜態(tài)成員變量賦默認值 ,解析
3.初始化:給類的靜態(tài)變量賦初值
JDK中的Runtime類就是使用了餓漢式。
如果通過反射來獲取實例對象呢?是否和getInstance()獲取的實例對象是同一個?
答案:不是
反射獲?。?/p>
//通過構(gòu)造函數(shù)實現(xiàn)Constructor declaredConstructor = HungrySingleton.class.getDeclaredConstractor();//訪問控制權(quán)設(shè)置為truedeclaredConstructor.setAccessible( true );//通過反射實例化HungrySingleton hungrySingleton = declaredConstructor.new Instance();//通過餓漢式實例化HungrySingleton instance = HungrySingleton.getInstance();//會輸出falseSystem.out.print(hungrySingleton == instance );
為了避免私有構(gòu)造被反射暴力使用的問題,可以在私有構(gòu)造方法中加多一個判斷instance是否為null,如果不為null,則拋出RuntimeException異常。
內(nèi)部類單例模式:
class InnerClassSingleton{private static InnerClassSingleton instance = new InnerClassSingleton(); //構(gòu)造方法私有化private InnerClassSingleton(){}//獲取實例的方法public static InnerClassSingleton getInstance(){ //返回的還是InnerClassSingleton的實例 return SingletonHolder.instance; }//靜態(tài)內(nèi)部類private static class SingletonHolder{private static InnerClassSingleton instance = new InnerClassSingleton();}}
相對于餓漢式,該方式只在調(diào)用getInstance()方法時才會初始化。是餓漢模式和懶漢模式的結(jié)合。
枚舉單例模式:
enmu EnmuSingleton{INSTANCE; public void print(){ System.out.println(“xxx”); }}
在JVM中生成的INSTANCE實例類型是static final類型的??梢酝ㄟ^calss文件反編譯看到。
另外,還可以通過反序列化的方式來獲取另一個單例,這兩個“單例”是不相同的,這就相當于對單例模式進行了破壞。
解決辦法:實現(xiàn)序列化接口,指定serialVersionUID的值。這樣就保證了實例被序列化,反序列化是的版本都是一樣的。否則會報異常。
還有些類使用到了雙重檢測單例:
比如:Spring中的ReactiveAdapterRegistry、tomcat中的TomcatURLStreanHandlerFactory.
OK,單例模式差不多就先學到這里了。