单例模式之懒汉式介绍

懒汉式的最基本写法

最后展示的代码包含详细的过程和注解,可以结合最后的代码来理解哦。

写法

我们先来看一下最基本的懒汉式写法,不考虑任何的安全问题

package singleton;

public class LazyMan {

    private LazyMan() {
        System.out.println(Thread.currentThread().getName()+"ok");
    }
    
    private static LazyMan lazyMan = null;
    
    public static LazyMan getInstance() {
        if(lazyMan==null) {
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }

}
复制代码

测试

然后我们写一个测试代码来看看它是否满足单例的要求

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

结果

我们来看一下输出结果

image.png

通过图片,我们明显看出来这不遵循单例模式的要求,所以我们需要解决单例的线程安全问题。

双重检测模式来解决懒汉式线程安全问题

代码

下面是改进代码:

package singleton;

public class LazyMan {

    private LazyMan() {
        System.out.println(Thread.currentThread().getName()+"ok");
    }

    private static LazyMan lazyMan = null;

    public static LazyMan getInstance() {
        if(lazyMan==null) {
            synchronized (LazyMan.class) {
                if(lazyMan==null) {
                    lazyMan = new LazyMan();
                }
            }
        }

        return lazyMan;
    }

}
复制代码

测试

下面我们来测试一下

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

结果

image.png

写到这里,我们解决了线程的安全问题,这样看来,问题基本上解决了。但是我们面试的时候,这种写法是大部分人都能写出来的,所以我们需要一些骚操作。

懒汉式的一些骚操作

破坏双重检测锁模式

首先我们来破坏一下上面双重检测锁模式,下面是破坏代码演示

public static void main(String[] args) throws Exception {
    LazyMan test1 = LazyMan.getInstance();
    Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);
    LazyMan test2 = declaredConstructor.newInstance();
    System.out.println(test1);
    System.out.println(test2);

}
复制代码

我们来看一下结果

image.png

我们看到了此时的结果就不符合单例模式了。

解决破坏双重检测锁的操作

我们只需要一步就可以解决这个问题,就是手动抛出异常。实现操作如下:

private LazyMan() {
    if(lazyMan!=null) {
        throw new RuntimeException("不可以通过反射来破坏哦~");
    }
}
复制代码

我们来看一下此时的结果:

image.png

我们可以看见此时通过一次反射问题就解决啦。 但是这里为什么写一次反射呢?不经有小伙伴问了,假如我俩次都是通过反射获得实例,是不是又要出事啦? 答案是肯定的。下面我们就继续深入这个问题。

继续破坏已经解决破坏双重检测锁的操作

直接上代码

public static void main(String[] args) throws Exception {
    Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);
    LazyMan test1 = declaredConstructor.newInstance();
    LazyMan test2 = declaredConstructor.newInstance();
    System.out.println(test1);
    System.out.println(test2);

}
复制代码

我们来看一下结果

image.png

啊,真的被破坏了(^-^)V。但是魔高一尺,道高一丈,肯定有解决方法的。下面我们来看解决方法。

解决通过俩次反射得到实例的操作

话不多说,我们直接看代码

private static boolean del = false;

private LazyMan() {
    synchronized (LazyMan.class){
        if(del==false) {
            del = true;
        } else  {
            throw new RuntimeException("不可以通过反射来破坏哦~");
        }
    }

}
复制代码

我们来看一下结果

image.png

我们通过创建了del这个旗帜来判断,解决了这个问题。但是我们知道反射是无所不能的,所以这个解决方法还可以被破坏。

破坏旗帜

我们直接看代码

public static void main(String[] args) throws Exception {
    Field del = LazyMan.class.getDeclaredField("del");
    del.setAccessible(true);
    Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);
    LazyMan test1 = declaredConstructor.newInstance();
    del.set(test1,false);
    LazyMan test2 = declaredConstructor.newInstance();
    System.out.println(test1);
    System.out.println(test2);

}
复制代码

我们来看一下结果

image.png

我们可以看到单例模式又被破坏了。救命,到这里问题一直都没解决,这不就是套娃嘛,下面我们来说一下这种问题到底如何来解决。

最终解决方式

我们知道枚举是不能被反射拿到的,所以我们只能通过枚举类来防止反射的破坏。小伙伴们,你们想到了吗。 好了,这是我的对单例模式的懒汉式的介绍,欢迎大佬们指点。

最终代码如下(包含详细的注解)

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

//懒汉式单例
//道高一尺,魔高一丈,还是没能解决反射造成的安全问题,想解决得看枚举
public class LazyMan {

   //这个的目的是解决俩次都是调用反射创建对象不安全的问题
   private static boolean delTwoFanShe = false;

   private LazyMan(){

//        System.out.println(Thread.currentThread().getName()+"ok");

       synchronized (LazyMan.class) {

           if(delTwoFanShe == false) {
               delTwoFanShe = true;
           } else {
               throw new RuntimeException("不要试图使用反射破坏异常");
           }

//            if(lazyMan!=null) {
//                //此方式是解决创建实例时一个用的getInstance(),另一个用的反射得到的,但是解决不了俩次都是通过反射获得的情况
//                throw new RuntimeException("不要试图使用反射破坏异常");
//            }
       }

   }

   private volatile static LazyMan lazyMan = null;

   //双重检测锁模式的懒汉式模式,简称DCL懒汉式
   public static LazyMan getInstance() {
       //上锁
       if(lazyMan==null) {
           synchronized (LazyMan.class) {
               if(lazyMan==null) {
                   lazyMan = new LazyMan();//不是一个原子性操作
                   /**
                    * 1.分配内存空间
                    * 2.执行构造方法,初始化对象
                    * 3.把这个对象指向这个空间
                    * 假如不是按照123步骤执行的话,就是发生了指令重排,那么lazyMan就没有完成初始化操作
                    */
               }
           }
       }

       return lazyMan; //如果不是按顺序执行123操作的话,此时lazyMan还没有完成构造,所以必须加上volatile来避免这种问题的发生
   }


   //我们写main函数来破坏,不断增强懒汉式的安全性
   public static void main(String[] args) throws Exception {

//        for(int i=0;i<10;i++) {
//            new Thread(()->{
//                LazyMan.getInstance();
//            }).start();
//        }

//        LazyMan instance = LazyMan.getInstance();  //这是一般的获取方式

       //我们得到自己设置的隐藏值,进行破坏
       Field delTwoFanShe = LazyMan.class.getDeclaredField("delTwoFanShe");
       delTwoFanShe.setAccessible(true);

       Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);

       declaredConstructor.setAccessible(true);

       LazyMan lazyMan = declaredConstructor.newInstance();//通过反射得到实例对象

       //我们修改第一次的实例,来将值变成false,进行破坏
       delTwoFanShe.set(lazyMan,false);

       LazyMan lazyMan1 = declaredConstructor.newInstance();//俩次实例对象都由反射来实现,造成破坏

       System.out.println(lazyMan);
       System.out.println(lazyMan1);

   }

}
复制代码

猜你喜欢

转载自juejin.im/post/7086738661227626503