关于单例设计模式,就这么多了!

设计模式(一)— 单例设计模式

1、什么是单例模式

​ 当我们需要某个类只创建一个实例的时候,就用到了单例设计模式。比如加载配置文件的类。

​ 单例模式便是创建型设计模式的一种。

​ 单例的可以分为三个主要的步骤来实现:

​ 私有化静态变量

​ 私有化构造

​ 创建一个public方法,供外界调用

2、饿汉式

public class Hungry {

    private Hungry() {}

    private static Hungry hungry = new Hungry();

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

​ hungry 为 static 的关系,在类加载过程中就会执行。由此带来的好处是Java的类加载机制本身为我们保证了实例化过程的线程安全性(没有线程安全的问题)。

​ 缺点是:不管我们是否使用这个类,它都会在加载时实例化,占用我们的空间。

3、懒汉式

public class Lazy {

    private static Lazy lazy = null;

    private Lazy() {}

    public static Lazy getInstance() {
        if(lazy == null) {
            lazy = new Lazy();
        }

        return lazy;
    }
}

​ 懒汉模式的好处就在于当我们用到的时候才会去实例化。

​ 缺点就是:单例时为了确保我们的系统中只存在一个实例,上述的代码在单线程中是能保证的,但是在多线程中就不能保证了。

​ 下边我们测试一下多线程下的单例:

public class Lazy {

    private static Lazy lazy = null;

    private Lazy() {
        System.out.println(Thread.currentThread().getName() + " is init !!!");
    }

    public static Lazy getInstance() {
        if(lazy == null) {      // ------------1
            lazy = new Lazy();   // ------------2
        }

        return lazy;
    }


    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Lazy.getInstance();
            }).start();
        }
    }
}

​ 如果我们多跑几次,一定会出现产生多个实例的情况。

Thread-0 is init !!!
Thread-2 is init !!!
Thread-1 is init !!!

原因解释:

​ 假设现在有两个线程A和B,首先A线程调用 getInstance 方法,当执行到语句1时会判断对象是否为空,由于该类还没被实例化,所以条件成立,遍进入到花括号中准备执行语句2,正如前面所说线程的切换是随机,当正准备执行语句2时,线程A突然停在这里了,CPU切换到线程B去执行。当线程B执行这个方法时,也会判断语句1的条件是否成立,由于A线程停在了语句1和2之间,实例还未创建,所以条件成立,也会进入到花括号中,注意此时线程B并未停止,而是顺利的执行语句2,创建了一个实例,并返回。然后线程B又切换回了线程A,别忘了,这时,线程A还停在语句1和2之间,切换回它的时候就又继续执行下面的代码,也就是执行语句2,创建了一个实例,并返回。这样,两个对象就被创建出来了,我们的单例模式也就失效了。

解决:

​ 给 getInstance 这个方法加把锁:

public static synchronized Lazy getInstance() {
    if(lazy == null) {
        lazy = new Lazy();
    }

    return lazy;
}

带来的问题

​ synchronized锁住了整个方法,降低了执行了效率。

4、 双重检查锁

​ 针对上边出现的问题,我们修改一下锁的范围,将同步方法改为同步代码块

public class Lazy {

    private static Lazy lazy = null;

    private Lazy() {
        System.out.println(Thread.currentThread().getName() + " is init !!!");
    }

    public static Lazy getInstance() {
        if(lazy == null) {     // -------1
            synchronized (Lazy.class){  // -------2
                lazy = new Lazy();   // -------3
            }
        }

        return lazy;
    }
}

​ 上边的代码会出现什么问题呢?

​ 如果有两个线程A、B调用 getInstance 方法。假设A先调用,当A调用方法时,会执行语句1进行条件判断,由于对象尚未创建,所以条件成立,正准备执行语句2来获取同步锁。我们上面也分析过了,线程的切换是随机的,还未执行语句2时,线程A突然停这了,切换到线程B执行。当线程B调用 getInstance 方法时也会执行语句1进行条件的判断,由于这时实例还未创建,所以条件成立,注意这时线程B还是没有停,又继续执行了语句2和3,即获取了同步锁并创建了Singleton对象。这时线程B切换回A,由于A此时还停在语句1和2之间,切回A时,就又继续执行语句2和3,即获取同步锁并创建了Singleton对象,这样两个对象就被创建出来了,synchronized 也失去了意义。

解决:

​ 在步骤2和步骤3中间在加一个空判断

public class Lazy {

    private static Lazy lazy = null;

    private Lazy() {
        System.out.println(Thread.currentThread().getName() + " is init !!!");
    }

    public static Lazy getInstance() {
        if(lazy == null) {
            synchronized (Lazy.class){
                if(lazy == null) {
                    lazy = new Lazy();
                }
            }
        }

        return lazy;
    }
}

​ 代码到了这一步,即解决了多线程下重复实例化的问题,也提高了代码的执行效率,同时也是懒加载,只有使用到的时候才会实例化。

现在还存在什么问题呢???

​ 我们先来了解一下java虚拟机在创建对象的时候,会具体做哪些事情呢?

  1. 在栈内创建 lazy 变量,在堆内存中开辟出一块空间用于存放Lazy实例对象,该空间会得到一个随机地址,假设为0x0001;

  2. Lazy 对象进行初始化;

  3. lazy 变量 指向该对象,也就是将该对象的地址0x0001赋值给 lazy变量,此时lazy就不为null了;

​ 还有就是程序的运行过程中实际上就是CPU在执行一条条的指令,有的时候CPU为了提高执行效率,会将指令的顺序打乱,但是不会影响到程序的运行结果,也就是所谓的指令重排序。

​ 了解了以上这些,我们再来看一下上边的代码会有什么问题呢?

​ 假设现在有两个线程A、B,CPU先切换到线程A,当执行上述创建对象语句时,假设是以132的顺序执行,当线程A执行完3时(执行完第3步后 lazy 就不为null了),突然停住了,CPU切换到了线程B去调用 getInstance 方法,由于 lazy 此时不为null,就直接返回了lazy,但此时步骤2是还没执行的,返回的对象还是未初始化的,这样程序也就出问题了。

解决:

这个时候就要用到我们的 volatile 了,volatile可以避免上述出现的指令重排序问题。

现在来看一下完整的DCL代码(Double Check Lock)

package com.zzp.design.singleton;

public class Lazy {

    private static volatile Lazy lazy = null;

    private Lazy() {
        System.out.println(Thread.currentThread().getName() + " is init !!!");
    }

    public static Lazy getInstance() {
        if(lazy == null) {
            synchronized (Lazy.class){
                if(lazy == null) {
                    lazy = new Lazy();
                }
            }
        }

        return lazy;
    }
}

​ 存在的问题:

​ 如果说我们只是简单的使用的话,到这里已经是可以使用了,但是这还不是最安全的。为什么这么说呢??? 因为java还有一个反射机制,它可以不管你的构造是否是私有的,都能拿到你的构造创建对象。

public static void main(String[] args) throws Exception {
    
    Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);  //设置为true,就能操作private修饰的变量或者是方法

    Lazy lazy1 = declaredConstructor.newInstance();
    Lazy lazy2 = declaredConstructor.newInstance();

    System.out.println(lazy1);
    System.out.println(lazy2);
}

​ 执行一下我们的代码:

com.zzp.design.singleton.Lazy@135fbaa4
com.zzp.design.singleton.Lazy@45ee12a7

​ 我们可以看到,我们通过反射创建的实例还是无法保证是唯一的,神奇吧!!!!那我们试着去解决一下它。

package com.zzp.design.singleton;

import java.lang.reflect.Constructor;

public class Lazy {

    private static volatile Lazy lazy = null;

    private static boolean flag = false;
    private Lazy() {
        if(!flag) {
            flag = true;
        }else{
            throw new RuntimeException("请不要试着用反射破坏单例!!!!");
        }
    }

    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 Exception {
        Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true); //设置为true,就能操作private修饰的变量或者是方法

        Lazy lazy1 = declaredConstructor.newInstance();
        Lazy lazy2 = declaredConstructor.newInstance();

        System.out.println(lazy1);
        System.out.println(lazy2);
    }
}

​ 这个时候再去打印一下我们的结果:

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.zzp.design.singleton.Lazy.main(Lazy.java:36)
Caused by: java.lang.RuntimeException: 请不要试着用反射破坏单例!!!!
	at com.zzp.design.singleton.Lazy.<init>(Lazy.java:14)
	... 5 more

​ 这样就解决了别人试图用反射破坏你的单例模式!!!!

​ 这就是道高一尺,魔高一丈!!!

​ 那有没有更简单快捷,同时又能避免上述问题的办法呢????

答案肯定是有的!!!

5、静态内部类实现单例

//静态内部类实现单例
public class Honner {

    // 只有调用该静态内部类时才会创建该对象
    public static class InnerClass {
        private static final Honner honner = new Honner();
    }

    private Honner () {
    }

    public static Honner getInstance() {
        return InnerClass.honner;
    }
}

​ 在我们的Honner类中创建一个InnerClass的静态内部类,在静态内部类中创建对象的实例。

​ 由于JVM的特性,只有在使用到静态内部类的时候,才会实例化该对象,实现了延迟加载,又避免了多线程下线程不安全的问题。

6、使用枚举实现单例

public enum User {
    user;  //这个user就相当于创建User对象的对象实例,也就是不需要创建对象,直接拿这个值就行
    private String userNm;

    public String getUserNm() {
        return userNm;
    }

    public void setUserNm(String userNm) {
        this.userNm = userNm;
    }
}

class test1{

    public static void main(String[] args) {
        User user1 = User.user; 
    }
}

​ 这样我们就能直接拿到枚举创建的单例。

​ 是因为枚举类的构造方法是私有的,你是无法调用到的并且你也无法通过反射来创建该实例,这也是枚举的独特之处。

枚举是不让我们通过反射来创建对象,所以是安全的。

现在我们通过反射来验证一下枚举到底能不能通过反射创建对象呢?

public static void main(String[] args) throws Exception {
    Constructor<User> declaredConstructor = User.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);

    User user = declaredConstructor.newInstance();

    System.out.println(user);
}

运行一下我们的程序:

在这里插入图片描述

这个时候我们的运行报错了,看一下原因。 是说User类没有无参的构造导致报错的。

这个时候就很奇怪了,明明User这个枚举类中没有构造方法,不是应该就只有一个默认的无参构造吗?

带着我们的疑惑,我们去看一下编译后的User类

在这里插入图片描述

我们看到IDEA中编译后的class文件中,User确实只有一个无参构造,咦???

感觉我们被它骗了,这个时候怎么办呢?

这个时候我们要用到一个jad反编译工具,Jad是可以将java中的**.class文件反编译成对应的.java文件**的一个工具。

下载链接 http://www.javadecompilers.com/jad

将jad拷贝到我们的User类路径下,执行命令 反编译出java文件

在这里插入图片描述

在这里插入图片描述

jad -s java User.class

这个时候就能拿到我们的java文件
在这里插入图片描述
在这里插入图片描述

打开看一下这个文件,我们可以看到,它的构造有两个参数,一个是String,一个是int

现在我们再去试一下我们的反射创建实例:

public static void main(String[] args) throws Exception {
    Constructor<User> declaredConstructor = User.class.getDeclaredConstructor(String.class, int.class);
    declaredConstructor.setAccessible(true);

    User user = declaredConstructor.newInstance();

    System.out.println(user);
}

我们运行一下我们的程序:

在这里插入图片描述

看到一个异常,说是不能够通过反射创建枚举对象。好了,到了这一步,我们验证出,枚举确实不能通过反射创建对象。

这个时候,我就又疑惑了,为什么枚举不能通过反射创建对象呢????

我们来看一下 newInstance 的源码:

在这里插入图片描述

它里边有这么一行话,如果这个类是枚举的话,就抛出异常。

Constructor<User> declaredConstructor = User.class.getDeclaredConstructor(String.class, int.class);
    declaredConstructor.setAccessible(true);

    User user = declaredConstructor.newInstance();

    System.out.println(user);
}

关于单例模式,就总结到这!!!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值