ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

彻底搞懂单例模式

2020-12-22 15:29:42  阅读:208  来源: 互联网

标签:SingleTonDemo getInstance 彻底 class static 单例 搞懂 singleTonDemo public


彻底搞懂单例模式

一、普通单例模式

饿汉式与懒汉式

1、饿汉式

public class SingleTonDemo {
    private final static SingleTonDemo singletonDemo=new SingleTonDemo();
    private SingleTonDemo(){

    }
    public static SingleTonDemo getInstance(){
        return singletonDemo;
    }
}

2、懒汉式

public class SingleTonDemo {
    private static SingleTonDemo singleTonDemo;
    private SingleTonDemo(){
    }
    public static SingleTonDemo getInstance(){
        if (singleTonDemo==null){
            singleTonDemo=new SingleTonDemo();
        }
        return singleTonDemo;
    }
}

以上就是基本的单例模式 饿汉式和懒汉式,可以发现,饿汉式在定义属性的时候就创建对象了,相当于在类一创建的时候就创建对象,因此效率较低。而懒汉式是在调用**getInstance()**方法的时候才创建对象,能够更好的节省计算资源。

但是饿汉式相对于懒汉式,饿汉式是线程安全的。

(1)饿汉模式线程安全

public class SingleTonDemo {
    private final static SingleTonDemo singletonDemo=new SingleTonDemo();
    private SingleTonDemo(){
        System.out.println(Thread.currentThread().getName()+"-->"+"create object finish");
    }
    public static SingleTonDemo getInstance(){
        return singletonDemo;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                getInstance();
            },"thread"+i).start();
        }
    }
}

构造方法中加一个输出,输出线程名称,发现只有main线程创建了该对象。

image-20200715094748623

因此该对象只被创建了一次,因此在创建对象时,饿汉模式是安全的,他不会多创建对象。而懒汉模式,如果创建对象时两个线程同时进入构造器,则可能会创建两个对象。

(2)懒汉式线程不安全

public class SingleTonDemo {
    private static SingleTonDemo singleTonDemo;
    private SingleTonDemo(){
        System.out.println(Thread.currentThread().getName()+"create object finish");
    }
    public static SingleTonDemo getInstance(){
        if (singleTonDemo==null){
            singleTonDemo=new SingleTonDemo();
        }
        return singleTonDemo;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                SingleTonDemo instance = getInstance();
            }).start();
        }

    }
}

image-20200715095223553

image-20200715095236735

几次运行的结果都不一样,有时候创建了一个对象,有时候创建了多个对象,这就说明懒汉式是线程不安全的。

二、线程安全的懒汉式模式

1、双重检查锁的懒汉式

public class SingleTonDemo {
    //为了方式重新编排导致的小概率错误,使用volatile关键字
    private volatile static SingleTonDemo singleTonDemo;
    private SingleTonDemo(){
        System.out.println(Thread.currentThread().getName()+"create object finish");
    }
    public static SingleTonDemo getInstance(){
        //双重检查锁
        if (singleTonDemo==null){
            synchronized (SingleTonDemo.class){
                if (singleTonDemo==null){
                    singleTonDemo=new SingleTonDemo();
                }
            }
        }
        return singleTonDemo;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                SingleTonDemo instance = getInstance();
            }).start();
        }
    }
}

image-20200715100534805

测了好几次,都是只有一个对象被创建。

双重检查锁顾名思义就是两个判断,加锁之前先看看对象有没有被其他线程创建,没有的话加上同步代码块,再判断判断对象有没有被其他线程创建,没有的话才创建对象。

2、静态内部类

public class SingleTonDemo {
    private SingleTonDemo(){
        System.out.println(Thread.currentThread().getName());
    }
    private static class Singleton{
        private volatile static SingleTonDemo single = new SingleTonDemo();
    }

    public static SingleTonDemo getInstance(){
        return Singleton.single;
    }

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

image-20200715102115766

静态内部类的getInstance方法没有被同步,只是把类的加载给延迟了,这样既不是类一创建就创建对象,也不用加锁,内部类是一个静态的,主类被创建的时候,他就加载了,但是内部的构造方法并没有被加载,需要被调用的时候才加载,因此当线程访问内部类时,就可以创建主类对象了,而且内部类是一个类,他把主类对象作为他的属性,就被创建了一次,内部类内部相当于是一个饿汉模式,这样可以很大的节省资源。

以上两种方法虽然是线程安全的,但是太容易被反射修改对象内容,存在安全问题。

public class SingleTonDemo {
    //为了方式重新编排导致的小概率错误,使用volatile关键字
    private volatile static SingleTonDemo singleTonDemo;
    private SingleTonDemo(){
        System.out.println(Thread.currentThread().getName()+"create object finish");
    }
    public static SingleTonDemo getInstance(){
        //双重检查锁
        if (singleTonDemo==null){
            synchronized (SingleTonDemo.class){
                if (singleTonDemo==null){
                    singleTonDemo=new SingleTonDemo();
                }
            }
        }
        return singleTonDemo;
    }
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        SingleTonDemo instance1 = SingleTonDemo.getInstance();
        Constructor<SingleTonDemo> declaredConstructor = SingleTonDemo.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        SingleTonDemo instance2=declaredConstructor.newInstance();
        System.out.println("singleTonDemo1 = " + instance1);
        System.out.println("singleTonDemo2 = " + instance2);
    }
}

image-20200715105216509

上图可以看出,通过反射,对象已经被更改了。

public class SingleTonDemo {
    //为了方式重新编排导致的小概率错误,使用volatile关键字
    private volatile static SingleTonDemo singleTonDemo;
    private SingleTonDemo(){
        if (singleTonDemo!=null){
            throw new RuntimeException("不要通过反射篡改对象");
        }else {
            System.out.println(Thread.currentThread().getName()+"create object finish");
        }
    }
    public static SingleTonDemo getInstance(){
        //双重检查锁
        if (singleTonDemo==null){
            synchronized (SingleTonDemo.class){
                if (singleTonDemo==null){
                    singleTonDemo=new SingleTonDemo();
                }
            }
        }
        return singleTonDemo;
    }
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        SingleTonDemo instance1 = SingleTonDemo.getInstance();
        Constructor<SingleTonDemo> declaredConstructor = SingleTonDemo.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        SingleTonDemo instance2=declaredConstructor.newInstance();
        System.out.println("singleTonDemo1 = " + instance1);
        System.out.println("singleTonDemo2 = " + instance2);
    }
}

image-20200715105853153

可以将双重检测升级成三重检测,每当构造对象的时候判断一下,对象是否为空,如不为空,说明已经被篡改了,然后抛出异常。

但是上面的方法是基于正常方式创建过一次对象了才有用,如果侵入者两次都是用反射创建对象,改方法就没用了。

反射获取构造器,创建对象是直接通过构造器创建对象,如果在反射之前已经通过正常方式创建了对象,则getInstance方法会把对象中的SingleTonDemo singleTonDemo赋值,此时SingleTonDemo singleTonDemo就不再时null了,当采用反射通过构造器创建对象时,因为SingleTonDemo singleTonDemo已经有值了 因此会满足判断singleTonDemo!=null从而抛出异常,但是如果一开始就不用getInstance创建对象,那么singleTonDemo属性也不会有值,所有后面用反射通过构造器创建对象是可以创建出来的。。。。。。道高一尺魔高一丈,因此双重锁和静态内部类是不能避免反射的。

当然如果实在想用双重锁和静态内部类,可以定义一个属性,通过加密,保护这个属性的属性名,不被知道,然后再同步代码块中再加一个判断。这里就不加演示了。。。。因为总有方法可以破解你的属性名,还是不够安全。

public class SingleTonDemo {
    //为了方式重新编排导致的小概率错误,使用volatile关键字
    private volatile static SingleTonDemo singleTonDemo;
    private SingleTonDemo(){
        if (singleTonDemo!=null){
            throw new RuntimeException("不要通过反射篡改对象");
        }else {
            System.out.println(Thread.currentThread().getName()+"create object finish");
        }
    }
    public static SingleTonDemo getInstance(){
        //双重检查锁
        if (singleTonDemo==null){
            synchronized (SingleTonDemo.class){
                if (singleTonDemo==null){
                    singleTonDemo=new SingleTonDemo();
                }
            }
        }
        return singleTonDemo;
    }
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<SingleTonDemo> declaredConstructor = SingleTonDemo.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        SingleTonDemo instance1=declaredConstructor.newInstance();
        SingleTonDemo instance2=declaredConstructor.newInstance();
        System.out.println("singleTonDemo1 = " + instance1);
        System.out.println("singleTonDemo2 = " + instance2);
    }
}

为了解决反射修改属性的问题,可以采用枚举类方式的懒汉模式。

3、枚举类

点进newInstance()方法,发现

image-20200715131637584

public enum EnumSingle{
    INSTANCE;
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

测试:

class Test{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle instance1=EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2=declaredConstructor.newInstance();
    }
}

image-20200715123542648

报错,没有空参构造器??? ,正常应该报错反射不能用于枚举类才对。。。。

image-20200715123926941

javap -p 类名.class 可以将字节码转换成代码

image-20200715124129054

1.用jd-gui

image-20200715125104391

终于用jd-gui发现确实没有空参构造器。

2.用jad

a.用jad把jad.exe复制到字节码所在目录下

b.然后在路径处输入cmd,进入命令行

c.在命令行输入 jad -sjava 类名.class

image-20200715130926567

d.回到字节码所在的目录发现了生成的java文件

image-20200715131016703

e.打开查看代码

image-20200715131107158

因此改造代码

class Test{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle instance1=EnumSingle.INSTANCE;
        //换成有参构造器
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(
                String.class,int.class
        );
        declaredConstructor.setAccessible(true);
        EnumSingle instance2=declaredConstructor.newInstance();
    }
}

image-20200715131430896

=EnumSingle.INSTANCE;
//换成有参构造器
Constructor declaredConstructor = EnumSingle.class.getDeclaredConstructor(
String.class,int.class
);
declaredConstructor.setAccessible(true);
EnumSingle instance2=declaredConstructor.newInstance();
}
}


[外链图片转存中...(img-jeURGa1H-1608621603977)]

报这个错就正常了,说明反射确实不能操作枚举类。

标签:SingleTonDemo,getInstance,彻底,class,static,单例,搞懂,singleTonDemo,public
来源: https://blog.csdn.net/m0_46473771/article/details/111556630

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有