单例设计模式

单例设计模式

该设计模式解决的问题是,一个环境中只会存在一个该类的对象

该模式有五种实现方式

  1. 恶汉模式
  2. 懒汉模式
  3. 静态内部类模式
  4. 枚举模式

该设计模式的核心是构造方法私有化

饿汉模式

恶汉模式:见名知意,就是在类被加载的时候就创建。

案例:Runtime类

缺点:浪费内存空间

我查了资料,查到了这个缺点,但是这个缺点我不时很能理解

1
2
3
4
5
6
7
8
9
public class Hungry {
private final static Hungry HUNGRY = new Hungry();
private Hungry(){

}
public static Hungry getInstance(){
return HUNGRY;
}
}

懒汉模式

这种实现方式,我们从分几个版本从简单到复杂

Version 01

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LazyMan {
private static LazyMan lAZYMAN;
private LazyMan(){

}
public static LazyMan getInstance(){
if(lAZYMAN==null){
//A
lAZYMAN = new LazyMan();
}
return lAZYMAN;
}
}

缺点:只能在单线程中安全,多线程中是不安全的

现在有线程A和线程B,线程A走到了注解A处,然后线程B抢占了CPU,这时候lAZYMAN还是null,所以线程B创建了一个lAZYMAN对象,然后线程A抢回CPU,它这时候已经在if语句里面了,箭在弦上不得不发,所以线程A也创建了一个lAZYMAN对象,导致单例模式被破坏,所以该实现方式线程不安全

Version 02

1
2
3
4
5
6
7
8
9
10
11
12
public class LazyMan {
private static LazyMan LAZYMAN;
private LazyMan(){

}
public synchronized static LazyMan getInstance(){
if(LAZYMAN==null){
LAZYMAN = new LazyMan();
}
return LAZYMAN;
}
}

缺点:为方法加锁,确实保证了单例模式,但是我们都知道同步方法,锁的是LazyMan的Class类,所以我们得知这种方式可以保证单例模式的线程安全。但是我们好好想想,当我们创建了第一个单例对象之后,还需要锁来保证同步吗?答案显而易见,肯定是不需要的,所以这种方式效率低下,需要改进,我们来看下一个版本

Version 03

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LazyMan {
private static LazyMan LAZYMAN;
private LazyMan(){

}
public static LazyMan getInstance(){
if(LAZYMAN==null){
//A
synchronized (LazyMan.class){
LAZYMAN = new LazyMan();
}
}
return LAZYMAN;
}
}

我们不使用同步方法来保证所有的线程同步,我们只对LAZYMAN对象还为null的线程同步,这时候又出现了问题。我们来分析一下

现在我们有两个线程,线程A和线程B。线程A走到了注释A处,然后线程B抢占了CPU,这时候lAZYMAN还是null,所以线程B创建了一个lAZYMAN对象,然后线程A抢回CPU,它这时候已经在if语句里面了,箭在弦上不得不发,所以线程A也创建了一个lAZYMAN对象,导致单例模式被破坏,所以该实现方式线程不安全,我们发现这和我们没有加锁的时候的情形几乎一模一样。我们来看下一个版本,双重锁机制

Version 04

public class LazyMan {
    private static LazyMan LAZYMAN;
    private LazyMan(){
    
    }
    public static LazyMan getInstance(){
        if(LAZYMAN==null){
            //A
            synchronized (LazyMan.class){
                if(LAZYMAN==null){
                    //B
                    LAZYMAN = new LazyMan();
                }
            }
        }
        return LAZYMAN;
    }
}

我个人的理解是第一重锁是为了过滤LAZYMAN为NULL的线程,第二重锁是为了保证只能有一个线程去创建对象,这时候我们似乎找不到什么破绽了,我们来继续往下看

Version 05

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LazyMan {
private volatile static LazyMan LAZYMAN;
private LazyMan(){

}
public static LazyMan getInstance(){
if(LAZYMAN==null){
//A
synchronized (LazyMan.class){
if(LAZYMAN==null){
//B
LAZYMAN = new LazyMan();
}
}
}
return LAZYMAN;
}
}

乍一看,好像和上一个版本没什么区别,仔细一看,其实区别是在这个单例变量的修饰符多了一个volatile。

volatile三大作用

  1. 保证可见性
  2. 不保证原子性
  3. 禁止该变量出现的地方发生指令重排

这里的话,我们使用到的特性是第三点,我们来分析一下 LAZYMAN = new LazyMan();

这一句话其实执行了三个步骤

  1. 开辟一个内存空间
  2. 初始化对象
  3. 将该内存空间的地址指向变量

JVM内部会有一种叫指令重排的代码优化机制,这时候,这三者的顺序就有可能会改变。

现在我们有两个线程,线程A和线程B。线程A走到了注释A处,然后线程B抢占了CPU,开始执行LAZYMAN = new LazyMan();开辟内存空间后,没有先初始化对象,而是先将该内存空间的地址指向变量,这时候线程A抢回了CPU,这时候的LAZYMAN已经不是NULL了,直接将其返回了出去,但是这个对象其实还没有初始化,还是个空架子,这就出现了问题。

当然这种情况十分十分少,但是为了严谨性,最好还是加上volatile关键词禁止指令重排

静态内部类模式

1
2
3
4
5
6
7
8
9
10
11
public class StaticWay {
private StaticWay(){

}
public static class InnerClass{
private static final StaticWay STATICWAY = new StaticWay();
}
public static StaticWay getInstance(){
return InnerClass.STATICWAY;
}
}

该实现方式其实就是利用的JVM的一些特性

类的加载分为以下过程

  • 类的加载:将Class文件字节码加载到内存中,并将这些静态数据转换为方法区的运行时数据结构,然后在堆中生成一个代表该类的Class对象,作为方法区类数据的访问入口,可以使用反射获取该类的所有信息。

  • 类的链接:将Java类的二进制代码合并到JVM中

    • 验证:确保加载的类符合JVM规范
    • 准备:为静态变量分配内存并设置成员变量的默认值
    • 解析:JVM常量池内的符号引用(常量名)替换为直接引用(地址)的过程
  • 类的初始化:执行类构造器< clinit >()方法的过程,当初始化一个类的时候,如果其父类为初始化,则先初始化其父类 ,JVM会保证类构造器在多线程环境下被正确加锁

其实最开始的饿汉单例模式也是也是这个原理

颠覆

其实一路到了这里,以上方法都是不安全的。因为java有一个很牛X的包。

java.lang.reflect

我们在最开始说过,单例模式的本质就是构造器私有化,但是java的反射机制,它不讲武德,它可以直接修改权限修饰符。我们来看看代码。

1
2
3
4
5
6
7
8
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<Hungry> constructor = Hungry.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
Hungry hungry1 = Hungry.getInstance();;
Hungry hungry2 = constructor.newInstance();
System.out.println(hungry1==hungry2);
}
//可想而知最后的结果是 false

可不可以对其作出防御呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Hungry(){
if(HUNGRY!=null){
throw new IllegalArgumentException("请不要尝试使用反射破坏单例模式");
}
}
Exception in thread "main" java.lang.reflect.InvocationTargetException
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490)
at com.lizhi.hungry.Hungry.main(Hungry.java:26)
Caused by: java.lang.IllegalArgumentException: 请不要尝试使用反射破坏单例模式
at com.lizhi.hungry.Hungry.<init>(Hungry.java:15)
... 5 more

上述方法对饿汉已经作出了合适的防御,那对于懒汉模式,如果两个对象都是使用反射创建的呢?这时候这个防御就是形同虚设了,因为我们肯定是禁止反射去创建对象,所以这种肯定是不行的

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
public class LazyMan {
private static LazyMan LazyMan;
private LazyMan(){
if(LAZYMAN!=null){
throw new IllegalArgumentException("请不要尝试使用反射破坏单例模式");
}else {
synchronized (LazyMan.class){
LazyMan = new LazyMan();
}
}
}
public static LazyMan getInstance(){
if(LazyMan==null){
synchronized (LazyMan.class){
if(LazyMan==null){
LazyMan = new LazyMan();
}
}
}
return LazyMan;
}

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan lazyMan1 = constructor.newInstance();
LazyMan lazyMan2 = LazyMan.getInstance();
System.out.println(lazyMan1==lazyMan2);
}
}

我们引入一个布尔值来完成这个任务

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
public class LazyMan {
private volatile static LazyMan LazyMan;
private volatile static boolean flag ;
private LazyMan(){
if(flag){
throw new IllegalArgumentException("请不要尝试使用反射破坏单例模式");
}else {
flag=true;
}
}
public static LazyMan getInstance(){
if(LazyMan==null){
synchronized (LazyMan.class){
if(LazyMan==null){
LazyMan = new LazyMan();
}
}
}
return LazyMan;
}

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan lazyMan1 = constructor.newInstance();
// LazyMan lazyMan2 = constructor.newInstance();
LazyMan lazyMan2 = LazyMan.getInstance();
// LazyMan lazyMan1 = LazyMan.getInstance();
System.out.println(lazyMan1==lazyMan2);
}
}

这样反射就无法控制我们的单例了!

枚举类

这时候有没有想起来我们在学javaSE的时候就学到了一个自带单例模式的类型,那就是枚举类Enum!

我们来尝试攻击一下枚举类!

1
2
3
4
5
6
7
8
9
10
11
public enum SingleEnum {
SINGLETON;

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<SingleEnum> constructor = SingleEnum.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
SingleEnum single1 = constructor.newInstance("single", 2);
SingleEnum single2 = constructor.newInstance("single", 2);
System.out.println(single1==single2);
}
}

这时候发现报错了!!

1
2
3
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:484)
at com.lizhi.SingleEnum.main(SingleEnum.java:12)

我们点进newInstance方法看下源码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@CallerSensitive
@ForceInline // to ensure Reflection.getCallerClass optimization
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, clazz, modifiers);
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}

我们发现了这句话throw new IllegalArgumentException("Cannot reflectively create enum objects");

这就解释通了,原来是java的反射已经处理好了枚举类,使其真正的实现了安全的单例!

给作者买杯咖啡吧~~~