你給我解釋解釋裸燎?什么叫Tm單例模式顾瞻?
餓漢式
顧名思義:在類加載時就進(jìn)行單例對象的創(chuàng)建,十分簡單德绿。
代碼如下:
public class Hungry {
// 餓漢式單例
private Hungry(){
// 模擬類中屬性
byte[] data1 = new byte[1024];
byte[] data2 = new byte[1024];
byte[] data3 = new byte[1024];
}
private static Hungry hungry = new Hungry();
public static Hungry getInstance(){
return hungry;
}
}
存在的問題是顯而易見的:如果類始終不需要使用荷荤,單例對象還是會一直占用內(nèi)存資源
懶漢式
僅在需求單例對象時才進(jìn)行創(chuàng)建,把對象的創(chuàng)建時機(jī)延后了移稳。
public class LazyMan {
// 懶漢式
private LazyMan(){
System.out.println(Thread.currentThread().getName()+" is running constructor");
}
private static LazyMan lazyMan = null;
private static LazyMan getInstance(){
if (lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(LazyMan::getInstance).start();
}
}
}
但是這種方式很容易看出在多線程中是不安全的蕴纳。
考慮多個線程同時調(diào)用getInstance()
方法并成功進(jìn)入了if
的條件中,就會出現(xiàn)多個對象秒裕。
? Thread-0 is running constructor
? Thread-1 is running constructor
DCL懶漢式
使用雙重判斷,并加鎖的方法钞啸。
樣例:
public class DCLLazy {
//雙重檢測
private DCLLazy(){
System.out.println(Thread.currentThread().getName()+" is running constructor");
}
private static DCLLazy dclLazy = null;
private static DCLLazy getInstance(){
if (dclLazy == null){
synchronized (DCLLazy.class){
if (dclLazy == null){
dclLazy = new DCLLazy();
}
}
}
return dclLazy;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
getInstance();
}).start();
}
}
}
- 第一次判斷是判斷當(dāng)前是否有對象几蜻,這里并不會有線程安全問題。(如果有体斩,多個線程都返回對象梭稚,如果沒有多個線程進(jìn)入內(nèi)部嘗試創(chuàng)建對象)
- 加鎖確保只會有一個線程進(jìn)入去創(chuàng)建對象
- 第二次判斷,因?yàn)槌顺晒?chuàng)建對象的那個線程絮吵,其他線程都在阻塞等待弧烤。而等待到了鎖也沒有創(chuàng)建的必要了,為了防止多次創(chuàng)建這里再進(jìn)行一次判斷
靜態(tài)內(nèi)部類
public class SLazy {
//靜態(tài)內(nèi)部類
private SLazy(){
System.out.println(Thread.currentThread().getName()+" is running constructor");
}
private static class SLazyHolder{
private static final SLazy instance = new SLazy();
}
public static SLazy getInstance(){
return SLazyHolder.instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
getInstance();
}).start();
}
}
}
DCL和靜態(tài)內(nèi)部類的特點(diǎn)是類似的蹬敲,保證了線程安全暇昂,會受指令重排影響
其實(shí)這樣的DCL和靜態(tài)內(nèi)部類也還是有問題的,問題在于指令重排序伴嗡。
真正安全的單例方式應(yīng)該對單例對象加上volatile
關(guān)鍵字急波。(這是volatile
禁止指令重排的重要應(yīng)用,務(wù)必記妆裥!)
new
方法并不是原子操作澄暮。大致有以下三步
- 申請內(nèi)存空間
- 調(diào)用構(gòu)造器方法
- 將對象指向內(nèi)存
而實(shí)際上因?yàn)橹噶钪嘏诺脑蛎危襟E2,3順序可能顛倒泣懊。導(dǎo)致對象指向了一個沒有成功構(gòu)造的內(nèi)存伸辟。而其他線程會認(rèn)為已經(jīng)構(gòu)造,導(dǎo)致調(diào)用沒有構(gòu)造的對象馍刮。
反射破解
使用如下代碼就可以跨過getInstance()
方法直接構(gòu)造
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class clazz = DCLLazy.getInstance().getClass();
Constructor declaredConstructor = clazz.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
DCLLazy instance1 = (DCLLazy) declaredConstructor.newInstance();
DCLLazy instance2 = (DCLLazy) declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
main is running constructor
main is running constructor
main is running constructor
com.baidu.ai.aip.auth.single.DCLLazy@74a14482
com.baidu.ai.aip.auth.single.DCLLazy@1540e19d
可以看到對象被構(gòu)建了三次
那么對構(gòu)造函數(shù)進(jìn)行如下升級
private DCLLazy(){
synchronized (DCLLazy.class){
if(dclLazy != null){
throw new RuntimeException("請不要使用反射進(jìn)行創(chuàng)建");
}
}
System.out.println(Thread.currentThread().getName()+" is running constructor");
}
再次嘗試
main is running constructor
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.baidu.ai.aip.auth.single.DCLLazy.main(DCLLazy.java:37)
Caused by: java.lang.RuntimeException: 請不要使用反射進(jìn)行創(chuàng)建
at com.baidu.ai.aip.auth.single.DCLLazy.<init>(DCLLazy.java:13)
... 5 more
可以看到這種方式已經(jīng)行不通了信夫。
但如果不通過單例對象獲取字節(jié)碼,直接通過字節(jié)碼得到構(gòu)造方法進(jìn)行構(gòu)造渠退,這種方式仍然是會被破解的忙迁。
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// Class clazz = DCLLazy.getInstance().getClass();
Constructor declaredConstructor = DCLLazy.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
DCLLazy instance1 = (DCLLazy) declaredConstructor.newInstance();
DCLLazy instance2 = (DCLLazy) declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
可以看到成功創(chuàng)建了兩個對象
main is running constructor
main is running constructor
com.baidu.ai.aip.auth.single.DCLLazy@74a14482
com.baidu.ai.aip.auth.single.DCLLazy@1540e19d
總結(jié):無論是增加判斷,鎖碎乃,還是私有屬性進(jìn)行檢查姊扔,都沒辦法阻止反射通過獲取私有構(gòu)造器,并直接構(gòu)造梅誓。
枚舉類型
枚舉類型相比之前的方式最大的優(yōu)點(diǎn)在于不會被反射破解恰梢。
public enum EDemo {
INSTANCE;
public EDemo getInstance(){
return INSTANCE;
}
}