单例模式

1、定义

单例模式就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其实例的方法。
构造方法私有化

2、实现方式

2.1、饿汉式

顾名思义:饿汉,非常饿,该类被加载的时候就会马上实例化,一刻也不会等待
构造私有化
对象私有化
公共静态getInstance方法用来获取对象

public class Hungry {
    // 构造器私有化是为了方式new 去创建对象
    private Hungry(){
        System.out.println(Thread.currentThread().getName()+"成功了");
    }

    private static final Hungry HUNGRY = new Hungry();
    // 暴露公共方法是为了返回对象
    public static Hungry getInstance(){
        return HUNGRY;
    }

    // 多线程测试
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                Hungry.getInstance();
            }).start();
        }
    }
}

image

优点:

  • 写法简单,在类装载的时候就实现了实例化。避免了线程同步的问题
  • 调用速度快,因为在类装载的时候对象已经创建完毕了

缺点:

  • 因为在类装载的时候就已经完成了实例化,如果这个类一直用不到,造成了内存占用,以及实例的浪费

2.2、懒汉式(线程不安全)

顾名思义:懒汉比较懒,在类被加载的时候什么也不会干,只有当需要他的时候才会被实例化

public class Lazy {
    private Lazy(){
        System.out.println(Thread.currentThread().getName()+"成功了");
    }

    // 在类加载的时候先不创建对象
    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();
        }
    }
}

运行了多次,发现多线程状态下不安全
image

优点:

  • 解决了饿汉模式的缺点

缺点:

  • 只能在单线程下使用,线程不安全
  • 多线程状态下,一个线程执行到if(LAZY == null),还没来得及往下执行,另外一个线程也通过了这个判断语句,就会产生多个实例(上边的演示结果)

2.3、DCL懒汉式(双重检查)

public class Lazy {
    private Lazy(){
        System.out.println(Thread.currentThread().getName()+"成功了");
    }

    // volatile关键字的所用是防止指令重排
    private volatile static Lazy LAZY;

    // 同步代码块   更推荐使用  效率更高
    public static Lazy getInstance(){
        // 如果对象为空,再让线程去new对象
        if(LAZY == null){
            // 设置线程同步,每次只让一个线程去访问
            synchronized (Lazy.class){
                if(LAZY == null){
                    LAZY = new Lazy();
                    /*
                    因为new Lazy()的步骤有三步,且这三步不是原子性操
                    1、分配内存空间
                    2、执行构造方法,初始化对象
                    3、将对象指向分配的内存空间
                    在这三步过程中,期望执行123,但在极端情况下有一个线程A可能执行的步骤是132,
                    在执行步骤3的时候,A线程已经将LAZY对象的地址指向了内存中,
                    如果此时有一个有一个线程B调用了LAZY对象那么此时LAZY对象是空的可能会引发异常
                    所以需要给对象加volatile关键字防止指令重排
                    */
                }
            }
        }
        return LAZY;
    }

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

image

优点

  • 解决了线程同步的问题

2.4、静态内部类

public class Holder {
    private Holder(){

    }

    //通过内部类来获取对象
    public static synchronized Holder getInstance(){
        return Inner.HOLDER;
    }

    // 在内部类中创建Holder类的实例
    public static class Inner{
        private static final Holder HOLDER = new Holder();
    }
}

优点:

  • 避免了线程不安全,利用静态内部类特点实现延迟加载,效率高。

2.5、枚举类

public enum EnumSingle {
    INSTANCE;

    public static EnumSingle getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args) {
        EnumSingle instance = EnumSingle.INSTANCE;
        EnumSingle instance2 = EnumSingle.INSTANCE;
        System.out.println("instance的hashCode" + instance.hashCode());
        System.out.println("instance2的hashCode" + instance2.hashCode());
    }
}

结果

instance的hashCode460141958
instance2的hashCode460141958

优点:

  • 不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

3、注意点

懒汉式是我们一般最想要的东西,但是反射可以去破坏私有方法和属性,导致单例模式失效,下面我去演示一下
通过DCL懒汉获取一个对象后,通过反射再创建一个新的对象,查看效果

public class Lazy {
    private Lazy(){
        System.out.println(Thread.currentThread().getName()+"成功了");
    }

    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) throws Exception{
        // 获取到Lazy类的实例
        Lazy instance = Lazy.getInstance();
        // 虽然在方法getInstance中已经设置了线程同步
        // 通过反射来获取Lazy类中的空构造方法
        Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
        // 无视私有属性的构造器
        declaredConstructor.setAccessible(true);
        // 通过构造器再去创建一个对象
        Lazy instance1 = declaredConstructor.newInstance();
        // 比较两个对象的hashCode
        System.out.println("instance的hashCode:" + instance.hashCode());
        System.out.println("instance1的hashCode:" + instance1.hashCode());
    }
}

结果:

main成功了
main成功了
instance的hashCode:460141958
instance1的hashCode:1163157884

发现两个对象的hashCode并不一致,你可能会想到了,我在无参构造里边判断LAZY对象是否为空,如果不为空抛出一个运行期异常并且去加一个锁不就好了么,确实可以。来下边修改无参构造

    private Lazy(){
        // System.out.println(Thread.currentThread().getName()+"成功了");
        synchronized (Lazy.class){
            if(LAZY != null){
                throw new RuntimeException("反射创建对象异常");
            }
        }
    }

结果:

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.single.Lazy.main(Lazy.java:42)
Caused by: java.lang.RuntimeException: 反射创建对象异常
	at com.single.Lazy.<init>(Lazy.java:15)
	... 5 more

确实好用,但是如果我要是两个对象都是用反射来进行创建呢?修改main方法

    public static void main(String[] args) throws Exception{
        // 虽然在方法getInstance中已经设置了线程同步
        // 通过反射来获取Lazy类中的空构造方法
        Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
        // 无视私有属性的构造器
        declaredConstructor.setAccessible(true);
        // 通过构造器再去创建两个个对象
        Lazy instance = declaredConstructor.newInstance();
        Lazy instance1 = declaredConstructor.newInstance();
        // 比较两个对象的hashCode
        System.out.println("instance的hashCode:" + instance.hashCode());
        System.out.println("instance1的hashCode:" + instance1.hashCode());
    }

结果:

instance的hashCode:460141958
instance1的hashCode:1163157884

是不是又被创建出来了,此时你可能又会说,我在类中定义一个非本类的静态加密变量,在构造中用来控制是否生成对象不就可以了嘛,这个想法也可以,测试一下,修改Lazy类

public class Lazy {

    // 定义一个加密变量
    private static boolean PORTERDONGS23W221GH21V = true;
    private Lazy(){
        synchronized (Lazy.class){
            /*
            if(LAZY != null){
                throw new RuntimeException("反射创建对象异常");
            }
            */
            if(PORTERDONGS23W221GH21V){
                PORTERDONGS23W221GH21V = false;
            }else{
                throw new RuntimeException("反射创建对象异常");
            }
        }
    }

    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) throws Exception{
        // 虽然在方法getInstance中已经设置了线程同步
        // 通过反射来获取Lazy类中的空构造方法
        Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
        // 无视私有属性的构造器
        declaredConstructor.setAccessible(true);
        // 通过构造器再去创建两个个对象
        Lazy instance = declaredConstructor.newInstance();
        Lazy instance1 = declaredConstructor.newInstance();
        // 比较两个对象的hashCode
        System.out.println("instance的hashCode:" + instance.hashCode());
        System.out.println("instance1的hashCode:" + instance1.hashCode());
    }
}

结果:

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.single.Lazy.main(Lazy.java:50)
Caused by: java.lang.RuntimeException: 反射创建对象异常
	at com.single.Lazy.<init>(Lazy.java:24)
	... 5 more

确实好用,但是,俗话说的好,道高一尺,魔高一丈,这样我还是可以使用反射去破解,修改main方法

    public static void main(String[] args) throws Exception{
        // 虽然在方法getInstance中已经设置了线程同步
        // 通过反射来获取Lazy类中的空构造方法
        Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
        // 获取私有属性
        Field field = Lazy.class.getDeclaredField("PORTERDONGS23W221GH21V");
        // 无视私有属性的构造器
        declaredConstructor.setAccessible(true);
        // 通过构造器再去创建两个个对象
        Lazy instance = declaredConstructor.newInstance();
        // 将属性的值再次改为true
        field.set(instance,true);
        Lazy instance1 = declaredConstructor.newInstance();
        // 比较两个对象的hashCode
        System.out.println("instance的hashCode:" + instance.hashCode());
        System.out.println("instance1的hashCode:" + instance1.hashCode());
    }

结果:

instance的hashCode:1163157884
instance1的hashCode:1956725890

可以看到,通过反射也可以忽视成员的私有属性,来实现破解,既然懒汉可以破解,那饿汉和内部类也可以破解,难道就没有什么解决办法了吗?对还有枚举类型。
可以看一下newInstance的源码
image
看到如果使用反射去操作枚举类型会抛出异常,下边使用枚举去测试一下,咱们先通过idea查看编译完后的EnumSingle.class
image
可以看到,在编译完成的枚举类中,有一个无参构造,好,下边使用反射去攻破一下,修改EnumSingle的main方法

    public static void main(String[] args) throws Exception {
        // 先获取类的构造函数
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
        // 忽略私有属性
        declaredConstructor.setAccessible(true);
        // 通过构造创建对象
        EnumSingle enumSingle = declaredConstructor.newInstance();
        System.out.println(enumSingle.INSTANCE);
    }

结果:

Exception in thread "main" java.lang.NoSuchMethodException: com.single.EnumSingle.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.single.EnumSingle.main(EnumSingle.java:19)

你可能会说,这没问题啊,你上边也说了,不能使用反射破解枚举,但是注意一下,运行结果中提示的是找不到无参构造NoSuchMethodException: com.single.EnumSingle.<init>(),而newInstance的异常是IllegalArgumentException("Cannot reflectively create enum objects");,并不是一个异常,难道我们被idea给骗了,没有无参构造?或者是验证失败了?
咱们找到编译的class文件的路径,通过cmd命令执行javap查看具体信息
image
难道也被javap命令给骗了?下面我是用jad反编译工具来反编译一下EnumSingle.class,jad下载地址,将jad工具复制到生成的class文件目录,通过cmd执行命令
image
image
执行完毕之后,打开反编译后的EnumSingle.java,发现构造是有参的,并不是无参的
image
修改咱们的main方法,将通过反射获取构造修改一下

    public static void main(String[] args) throws Exception {
        // 先获取类的构造函数
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        // 忽略私有属性
        declaredConstructor.setAccessible(true);
        // 通过构造创建对象
        EnumSingle enumSingle = declaredConstructor.newInstance();
        System.out.println(enumSingle.INSTANCE);
    }

结果:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.single.EnumSingle.main(EnumSingle.java:23)

这次就成功了!!!!

所以相对于饿汉式、DCL懒汉式、静态内部类、枚举,这几种使用方式,枚举是比较安全的,但是更推荐使用DCL懒汉式。此处使用反射来操作懒汉只是作为一个扩展。

4、使用场景

  • 网站的计数器,一般也是采用单例模式实现,否则难以同步。
  • 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  • Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。

使用细节

  • 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
  • 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new
  • 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session工厂等)
posted @ 2022-11-30 15:31  BTDong  阅读(80)  评论(0编辑  收藏  举报