23种设计模式(二)单例模式

单例模式

优点:
在内存中只有一个实例,减少了内存的开销
可以避免对资源的多重占用
缺点:
没有接口,无法扩展

必须点

私有构造器,线程安全的,序列化与反序列化对单利的破坏,反射攻击

懒汉式单例模式
public class LazySingleton {
    private static LazySingleton lazySingleton=null;
    private LazySingleton(){
    }
    public static LazySingleton getInstance(){
        if(!Optional.ofNullable(lazySingleton).isPresent()){
            lazySingleton=new LazySingleton();
        }
        return lazySingleton;
    }
}

Test:

public class Test {
    public static void main(String[] args) {
       LazySingleton instance = LazySingleton.getInstance();
       System.out.println(instance);
    }
}

由上代码:简单的单利模式,就产生了
单例模式是线程安全的吗? 答案:并不是,如下一一为大家解答,如何解决线程安全问题,接下来用多线程debug模式
创建线程类:

public class T implements Runnable {
    @Override
    public void run() {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName()+lazySingleton);
    }
}

Test类:

public class Test {
    public static void main(String[] args) {
        Thread thread = new Thread(new T());
        Thread thread1 = new Thread(new T());
        thread.start();
        thread1.start();
        System.out.println("END");
    }
}

如上代码:进行验证
在这里插入图片描述
竟然两个对象不一致,接下来给大家使用多线程debug讲解:
他的运行流程:
在这里插入图片描述
如上图:三个线程,我只创建了两个,为什么会有三个why?因为一个主线程,main
注意看这两个:
Thread-0
Thread-1
我先执行Thread-0 ,将线程0卡在这个地方,我再去执行Thread-1
在这里插入图片描述
Thread-1 也卡在这里,这样就会出现问题 线程1线程2同时进来覆盖其对象,我先将线程1执行完毕,在将线程2执行完毕
在这里插入图片描述
放行后的结果
在这里插入图片描述
所以说是不安全的,我如何将它变成线程安全的哪?加上 synchronized如果加在静态方法上 他锁的是.class这个类,加上

public class LazySingleton {
    private static LazySingleton lazySingleton=null;
    private LazySingleton(){
    }
    public synchronized static LazySingleton getInstance(){
        if(!Optional.ofNullable(lazySingleton).isPresent()){
            lazySingleton=new LazySingleton();
        }
        return lazySingleton;
    }
}

按照如上操作继续操作,就会出现阻塞,线程1进不去,直到线程0执行完毕,线程1才可以进入,这样保证了单利模式的线程安全性,synchronized加上会影响性能,还有一种能解决单利模式线程安全,大家静静的看着
在这里插入图片描述
双重检查:
双重检查的话没有什么问题,但是多线程的情况下,有可能出现指令重排序
1.给这个对象分配内存
2.初始化这个对象
3.指向lazyDoubleCheckSingleton 这个内存地址
4.初次访问对象
为什么会出现这种状况? 有可能2和3的位置颠倒,请看下面的图
有可能出现指令重排序 单线程内的指令重排序没有什么问题 多线程

 public  static LazyDoubleCheckSingleton getInstance(){
        if(!Optional.ofNullable(lazyDoubleCheckSingleton).isPresent()){
            //双重检查 减少线程开销
            synchronized (LazyDoubleCheckSingleton.class){
                if(!Optional.ofNullable(lazyDoubleCheckSingleton).isPresent()){
                    lazyDoubleCheckSingleton =new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }

单线程下线程重排序,不会造成太大的影响,但是多线程 请看第二个图
在这里插入图片描述
如果两个线程,0线程已经进去,然后instance是否为null,然后进行线程1第一次访问对象,线程与线程之间不可见的,内存可见性,会出现异常情况,我们如何解决这种情况?
在这里插入图片描述
解决多线程下的指令重排序:volatile 这样就完成了,线程的可见性,内存的可见性,线程这块先不细讲,只是先公布答案,先不说原理,因为涉及太多计算机原理,汇编等,关注我后续单独讲解线程原理,满满的干干货!!!!

public class LazyDoubleCheckSingleton {
    //保证线程不会重排序,线程可见性  内存的可见性
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton=null;
    private LazyDoubleCheckSingleton(){
    }
    public  static LazyDoubleCheckSingleton getInstance(){
        if(!Optional.ofNullable(lazyDoubleCheckSingleton).isPresent()){
            //双重检查 减少线程开销
            synchronized (LazyDoubleCheckSingleton.class){
                if(!Optional.ofNullable(lazyDoubleCheckSingleton).isPresent()){
                    //1.给这个对象分配内存
                    //2.初始化这个对象
                    //3.指向lazyDoubleCheckSingleton 这个内存地址
                    //有可能出现指令重排序    单线程内的指令重排序没有什么问题    多线程
                    lazyDoubleCheckSingleton =new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

静态内部类的单例模式:
先看图 多线程情况下 线程0或者线程1 这两个有一个会获得Class对象的初始化锁,例如线程0抢到了锁,线程1是卡拿不到指令重排序的,线程1是卡在绿色的部分的,就算指令重排序了,也没有问题
在这里插入图片描述
代码如下:
别忘了私有构造器哦,为了防止别人new出来这个对象

/**
 * 静态内部类   单例模式
 */
public class StaticInnerClassSingleton {
    private static  class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton=new StaticInnerClassSingleton();
    }
    public StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
    //私有构造器  为了不让别人new 出来
    private StaticInnerClassSingleton() {
    }
}

执行结果:
都是为了做延迟加载

 new Thread(()->{
            StaticInnerClassSingleton lazySingleton = StaticInnerClassSingleton.getInstance();
            System.out.println(Thread.currentThread().getName()+lazySingleton);
        }).start();
        new Thread(()->{
            StaticInnerClassSingleton lazySingleton = StaticInnerClassSingleton.getInstance();
            System.out.println(Thread.currentThread().getName()+lazySingleton);
        }).start();

在这里插入图片描述

饿汉式单利模式:

类加载的时候该对象会被创建,如果该类初始化,从来没有被使用过,造成内存的浪费
创建的两种方式
第一种: 静态成员变量 类初始化的时候被创建

public class HungrySingleton {
    private final static HungrySingleton hungrySingleton=new HungrySingleton();
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

第二种: 通过静态代码块进行构建

public class HungrySingleton {
    private final static HungrySingleton hungrySingleton;
    static {
        hungrySingleton=new HungrySingleton();
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

序列化与反序列化破坏单例模式:

何为对象序列化&反序列化?
序列化和反序列化是java中进行数据存储和数据传输的一种方式.
序列化: 将对象转换为字节的过程。
反序列化: 将字节转换为对象的过程。
说明:在当前软件行业中有时也会将对象转换为字符串的过程理解为序列化,例如将对象转换为json格式的字符串。
序列化的应用场景?
序列化和反序列化通常应用在:
网络通讯(C/S): 以字节方式在网络中传输数据
数据存储(例如文件,缓存)
说明:项目一般用于存储数据的对象通常会实现序列化接口.便于基于java中的序列化机制对对象进行序列化操作.
接下来破坏单利模式:
根据饿汉式单利模式举例子

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);
        System.out.println("序列化 ok");
        //oos.close();
        ObjectInputStream in=
                new ObjectInputStream(new FileInputStream("singleton_file"));
        HungrySingleton hungrySingleton=(HungrySingleton)in.readObject();
        System.out.println(instance);
        System.out.println(hungrySingleton);
        System.out.println(instance==hungrySingleton);
    }
}

运行会出现如下错误,这是因为什么那?因为这个类没有实现序列化接口,implements Serializable

Exception in thread "main" java.io.NotSerializableException: com.qjc.pattern.creational.singleton.HungrySingleton
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at com.qjc.pattern.creational.singleton.Test.main(Test.java:31)

然后实现序列化接口继续运行,如下结果,显然不是同一个对象他又创建了一个对象,返回的false
在这里插入图片描述
那我加上一个私有的方法 猜猜会返回什么 readResolve()

public class HungrySingleton implements Serializable {
    private final static HungrySingleton hungrySingleton;
    static {
        hungrySingleton=new HungrySingleton();
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    private Object readResolve(){
        return hungrySingleton;
    }
}

运行结果如下:
在这里插入图片描述
jdk源码我就不粘贴了 粘贴最主要的,我用文字描述,他的执行过程,执行过程通过反射inreadObject()读取序列化的反序列化对象可以定义一个readResolve方法,对其他方可见的对象,就是返回原对象,还是返回的原来的,并没有使用读取的序列化对象,而是用的原来的对象,进行破坏
在这里插入图片描述
反射攻击如何解决:

public class HungrySingleton implements Serializable {
    private final static HungrySingleton hungrySingleton;
    static {
        hungrySingleton=new HungrySingleton();
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    private Object readResolve(){
        return hungrySingleton;
    }

    private HungrySingleton() {
    }
}

接下来运行结果:
改变权限 ,如果不改变权限,就会出现,因为私有构造器,我将权限进行改变,不会出现异常

 Class objectClass=HungrySingleton.class;

       Constructor constructor=objectClass.getDeclaredConstructor();
       //constructor.setAccessible(true);//改变权限
       HungrySingleton instance = HungrySingleton.getInstance();

       HungrySingleton hungrySingleton=(HungrySingleton)constructor.newInstance();

       System.out.println(instance);
       System.out.println(hungrySingleton);
       System.out.println(instance==hungrySingleton);
Exception in thread "main" java.lang.IllegalAccessException: Class com.qjc.pattern.creational.singleton.Test can not access a member of class com.qjc.pattern.creational.singleton.HungrySingleton with modifiers "private"
	at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
	at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
	at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:413)
	at com.qjc.pattern.creational.singleton.Test.main(Test.java:48)

Process finished with exit code 1

我改变了权限:我就可以访问了,这样的话反射攻击,如何解决?
在这里插入图片描述
解决方案:
因为静态代码块,在一开始加载就会被初始化,所以其实不会为null的,如果在通过反射去创建对象,是禁止的直接抛出异常

public class HungrySingleton implements Serializable {
    private final static HungrySingleton hungrySingleton;
    private HungrySingleton() {
    //私有构造器中加入如下代码
        if(hungrySingleton!=null){
            throw  new RuntimeException("禁止反射");
        }
    }
    static {
        hungrySingleton=new HungrySingleton();
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    private Object readResolve(){
        return hungrySingleton;
    }
}

出现异常

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.qjc.pattern.creational.singleton.Test.main(Test.java:48)
Caused by: java.lang.RuntimeException: 禁止反射
	at com.qjc.pattern.creational.singleton.HungrySingleton.<init>(HungrySingleton.java:11)
	... 5 more

懒汉式的反射攻击如何解决?
饿汉式的,类加载并没有初始化,然后通过反射,去调用私有构造方法,又创建了对象,正常再去调用,会是不同的对象 ,那该如何做那? 就算在私有构造方法里面加入开关,但是反射也可以修改私有属性,显然是不安全的,反射就会拿两个对象了
看如下这个逻辑是很正常的

public class LazySingleton {
    private static LazySingleton lazySingleton=null;
    private static  boolean flag=true;
    private LazySingleton(){
        if(flag){
            flag=false;
        }else{
            throw  new RuntimeException("禁止反射");
        }
    }
    public synchronized static LazySingleton getInstance(){
        if(!Optional.ofNullable(lazySingleton).isPresent()){
            lazySingleton=new LazySingleton();
        }
        return lazySingleton;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class c=LazySingleton.class;
        Constructor constructor=c.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazySingleton o1=(LazySingleton)constructor.newInstance();
        LazySingleton o2 = LazySingleton.getInstance();
        System.out.println(o1);
        System.out.println(o2);
        System.out.println(o1==o2);
    }
}

执行结果:达到了预期,但是我在改一下 反射

Exception in thread "main" java.lang.RuntimeException: 禁止反射
	at com.qjc.pattern.creational.singleton.LazySingleton.<init>(LazySingleton.java:17)
	at com.qjc.pattern.creational.singleton.LazySingleton.getInstance(LazySingleton.java:22)
	at com.qjc.pattern.creational.singleton.LazySingleton.main(LazySingleton.java:32)

我加入如下代码:

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        Class c=LazySingleton.class;
        Constructor constructor=c.getDeclaredConstructor();

        constructor.setAccessible(true);
        LazySingleton o1=(LazySingleton)constructor.newInstance();

        Field flag = c.getDeclaredField("flag");
        flag.set(o1, true);

        LazySingleton o2 = LazySingleton.getInstance();
        System.out.println(o1);
        System.out.println(o2);
        System.out.println(o1==o2);
    }

第一次获取将flag设置为false了,但是我通过反射将他设置为true ,运行结果两个不同的对象,无论逻辑多复杂,在构造方法中,但是成员变量始终会被修改。单例懒汉模式,怎么做都防不住反射的

com.qjc.pattern.creational.singleton.LazySingleton@74a14482
com.qjc.pattern.creational.singleton.LazySingleton@1540e19d
false

单例的模式还有一种就是单例模式不会被反射,也不会被破坏,继续往下看
使用枚举类改为单例模式:
枚举类型实现单例模式,是最佳的选择,多线程情况下也不会出现问题,反射的情况下,与序列化破坏
序列化破坏:

public enum EnumInstance {
    INSTANCE;
    
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
    public static EnumInstance getInstance(){
        return INSTANCE;
    }

}

测试代码:

 EnumInstance enumInstance=EnumInstance.getInstance();
       enumInstance.setData(new Object());
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(enumInstance);
        System.out.println("序列化 ok");
        //oos.close();
        ObjectInputStream in=
                new ObjectInputStream(new FileInputStream("singleton_file"));
        EnumInstance instance=(EnumInstance)in.readObject();
        System.out.println(enumInstance.getData());
        System.out.println(instance.getData());
        System.out.println(enumInstance.getData()==instance.getData());

执行结果:
他们显然是相等的,枚举类对于序列化是不受影响的

序列化 ok
java.lang.Object@5fd0d5ae
java.lang.Object@5fd0d5ae
true

反射攻击
答案是不能反射攻击的,因为什么那,这里我进行了反编译

public enum EnumInstance {
    INSTANCE{
        protected  void printTest(){
            System.out.println("测试拉");
        }
    };
    protected abstract void printTest();
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
    public static EnumInstance getInstance(){
        return INSTANCE;
    }

}

反编译:

public abstract class EnumInstance extends Enum
{

    public static EnumInstance[] values()
    {
        return (EnumInstance[])$VALUES.clone();
    //    0    0:getstatic       #2   <Field EnumInstance[] $VALUES>
    //    1    3:invokevirtual   #3   <Method Object _5B_Lcom.qjc.pattern.creational.singleton.EnumInstance_3B_.clone()>
    //    2    6:checkcast       #4   <Class EnumInstance[]>
    //    3    9:areturn         
    }

    public static EnumInstance valueOf(String name)
    {
        return (EnumInstance)Enum.valueOf(com/qjc/pattern/creational/singleton/EnumInstance, name);
    //    0    0:ldc1            #5   <Class EnumInstance>
    //    1    2:aload_0         
    //    2    3:invokestatic    #6   <Method Enum Enum.valueOf(Class, String)>
    //    3    6:checkcast       #5   <Class EnumInstance>
    //    4    9:areturn         
    }

    private EnumInstance(String s, int i)
    {
        super(s, i);
    //    0    0:aload_0         
    //    1    1:aload_1         
    //    2    2:iload_2         
    //    3    3:invokespecial   #7   <Method void Enum(String, int)>
    //    4    6:return          
    }

    protected abstract void printTest();

    public Object getData()
    {
        return data;
    //    0    0:aload_0         
    //    1    1:getfield        #8   <Field Object data>
    //    2    4:areturn         
    }

    public void setData(Object data)
    {
        this.data = data;
    //    0    0:aload_0         
    //    1    1:aload_1         
    //    2    2:putfield        #8   <Field Object data>
    //    3    5:return          
    }

    public static EnumInstance getInstance()
    {
        return INSTANCE;
    //    0    0:getstatic       #9   <Field EnumInstance INSTANCE>
    //    1    3:areturn         
    }


    public static final EnumInstance INSTANCE;
    private Object data;
    private static final EnumInstance $VALUES[];

    static 
    {
        INSTANCE = new EnumInstance("INSTANCE", 0) {

            protected void printTest()
            {
                System.out.println("\u6D4B\u8BD5\u62C9");
            //    0    0:getstatic       #2   <Field PrintStream System.out>
            //    1    3:ldc1            #3   <String "\u6D4B\u8BD5\u62C9">
            //    2    5:invokevirtual   #4   <Method void PrintStream.println(String)>
            //    3    8:return          
            }

            
            {
            //    0    0:aload_0         
            //    1    1:aload_1         
            //    2    2:iload_2         
            //    3    3:aconst_null     
            //    4    4:invokespecial   #1   <Method void EnumInstance(String, int, EnumInstance$1)>
            //    5    7:return          
            }
        }
;
    //    0    0:new             #10  <Class EnumInstance$1>
    //    1    3:dup             
    //    2    4:ldc1            #11  <String "INSTANCE">
    //    3    6:iconst_0        
    //    4    7:invokespecial   #12  <Method void EnumInstance$1(String, int)>
    //    5   10:putstatic       #9   <Field EnumInstance INSTANCE>
        $VALUES = (new EnumInstance[] {
            INSTANCE
        });
    //    6   13:iconst_1        
    //    7   14:anewarray       EnumInstance[]
    //    8   17:dup             
    //    9   18:iconst_0        
    //   10   19:getstatic       #9   <Field EnumInstance INSTANCE>
    //   11   22:aastore         
    //   12   23:putstatic       #2   <Field EnumInstance[] $VALUES>
    //*  13   26:return          
    }
}

看到了私有构造方法,和静态成员变量,通过静态代码块进行赋值操作,初始化就会被赋值,通过反射,也创建不了实例,也不支持反射newInstatice枚举类

public static final EnumInstance INSTANCE;
static{
}

讲到这里,大家对单例模式,有了新的认识,接下来我还有干活,单例模式的,单例模式是最简单的,也是最复杂的,以后面试的时候可不能在这里丢分
ThreadLocal单例模式,这个单例模式是带有引号的,保证不了单例模式是唯一的,但是线程我能保证唯一单个线程获取到的,对象保证是唯一的,时间换空间加上volatile,空间换时间就使用ThreadLocal

public class ThreadLocalInstance {
    private static  final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal
             = ThreadLocal.withInitial(() -> new ThreadLocalInstance());
    private ThreadLocalInstance(){

    }

    public static ThreadLocalInstance getInstance(){
        return threadLocalInstanceThreadLocal.get();
    }
}
        System.out.println(ThreadLocalInstance.getInstance());
        System.out.println(ThreadLocalInstance.getInstance());
        System.out.println(ThreadLocalInstance.getInstance());
        System.out.println(ThreadLocalInstance.getInstance());
        System.out.println(ThreadLocalInstance.getInstance());
        new Thread(()->System.out.println(ThreadLocalInstance.getInstance())).start();

执行结果:由此看出,带引号的单例,不同的场景用不同的单例模式,所以单线程下我能保证唯一性,多线程下就保证不了,因为不同的线程下获取的对象不同了

com.qjc.pattern.creational.singleton.ThreadLocalInstance@682a0b20
com.qjc.pattern.creational.singleton.ThreadLocalInstance@682a0b20
com.qjc.pattern.creational.singleton.ThreadLocalInstance@682a0b20
com.qjc.pattern.creational.singleton.ThreadLocalInstance@682a0b20
com.qjc.pattern.creational.singleton.ThreadLocalInstance@682a0b20
com.qjc.pattern.creational.singleton.ThreadLocalInstance@5deb05a4
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值