常见单例模式五种设计方式

Posted by SFHJavaer on 2022-04-17
Estimated Reading Time 7 Minutes
Words 1.8k In Total
Viewed Times

常见单例模式五种设计方式:

1.饿汉式(相对于懒汉式)
饿汉即没有调用getIntance时就已经创建了对象了,因为是静态的被初始化了

构造方法必须是私有的,除了本类不能调用;

提供一个静态的成员变量,值为用私有构造出创建的对象;

提供一个静态的方法getIntance用来返回静态实例,每一次调用该方法都是得到的同一对象

注意问题:静态块和变量在加载的时候就会执行和初始化,所以像下面的代码,在加载INSTANCE变量时会进行new Singleton,所以会调用构造方法,之后再进行调用main方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.company;

import java.io.Serializable;

//饿汉式
public class Singleton implements Serializable {
private Singleton() {
System.out.println("private Singleton()构造");
}

private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance(){
return INSTANCE;
}

public static void otherMethod() {
System.out.println("OtherMethod()执行");
}
}

如何进行编写方法的记忆:(比如普通懒汉式)
首先java基础语法一定不要忘了,构造方法后面必须有(),方法是访问修饰符+(返回值)+方法名(){},构造方法可写可不写,单例里面使用无参构造创建对象即可,无需有参因为instance不需要赋值,是在get中手动复制的,如果有有参构造了岂不是说外面可以进行instance属性赋值了?但是如果这里用private修饰构造的话外部仍然是不能通过new创建对象的,我们的绣球也正是只能由本类调用构造,如果创建对象时使用new有参构造进行创建的话,结果其实就是instance引用指向的创建的Singleton对象的属性instance用有参赋上值了而已,一定要理清楚关系。
另外getInstance方法返回的是一个Singleton对象,我们写单例的目的和创建这个静态变量的目的就是用这个静态变量作为单例,所以当instance为空时,当然是给instance赋上对象了,用无参构造创建一个,如果直接return Singleton()的话,这个对象是返回了,但是instance始终为空,还能满足单例吗?
方法一定是public,因为外部需要调该方法来获取对象,注意定义的instance如果是public的,即外部可以直接类名.调这个对象,其实是不影响单例的,具体对象的创建是要看getInstance的
instance不能加final,因为final代表的是不可变量或常量,对于fianl修饰的变量一定要显式赋值,所谓显式赋值也就是说(程序员)变量后面必须有等于号,赋个空值null也代表赋值了,如果没显式赋值编译不通过,即便是=null,后面也不能再修改了,即不能再用=再赋值了,所以这里的instance明显不能用final,因为要把无参构造的新对象赋值给instance(无参的话:instance.instance=null,卡缪前面的instance只是个引用,后面的是引用的属性即引用所指向的对象的属性,引用只是对象的代名词)

但是上面的单例模式会被三种方式破坏:

1.反射破坏

解决:在构造方法执行代码前加入条件判断

1
2
3
4
if(INSTANCE != null){
throw new RuntimeException("对象已创建");
}
sout("正常创建!");

2.反序列化破坏

解决:在Singleton类中加一个方法,反序列化时检测到该方法被重写,就会执行并返回INSTANCE而不是新对象

1
2
3
public Object readResolve(){
return INSTANCE;
}

3.Unsafe类破坏(未解决)

2.枚举饿汉类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.company;
//枚举饿汉
public enum Singleton2 {
INSTANCE;
private Singleton2(){
System.out.println("private Singleton()");
}
public String toString(){
return getClass().getName()+"@"+Integer.toHexString(hashCode());
}
public static Singleton2 getInstance(){
return INSTANCE;
}
public static void otherMethod(){
System.out.println("otherMethod()");
}
}

枚举在底层已经解决了反射和反序列化破坏(没解决safe)

3.懒汉式(synchronized性能太低)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.company;

public class Singleton3 {
private Singleton3(){
System.out.println("private ()");
}
private static Singleton3 INSTANCE = null;
//直接在创建实例的方法上进行加锁,避免多线程并发重复创建
public static synchronized Singleton3 getInstance(){
if(INSTANCE != null){
INSTANCE = new Singleton3();
}
return INSTANCE;
}
public static void otherMethod(){
System.out.println("otherMEthod()");
}
}

4.DCL懒汉(双检锁懒汉)

不需要每一次检测都要加锁,只需要在并发情况下第一次创建对象的时候进行加锁

一定要进行两次if判断,外面是为了创建完对象之后不在进入同步块中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.company;
//DCL懒汉
public class Singleton4 {
private Singleton4(){
System.out.println("private ()");
}
private static volatile Singleton4 INSTANCE = new Singleton4();
public static Singleton4 getInstance(){
if (INSTANCE != null){
//直接锁的类对象
synchronized (Singleton4.class){
if(INSTANCE != null){
INSTANCE = new Singleton4();
}
}
}
return INSTANCE;
}
}

PS:DCL懒汉创建出的对象必须被volatile修饰,这里是为了保证有序性,不然多线程下会导致创建对象、连接引用指令和if判断之间的错误,不然还是会出现重复创建对象

推荐:5、内部类懒汉式

在单例对象类内部创建静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.company;
//最终版
public class Singleton5 {
private Singleton5(){
System.out.println("private ()");
}
public static class Holder{
static Singleton5 INSTANCE = new Singleton5();
}
public static Singleton5 getInstance(){
return Holder.INSTANCE;
}
public static void otheMethod(){
System.out.println("otherMethod()");
}
}

既保留了懒汉的优良特性,又保证不会出现线程安全破坏单例的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package handwriting;
//普通饿汉式
//public class Singleton {
// private Singleton(){
// if(INSTANCE != null){
// throw new RuntimeException("对象已创建");
// }
// System.out.println("单例构造");
// }
// private static final Singleton INSTANCE = new Singleton();
// //重写readResove方法,反序列化时会自动检测
// public Object readResolve(){
// return INSTANCE;
// }
// public static Singleton getINSTANCE() {
// return INSTANCE;
// }
//}

//懒汉(加syn线程安全)
public class Singleton {
private static Singleton instance;
private Singleton(){
System.out.println("构造");
}
public static synchronized Singleton getInstance(){
if(instance != null){
instance = new Singleton();
}
return instance;
}


}

//public class Singleton {}
/***
* 双检锁的第一个if是为了避免所有的线程都进入syn而产生多余的消耗,第二个if是为了避免多线程并发进入没有syn包括的第一个if
* 注意双检锁的变量一定要volatile
*/
//public class Singleton {}
//public class Singleton {}
/***
* 一般来说,单例模式有五种写法:懒汉、饿汉、双重检验锁、静态内部类、枚举。
* 上述所说都是线程安全的实现,文章开头给出的第一种方法不算正确的写法。
* 就我个人而言,一般情况下直接使用饿汉式就好了,
* 如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,
* 如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例
* spring依赖注入时,使用了 双重判断加锁(DCL) 的单例模式。
*/


如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !