定義與類型
定義:保證一個類僅有一個實例,并提供一個全局訪問點
類型:創(chuàng)建型
單例模式使用場景
想確保任何情況下都絕對只有一個實例
例如:線程池,數(shù)據(jù)庫連接池一般都為單例模式
單例模式優(yōu)點
- 在內(nèi)存中只有一個實例,減少內(nèi)存開銷
- 可以避免對資源的多重占用
- 設(shè)置全局訪問點,嚴格控制訪問
缺點
沒有接口,擴展困難
單例模式-重點
- 私有構(gòu)造器
- 線程安全(重點)
- 延遲加載(重點)
- 序列化和反序列化安全(序列化和反序列化會破壞單例模式)
- 反射(防止反射攻擊)
實現(xiàn)單例模式-懶漢式
/**
* 懶漢式
*/
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
將構(gòu)造器私有化,提供一個靜態(tài)類來獲取對象實例
但是該方法是線程不安全的,如果在LazySingleton未實例化前,多個線程同時調(diào)用,例如線程1判斷l(xiāng)azySingleton為null,進入下一步并沒有創(chuàng)建對象時,另一個對象也判斷l(xiāng)azySingleton為空這時就會出現(xiàn)創(chuàng)造出多個實例的錯誤。
使用多線程debug方式顯示問題
在idea中想要使用多線程debug方式需要按如圖所示修改,將斷點改為Thread級別
在Test的main方法中開辟兩個線程
public class Test {
public static void main(String[] args) {
// LazySingleton instance = LazySingleton.getInstance();
Thread t1 = new Thread(new T());
Thread t2 = new Thread(new T());
t1.start();
t2.start();
System.out.println("main end");
}
}
T為Runnable的實現(xiàn)類
/**
* 設(shè)置線程,開啟其他線程獲取LazySingleton實例
*/
public class T implements Runnable{
@Override
public void run() {
LazySingleton instance = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName()+" "+instance);
}
}
使用debug方式啟動Test的main方法,此時在Frames中就可以看到除main線程外,還有兩個運行中的Thread
切換到Thread-0線程,走到if判斷后,且未創(chuàng)建LazySingleton實例時刻
?切換到Thread-1線程,使Thread-1線程繼續(xù)運行直到Thread-1線程結(jié)束
然后切回Thread-0線程,使Thread-0線程繼續(xù)運行直到Thread-0線程結(jié)束
觀看控制臺即可發(fā)現(xiàn)在堆中創(chuàng)建了兩個?LazySingleton實例
解決方式
?1、在獲取LazySingleton實例方法上加上synchronized關(guān)鍵字由于getInstance是靜態(tài)方法,所有給該方法上加入synchronized關(guān)鍵字等同于給整個LazySingleton類加鎖,雖然能解決并發(fā)情況下創(chuàng)建多個實例的問題,但是會影響性能。
DoubleCheck雙重檢查
?通過雙重檢查既能解決并發(fā)情況下創(chuàng)建多個實例的問題,也可以很好的提升性能
雙重檢查代碼實現(xiàn)
第一版雙重檢查代碼實現(xiàn)
在第一次判斷后加上synchronized關(guān)鍵字再判斷一次,這樣既可以解決并發(fā)情況下創(chuàng)建多個實例的問題,對性能的影響也非常有限
但是這種做法也會產(chǎn)生問題因為創(chuàng)建對象的過程并不是原子操作,在jvm內(nèi)部可能會使創(chuàng)建過程發(fā)生指令重排序
一般創(chuàng)建過程
- 分配內(nèi)存給這個對象
- 初始化對象
- 設(shè)置lazyDoubleCheckSingleton 指向分配的內(nèi)存地址
jvm會在什么情況下發(fā)生指令重排序呢?
jvm允許在單線程中不影響最終結(jié)果的情況下發(fā)生指令重排序
在該實例中第二步和第三步發(fā)生重排序,即先指向內(nèi)存地址在初始化對象并不會改變單線程中的最終結(jié)果,所有jvm會允許這兩步發(fā)生重排序
發(fā)生指令重拍尋后的創(chuàng)建過程
- 分配內(nèi)存給這個對象
- 設(shè)置lazyDoubleCheckSingleton 指向分配的內(nèi)存地址
- 初始化對象
但在并發(fā)的情況下就有可能出現(xiàn)線程1發(fā)生指令重排 剛指向完內(nèi)存地址時,?
線程2進入第一層判斷發(fā)現(xiàn)lazyDoubleCheckSingleton并不為空,然后直接返回
這時就有可能出現(xiàn)別的線程返回的是沒有初始化完成的對象從而發(fā)生一些意想不到的錯誤
解決指令重排序
1.禁用指令重排序
想要禁用指令重排序非常簡單只需將私有的成員變量 lazyDoubleCheckSingleton 增加?volatile關(guān)鍵字即可
2.線程1的指令重排對線程2是不可見的
想要線程1 的指令重排對線程2 不可見 需要應用靜態(tài)內(nèi)部類
/**
* 想要線程1 的指令重排對線程2 不可見 需要應用靜態(tài)內(nèi)部類
*/
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton(){
}
private static class Inner{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return Inner.staticInnerClassSingleton;
}
}
原理
在介紹原理前首先介紹下靜態(tài)內(nèi)部類的加載時機
- 外部類初次加載,會初始化靜態(tài)變量、靜態(tài)代碼塊、靜態(tài)方法,但不會加載內(nèi)部類和靜態(tài)內(nèi)部類。
- 實例化外部類,調(diào)用外部類的靜態(tài)方法、靜態(tài)變量,則外部類必須先進行加載,但只加載一次。
- 直接調(diào)用靜態(tài)內(nèi)部類時,外部類不會加載。
即?靜態(tài)內(nèi)部類不會自動初始化,只有調(diào)用靜態(tài)內(nèi)部類的方法,靜態(tài)域,或者構(gòu)造方法的時候才會加載靜態(tài)內(nèi)部類。
在了解靜態(tài)內(nèi)部類的加載時機后,再來解釋利用靜態(tài)內(nèi)部類是如何實現(xiàn)隔離線程間的指令重排的
jvm在類初始化階段會給class對象加鎖,在多個線程同時初始化該類時 會使多個線程初始化處于同步階段,所以在靜態(tài)內(nèi)部類初始化階段可以做到對其他線程的隔離。
?基于該特性可以設(shè)計出一個通過靜態(tài)內(nèi)部類的基于類初始化的延遲加載(懶加載)解決方案
實現(xiàn)單例模式-餓漢式
餓漢式是在類加載階段就進行實例化,餓漢式創(chuàng)造簡單,也能解決并發(fā)情況下創(chuàng)建多實例的問題
/**
* 餓漢式
*/
public class HungrySingleton {
private static HungrySingleton hungrySingleton;
private HungrySingleton(){
}
static {
// 基于靜態(tài)代碼塊的餓漢式單例模式
hungrySingleton = new HungrySingleton();
}
public HungrySingleton getInstance(){
return hungrySingleton;
}
}
餓漢式雖然簡單易用但是由于在類加載階段就會創(chuàng)建實例,如果大量使用這種方式會導致內(nèi)存占用過大問題
單例模式-序列化和反序列化安全
以餓漢式為例,將實例對象輸出到磁盤
代碼示例?
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("singleton_file"));
outputStream.writeObject(instance);
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("singleton_file"));
HungrySingleton object = (HungrySingleton)inputStream.readObject();
System.out.println("instance: "+instance);
System.out.println("object: "+object);
}
}
要想將實例對象輸出到磁盤上需要讓實例類實現(xiàn) Serializable接口使其可序列化
題外話 serialVersionUID版本號的作用
當對同一個實體序列化反序列化時,需要serialVersionUID值一致才能成功。如果我們不顯示指定serialVersionUID,在序列化時會自動生成一個serialVersionUID。當實體類改動了,反序列化時,會生成一個新serialVersionUID。這兩個serialVersionUID的值肯定不一致,從而反序列化會失敗。但是如果顯示指定,就不會生成新serialVersionUID值了。反序列化的serialVersionUID就是原序列化的serialVersionUID。
運行Test的main方法,查看打印結(jié)果
?可以看到在經(jīng)過序列化后獲取的對象實例并不是同一個對象了,這就破壞了單例模式。
如何解決?
先說結(jié)論,在單例類上加上readResolve方法就可以實現(xiàn)序列化后的對象也是同一個對象
private Object readResolve(){
return hungrySingleton;
}
加上該方法后重新執(zhí)行main方法,查看打印結(jié)果
?發(fā)現(xiàn)確實已經(jīng)是同一個對象了
為什么需要加一個readResolve方法呢?
讓我們來看下ObjectInputStream的源碼
首先ObjectInputStream的readObject方法調(diào)用了readObject0
?進入readObject0方法,發(fā)現(xiàn)內(nèi)部有一個switch case判斷,我們是序列化的對象實例,所有直接看case TC_OBJECT 這一部分
發(fā)現(xiàn)他的checkResolve方法調(diào)用的readOrdinaryObject方法,繼續(xù)進入該方法
/**
* Reads and returns "ordinary" (i.e., not a String, Class,
* ObjectStreamClass, array, or enum constant) object, or null if object's
* class is unresolvable (in which case a ClassNotFoundException will be
* associated with object's handle). Sets passHandle to object's assigned
* handle.
*/
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
handles.finish(passHandle);
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
原來readOrdinaryObject首先會通過一個三目運算來創(chuàng)建序列化的對象。如果這個對象能實例化就創(chuàng)建一個新的對象。
obj = desc.isInstantiable() ? desc.newInstance() : null;
那為什么在單例類中加入一個readResolve方法就能改變這種情況呢?
繼續(xù)往下看,在第2076行有個判斷,從名字上就能看得出是用來判斷是否能獲取到readResolve方法的
?進入readResolve方法查看
?在這里可以看到如果沒有readResolveMethod不為空那么就會返回true。
走到在就終于明白原因了,太不容易了
原來是readOrdinaryObject會查看實例類內(nèi)部是否有readResolve方法,如果存在readResolve方法
那么就會通過反射的方式獲取readResolve中的返回值,將obj的引用改為readResolve方法的返回,
從而最終改變了反序列化的引用地址,使其的引用變?yōu)閱卫膭?chuàng)建的實例。
總結(jié),反序列化時會創(chuàng)建了一個新的實例對象,如果在單例對象代碼中添加public Object readResolve()
方法,會在最后改變了這個實例對象的引用為單例對象readResolve()
方法的返回結(jié)果。
通過枚舉類實現(xiàn)單例模式
Google 首席 Java 架構(gòu)師、《Effective Java》一書作者、Java集合框架的開創(chuàng)者Joshua Bloch在Effective Java一書中提到:
單元素的枚舉類型已經(jīng)成為實現(xiàn)Singleton的最佳方法?!敬罄姓媸沁@么說的】
那么枚舉類型有什么神奇之處呢?
特點
- 枚舉類型可以解決序列化問題
- 枚舉類型可以解決反射功能的問題
創(chuàng)建一個枚舉類型
public enum EnumInstance {
INSTANCE;
private Object data;
private String name;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
}
序列化枚舉類實例
查看打印結(jié)果
?發(fā)現(xiàn)枚舉類的序列化結(jié)果是一致的
同樣的來看一下readEnum方法的具體邏輯
?核心邏輯是通過cl(類型),和name獲取枚舉常量,由于枚舉name是唯一的并且對應一個枚舉常量,所有最終拿到的是同一個枚舉常量,沒有重新創(chuàng)建對象。因此序列化對單例模式的破壞對于枚舉類型是不起作用的。
枚舉是如何實現(xiàn)單例模式的呢?
通過jad 反編譯工具來看下答案
jad的官網(wǎng)為
JAD Java Decompiler Download Mirrorhttps://varaneckas.com/jad/下載jad,并解壓
配置環(huán)境變量 在系統(tǒng)變量中創(chuàng)建JAD_HOME變量,將jad目錄配置進去
?然后配置系統(tǒng)變量里的path變量
這樣就配置成功了。
找到EnumInstance的class文件執(zhí)行jad EnumInstance.class命令
?可以看到生成了一個EnumInstance.jad文件,這樣就使用jad生成了 EnumInstance.class的反編譯文件,看一下反編譯后的文件
首先可以發(fā)現(xiàn)枚舉類居然是被final修飾的?
public final class EnumInstance extends Enum
?再往下看發(fā)現(xiàn) 雖然沒有寫構(gòu)造方法,但是內(nèi)部卻已經(jīng)被創(chuàng)建出來一個私有的構(gòu)造器
private EnumInstance(String s, int i)
{
super(s, i);
}
再然后就是定義的getInstance方法和一個被final修飾的靜態(tài)成員變量INSTANCE;
public static EnumInstance getInstance()
{
return INSTANCE;
}
public static final EnumInstance INSTANCE;
最后可以看到一個靜態(tài)代碼塊用來對成員變量INSTANCE初始化
static
{
INSTANCE = new EnumInstance("INSTANCE", 0);
$VALUES = (new EnumInstance[] {
INSTANCE
});
}
從枚舉類的反編譯文件可以看出,枚舉類是一個餓漢式的單例模式,在類被加載時初始化,沒有延遲加載。
單例模式之線程單例
變量值的共享可以使用public?static的形式,所有線程都使用同一個變量,如果想實現(xiàn)每一個線程都有自己的共享變量該如何實現(xiàn)呢?JDK中的ThreadLocal類正是為了解決這樣的問題。
ThreadLocal類并不是用來解決多線程環(huán)境下的共享變量問題,而是用來提供線程內(nèi)部的共享變量,在多線程環(huán)境下,可以保證各個線程之間的變量互相隔離、相互獨立。在線程中,可以通過get()/set()方法來訪問變量。
ThreadLocal實例通常來說都是private static類型的,它們希望將狀態(tài)與線程進行關(guān)聯(lián)。這種變量在線程的生命周期內(nèi)起作用,可以減少同一個線程內(nèi)多個函數(shù)或者組件之間一些公共變量的傳遞的復雜度。
ThreadLocal主要api介紹
get()方法:獲取與當前線程關(guān)聯(lián)的ThreadLocal值。
set(T value)方法:設(shè)置與當前線程關(guān)聯(lián)的ThreadLocal值。
initialValue()方法:設(shè)置與當前線程關(guān)聯(lián)的ThreadLocal初始值。
remove()方法:將與當前線程關(guān)聯(lián)的ThreadLocal值刪除。
當調(diào)用get()方法的時候,若是與當前線程關(guān)聯(lián)的ThreadLocal值已經(jīng)被設(shè)置過,則不會調(diào)用initialValue()方法;否則,會調(diào)用initialValue()方法來進行初始值的設(shè)置。
通常initialValue()方法只會被調(diào)用一次,除非調(diào)用了remove()方法之后又調(diào)用get()方法,此時,與當前線程關(guān)聯(lián)的ThreadLocal值處于沒有設(shè)置過的狀態(tài)(其狀態(tài)體現(xiàn)在源碼中,就是線程的ThreadLocalMap對象是否為null),initialValue()方法仍會被調(diào)用。
initialValue()方法是protected類型的,很顯然是建議在子類重寫該函數(shù)的,所以通常該方法都會以匿名內(nèi)部類的形式被重寫,以指定初始值,例如:
創(chuàng)建一個ThreadLocalInstance
/**
* ThreadLocal類可以實現(xiàn)線程之間的單例
*/
public class ThreadLocalInstance {
private static ThreadLocal<ThreadLocalInstance>
threadLocalInstanceThreadLocal = new ThreadLocal<ThreadLocalInstance>(){
// 匿名內(nèi)部類重寫 initialValue方法
@Override
protected ThreadLocalInstance initialValue() {
// return super.initialValue();
return new ThreadLocalInstance();
}
};
// 私有化構(gòu)造器
private ThreadLocalInstance(){
}
public static ThreadLocalInstance getInstance(){
return threadLocalInstanceThreadLocal.get();
}
}
main線程中多次調(diào)用getInstance,在開辟新線程調(diào)用
ThreadLocalInstance instance1 = ThreadLocalInstance.getInstance();
ThreadLocalInstance instance2 = ThreadLocalInstance.getInstance();
ThreadLocalInstance instance3 = ThreadLocalInstance.getInstance();
ThreadLocalInstance instance4 = ThreadLocalInstance.getInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance3);
System.out.println(instance4);
Thread t1 = new Thread(new T());
Thread t2 = new Thread(new T());
t1.start();
t2.start();
System.out.println("main end");
查看打印結(jié)果
可以發(fā)現(xiàn)ThreadLocalInstance會在每一個線程都創(chuàng)建一個實例,同一線程多次調(diào)用都是調(diào)用的該實例,這樣就實現(xiàn)了線程的單例
spring中的bean單例模式
Spring單例Bean與單例模式的區(qū)別在于他們關(guān)聯(lián)的環(huán)境不一樣,單例模式是指在一個jvm進程中僅有一個實例,而Spring單例是指一個Spring Bean容器(ApplicationContext)中僅有一個實例。
單例設(shè)計模式,在一個JVM進程中(理論上,一個運行的Java程序,就必定有自己獨立的JVM)僅有一個實例,于是無論在程序的何處獲取實例,始終都返回同一個對象,以Java內(nèi)置的Runtime為例(現(xiàn)在枚舉是單例模式的最佳實踐),無論何時獲取,下面的判斷始終為真:
// 基于懶漢模式實現(xiàn)
// 在一個JVM實例中始終只有一個實例
Runtime.getRuntime() == Runtime.getRuntime()
與此相比,Spring的單例Bean是與其容器(ApplicationContext)密切相關(guān)的,所以在一個JVM進程中,如果有多個Spring容器,即使是單例bean,也一定會創(chuàng)建多個實例,代碼示例如下:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class,args);
// 第一個spring bean容器
AnnotationConfigApplicationContext context1 = new AnnotationConfigApplicationContext(Contextbean.class);
User user1 = context1.getBean("user1", User.class);
// 第二個spring bean容器
AnnotationConfigApplicationContext context2 = new AnnotationConfigApplicationContext(Contextbean.class);
User user2 = context2.getBean("user1", User.class);
System.out.println("==========");
// 這里絕對不會相等,因為創(chuàng)建了多個實例
System.out.println(user1==user2);
}
?
打印結(jié)果和預想的一樣果然不是同一個對象實例
附:配置類和實體類
Spring的配置類
@Configuration
public class Contextbean {
@Bean("user1")
public User getUser(){
return new User("zhangsan","18");
}
}
User實體類文章來源:http://www.zghlxwxcb.cn/news/detail-783203.html
public class User {
private String name;
private String age;
public User(String name, String age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
}
?文章來源地址http://www.zghlxwxcb.cn/news/detail-783203.html
到了這里,關(guān)于Java設(shè)計模式之單例模式的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!