①餓漢式
public class Hungry {
private Hungry(){}
private static final Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
public static void main(String[] args) {
Hungry hungry = Hungry.getInstance();
System.out.println(hungry);
}
}
一上來(lái)就把對(duì)象new出來(lái)晕拆,并用
static final
修飾昌妹,通過getInstance
方法返回該對(duì)象
這種方式缺點(diǎn)也很明顯寝衫,不管用沒用這個(gè)對(duì)象柬批,都先new出來(lái)啸澡,浪費(fèi)了內(nèi)存
②懶漢式
public class Lazy {
private Lazy(){
System.out.println(Thread.currentThread().getName()+"調(diào)用了構(gòu)造函數(shù)");
}
private static Lazy lazy;
public static Lazy getInstance(){
if(lazy == null){
lazy = new Lazy();
}
return lazy;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
Lazy.getInstance();
}).start();
}
}
}
Thread-0調(diào)用了構(gòu)造函數(shù)
Thread-5調(diào)用了構(gòu)造函數(shù)
Thread-3調(diào)用了構(gòu)造函數(shù)
Thread-9調(diào)用了構(gòu)造函數(shù)
Thread-1調(diào)用了構(gòu)造函數(shù)
Thread-2調(diào)用了構(gòu)造函數(shù)
這種方式相對(duì)于懶漢式有所改進(jìn),在類中只聲明對(duì)象萝快,不實(shí)例化锻霎;調(diào)用
getInstance
方法時(shí),對(duì)象為null則實(shí)例化揪漩,否則直接返回旋恼。按理說(shuō)只有第一次調(diào)用getInstance
方法時(shí)會(huì)調(diào)用構(gòu)造函數(shù),new對(duì)象奄容,以后都直接return冰更。單線程下確實(shí)沒問題,但是多線程時(shí)昂勒,由運(yùn)行結(jié)果可知蜀细,構(gòu)造函數(shù)被多次調(diào)用,顯然不符合單例模式戈盈。問題就出在多個(gè)線程同時(shí)調(diào)用了
getInstance
方法奠衔,解決方法就是通過synchronized
加鎖同步。
public class Lazy {
private Lazy() {
System.out.println(Thread.currentThread().getName() + "調(diào)用了構(gòu)造函數(shù)");
}
private static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Lazy.getInstance();
}).start();
}
}
}
Thread-0調(diào)用了構(gòu)造函數(shù)
這時(shí)只有第一個(gè)線程調(diào)用了構(gòu)造函數(shù)塘娶,符合預(yù)期的結(jié)果归斤。
但是事實(shí)上還要考慮另一個(gè)問題:當(dāng)new一個(gè)對(duì)象時(shí),并不是一個(gè)原子操作刁岸,可以分為三個(gè)步驟:①為對(duì)象分配內(nèi)存空間脏里,②實(shí)例化對(duì)象,③向?qū)ο蟮囊弥赶蛟搩?nèi)存空間虹曙。正常情況下按照①②③順序執(zhí)行是沒問題的迫横,但是cpu可能會(huì)指令重排番舆,執(zhí)行順序變?yōu)棰佗邰冢@時(shí)如果一個(gè)線程執(zhí)行到③(即對(duì)象引用指向了內(nèi)存空間矾踱,不為null恨狈,但是由于對(duì)象還沒有實(shí)例化,這塊內(nèi)存什么都沒有)介返,另外一個(gè)線程同時(shí)調(diào)用了
getInstance
方法拴事,判斷l(xiāng)azy不為null沃斤,直接返回了lazy圣蝎,就出問題了。解決方法就是使用volatile
關(guān)鍵字禁止指令重排衡瓶。
public class Lazy {
private Lazy() {
System.out.println(Thread.currentThread().getName() + "調(diào)用了構(gòu)造函數(shù)");
}
private volatile static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Lazy.getInstance();
}).start();
}
}
}
以上就是雙重檢測(cè)鎖DCL
為什么要判斷兩次
lazy == null
徘公?
假設(shè)第一個(gè)線程進(jìn)入同步塊new對(duì)象的過程中,第二個(gè)線程同時(shí)調(diào)用了getInstance
方法哮针,因?yàn)閷?duì)象還沒有被實(shí)例化关面,外面的if不成立,所以也將會(huì)執(zhí)行同步塊中的代碼十厢,但是目前處于阻塞狀態(tài)等太。等到第一個(gè)線程創(chuàng)建完對(duì)象,第二個(gè)線程進(jìn)入同步塊蛮放,就會(huì)判斷里面的if不成立(第一個(gè)線程已經(jīng)實(shí)例化了對(duì)象)缩抡,就不會(huì)再new了,如果沒有里面的if包颁,第二個(gè)線程就會(huì)又new一個(gè)瞻想,單例模式就不成立了。
以上單例模式娩嚼,由于java反射機(jī)制的存在蘑险,都是不安全的,如②懶漢式岳悟,可以通過如下反射破環(huán)單例模式:
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class Lazy {
private Lazy() {
System.out.println(Thread.currentThread().getName() + "調(diào)用了構(gòu)造函數(shù)");
}
private volatile static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException {
Lazy.getInstance();
Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor();
constructor.setAccessible(true);
constructor.newInstance();
}
}
main調(diào)用了構(gòu)造函數(shù)
main調(diào)用了構(gòu)造函數(shù)
上述代碼中佃迄,先通過
getInstance
方法正常創(chuàng)建對(duì)象,然后又通過反射newInstance
創(chuàng)建對(duì)象贵少。由運(yùn)行結(jié)果可知呵俏,兩次創(chuàng)建對(duì)象都調(diào)用了構(gòu)造函數(shù),因?yàn)榉瓷渫ㄟ^setAccessible(true)
破壞了構(gòu)造函數(shù)的私有性春瞬,使得單例模式被破壞柴信。
如果加一個(gè)字段驗(yàn)證呢?
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class Lazy {
private static boolean flag = false;
private Lazy() {
if(!flag){
flag = true;
System.out.println(Thread.currentThread().getName() + "調(diào)用了構(gòu)造函數(shù)");
}else{
throw new RuntimeException("不要試圖通過反射破壞單例宽气!");
}
}
private volatile static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException {
Lazy.getInstance();
Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor();
constructor.setAccessible(true);
constructor.newInstance();
}
}
main調(diào)用了構(gòu)造函數(shù)
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 Lazy.main(Lazy.java:45)
Caused by: java.lang.RuntimeException: 不要試圖通過反射破壞單例随常!
at Lazy.<init>(Lazy.java:13)
... 5 more
這次加了一個(gè)flag字段潜沦,用在構(gòu)造函數(shù)里,判斷是否是第一次調(diào)用绪氛,第二次再調(diào)用時(shí)就會(huì)拋出異常唆鸡,這樣就無(wú)法通過反射無(wú)限制地創(chuàng)建對(duì)象了。
但是仍然是存在不安全性的枣察,以上結(jié)論是在不知道這個(gè)字段的情況下實(shí)現(xiàn)的争占。所謂道高一尺,魔高一丈序目,如果這個(gè)驗(yàn)證字段被破解了臂痕,就可以通過反射重新設(shè)置它的值。
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class Lazy {
private static boolean flag = false;
private Lazy() {
if (!flag) {
flag = true;
System.out.println(Thread.currentThread().getName() + "調(diào)用了構(gòu)造函數(shù)");
} else {
throw new RuntimeException("不要試圖通過反射破壞單例猿涨!");
}
}
private volatile static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchFieldException {
// 正常創(chuàng)建第一個(gè)對(duì)象
Lazy lazy1 = Lazy.getInstance();
// 反射修改flag
Field flag = Lazy.class.getDeclaredField("flag");
flag.setAccessible(true);
flag.set(lazy1, false);
// 通過反射再次創(chuàng)建對(duì)象
Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor();
constructor.setAccessible(true);
constructor.newInstance();
}
}
main調(diào)用了構(gòu)造函數(shù)
main調(diào)用了構(gòu)造函數(shù)
③靜態(tài)內(nèi)部類
public class External {
private External(){}
public static class Internal{
private static final External EXTERNAL = new External();
}
public static External getInstance(){
return Internal.EXTERNAL;
}
}
將類的實(shí)例作為靜態(tài)內(nèi)部類的成員屬性握童,再通過
getInstance
方法獲得
④枚舉
以上三種方法都存在不安全性,那么怎么才能保證安全呢叛赚,查看newInstance
源碼澡绩,直接就能看到這段代碼:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
不能通過反射創(chuàng)建枚舉對(duì)象!
枚舉類型其實(shí)是自帶單例模式
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum EnumSingle {
INSTANCE;
public static EnumSingle getInstance() {
return INSTANCE;
}
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException {
EnumSingle instance1 = EnumSingle.getInstance();
Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor();
constructor.setAccessible(true);
EnumSingle instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
Exception in thread "main" java.lang.NoSuchMethodException: EnumSingle.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at EnumSingle.main(EnumSingle.java:17)
好像哪里不對(duì)俺附?出錯(cuò)是必然的肥卡,但是錯(cuò)誤信息應(yīng)該是上面說(shuō)的
Cannot reflectively create enum objects
,而不是這個(gè)事镣。
NoSuchMethodException
步鉴,沒有該方法,顯然是構(gòu)造函數(shù)不對(duì)蛮浑,通過反編譯class文件唠叛,得知構(gòu)造函數(shù)有兩個(gè)參數(shù),分別為String
和int
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum EnumSingle {
INSTANCE;
public static EnumSingle getInstance() {
return INSTANCE;
}
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException {
EnumSingle instance1 = EnumSingle.getInstance();
Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
EnumSingle instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at EnumSingle.main(EnumSingle.java:19)
這才是想要的異常
得出結(jié)論:通過枚舉實(shí)現(xiàn)的單例模式是安全的