为了账号安全,请及时绑定邮箱和手机立即绑定

【学习笔记】单例模式

单例模式

定义:保证一个类仅有一个实例,并提供一个全局访问点

类型:创建型

使用场景:与定义无异,想在任何时候情况下都只有一个实例。当然,如果是在单机模式下肯定不用过多讨论。一般都是集群模式下,
比如一些共享的计数器,连接池,线程池等。

优点:

  • 内存开销少,只有一个实例。也可以避免对资源的多重浪费
  • 设置全局访问点,严格的控制了访问。换句话说就是没办法去进行new操作,只能调用方法获取。

缺点

-优点及缺点。严格的控制访问导致扩展性差,基本只能靠改代码进行修改。

单例模式设计的重点

  • 私有构造器
  • 线程安全
  • 延迟加载
  • 序列化与反序列化安全
  • 反射 -> 防止反射攻击

1. 懒汉模式

图片描述

通过上图我们能看到执行看似没什么问题,但是仔细一看的话便能发现它是线程不安全的。因为代码比较简单所以是很难触发的。所以我们需要进行一下多线程debug。
图片描述

设置之后我们分别对线程进行一下debug,手动模拟会出现问题的可能。
图片描述
      最后果然出现了不同的打印结果。知道了不安全的原因,那么如何解决自然变得很简单,我们只需要在静态方法前面加synchronized关键字即可,但是静态方法加锁就相当于这个类加锁。对于性能自然会不是很高。那么有没有让锁尽量不会起作用,还能延迟加载的方法呢?
自然是有,下面讲解一下双重检查

2. 双重检查模式

public class DoubleCheckLazySingleton {
    //private static DoubleCheckLazySingleton lazySingleton = null;
    private volatile static DoubleCheckLazySingleton lazySingleton = null;


    private DoubleCheckLazySingleton() {
    }

    public synchronized static DoubleCheckLazySingleton getInstance() {

        if (lazySingleton == null) {
            synchronized (DoubleCheckLazySingleton.class) {
                if (lazySingleton == null) {
                    lazySingleton = new DoubleCheckLazySingleton();
                    //因为指令重排可能会有一个隐患
                    //1 - 分配内存給这个对象
                    //3 - lazySingleton 指向刚分配的内存地址
                    //2 - 初始化对象
                    //-----------------------------
                    //3 - lazySingleton 指向刚分配的内存地址

                }

            }
        }
        return lazySingleton;
    }

    public static void main(String[] args) {
        //只是为了快速用而已,实际的话不建议这么创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 2; i++) {
           executorService.execute(DoubleCheckLazySingleton::getInstance);
        }
        executorService.shutdown();
    }
}

      通过代码可以看到我是把没有加volatile的代码注释掉了。原因下面会进行讲解。
图片描述
      通过图片我们能看到因为指令重排的原因,创建一个对象的指令可能会被重排序。如果出现上图的情况,那么就会导致程序报错。那么我们解决问题的方法无非两种:
1 禁止重排序
2 线程0的重排序,对其他线程不可见。

     其实加volatile关键字就是方法 1。volatile通过加入内存屏障和禁止重排序优化来实现可见性。这个应该是线程安全性相关的知识,因为今天主要是说单例模式,所以简单说一下:volatile写操作的时候会将本地内存的共享变量刷新到主内存,而读操作会从主内存中去读共享变量。

3. 静态内部类模式

public class StaticInnerSingleton {

    private static class InnerClass{

        private static StaticInnerSingleton staticInnerSingleton = new StaticInnerSingleton();
    }

    public static StaticInnerSingleton getInstance(){
        return InnerClass.staticInnerSingleton;
    }


    //私有构造方法
    private StaticInnerSingleton() {
    }
}

内部静态类模式就是上面2的解决方式。线程的重排序,对其他线程不可见。

(深入理解java虚拟机 p226)虚拟机会保证一个类的< clinit>()方法在多线程环境中被正确的加锁、同步、如果多个线程去同时初始化一个类,那么只有一个线程去执行这个类的< clinit>()方法,其他线程都需要阻塞等待,直到活动线程的方法执行完毕。

图片描述
类似于上图。只有一个线程是可以获取锁的,那么即使线程0去重排序,对于线程1也是不可见的。

4. 饿汉模式
饿汉比较简单就不详细说了。优点线程安全。缺点不是延迟加载,如果不用,会造成一定的开销。

public class HungrySingleton {
    private final static HungrySingleton hungrySIngleton = new HungrySingleton();

    public HungrySingleton() {
    }

    public HungrySingleton getHungrySIngleton() {
        return hungrySIngleton;
    }
}

序列化以及反射对单例模式的影响
     因为后续会讲解枚举类型的单例,因为天然特性的原因。所以这里先讲一下序列化以及反射对单例模式的影响

序列化
    private final static HungrySingleton hungrySIngleton = new HungrySingleton();

    public HungrySingleton() {
    }

   public static HungrySingleton getHungrySIngleton() {
        return hungrySIngleton;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {

        HungrySingleton hungrySingleton = HungrySingleton.getHungrySIngleton();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
        oos.writeObject(hungrySingleton);

        File file = new File("singleton");
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));

        HungrySingleton hungrySingletonTwo = (HungrySingleton) objectInputStream.readObject();

        System.out.println(hungrySingleton);
        System.out.println(hungrySingletonTwo);
        System.out.println(hungrySingleton.equals(hungrySingletonTwo));

    }
}

//打印结果
com.example.demo.singleton.HungrySingleton@1fb3ebeb
com.example.demo.singleton.HungrySingleton@5010be6
false

我们可以看出来这就违背了我们单例模式的初衷,因为我的得到了不一样的对象。解决方案也很简单我们只需要加一个方法就可以搞定了。
图片描述
     我们可以看到readResolve并不是灰色的,因为我的主题如果这个方法没有被调用的时候显示的是灰色的。那么为什么加一个readResolve就可以了呢,他有事在哪调用的?那接下来我们得看源码才能知道了。

        HungrySingleton hungrySingletonTwo = (HungrySingleton) objectInputStream.readObject();

因为源码太多,我就不贴太多图了,主要的地方我再贴图。
点击readObject这个方法 -> 进去可以看到readObject0()这个方法->进去之后发现里面有一个switch 找到TC_OBJECT然后我们进入readOrdinaryObjectreadOrdinaryObject这个方法-> 因为源码太多,我就不贴太多图了,主要的地方我在贴图。
点击readObject这个方法 -> 进去可以看到readObject0()这个方法->进去之后发现里面有一个switch 找到TC_OBJECT然后我们进入readOrdinaryObject这个方法。在里面找到

obj = desc.isInstantiable() ? desc.newInstance() : null;

通过这行代码,我们看到了obj,然后看了一下obj最后会返回,那么就说明obj没啥可看的,我们的重点在于这个判断。点击进入isInstantiable方法

  /**
     * Returns true if represented class is serializable/externalizable and can
     * be instantiated by the serialization runtime--i.e., if it is
     * externalizable and defines a public no-arg constructor, or if it is
     * non-externalizable and its first non-serializable superclass defines an
     * accessible no-arg constructor.  Otherwise, returns false.
     */
    boolean isInstantiable() {
        requireInitialized();
        return (cons != null);
    }
    cons是一个构造器点进去没有什么有效信息,那么只能看上方注解了。如果
    serializable/externalizable在运行的时候被实例化就会返回true。

可以看到返回true之后 desc.newInstance()通过反射拿到一个新的对象肯定会和原来不一样。虽然现在知道了我们会新获得一个对象,但是还没有解决我们最初的疑问,所以我们接着往后看。

 if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

     通过上面代码我们看到if里面有一个hasReadResolveMethod()方法,看名字我们也猜出来这到底是干啥的。进入if里面之后看到 Object rep = desc.invokeReadResolve(obj);点击进入发现里面是通过反射拿到我们类里面声明的readResolve方法,至此我们也知道了readResolve是在哪被调用了。但是这还有个不好的地方就是每次都会有新的对象被生成,只不过后期调用readResolve方法被替换了而已。

反射攻击
public class HungrySingleton {
    private final static HungrySingleton hungrySIngleton = new HungrySingleton();

    public HungrySingleton() {
    }

    public static HungrySingleton getHungrySIngleton() {
        return hungrySIngleton;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        Class jhwclass = HungrySingleton.class;
        Constructor constructor = jhwclass.getDeclaredConstructor();
        //放开私有权限
        constructor.setAccessible(true);
        HungrySingleton hungrySingleton = HungrySingleton.getHungrySIngleton();
        HungrySingleton hungrySingletonTwo = (HungrySingleton) constructor.newInstance();


        System.out.println(hungrySingleton);
        System.out.println(hungrySingletonTwo);
        System.out.println(hungrySingleton.equals(hungrySingletonTwo));


    }
}
打印结果
com.example.demo.singleton.HungrySingleton@13221655
com.example.demo.singleton.HungrySingleton@2f2c9b19
false

在构造方法加上防御代码
图片描述

静态内部类也可以用上面的方法。原因是两者都是在类加载的时候,实例就会生成。而懒汉加载就不能用了,因为无法确定哪个线程去进行加载,即使加了以一些防御性质的代码也不能保证,例如声明一个变量去当开关,还是可能会被反射进行更改。可以参考一下下面的代码。

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 (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        System.out.println(Thread.currentThread().getName() + "--lazySingleton:" + lazySingleton);
        return lazySingleton;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        Class jhwclass = LazySingleton.class;
        Constructor a = jhwclass.getDeclaredConstructor();
        a.setAccessible(true);
        LazySingleton lazySingleton = LazySingleton.getInstance();

        Field aa = lazySingleton.getClass().getDeclaredField("flag");
        aa.setAccessible(true);
        aa.set(lazySingleton, true);
        LazySingleton lazySingletonTwo = (LazySingleton) a.newInstance();
    }
}

5. 枚举模式

public enum EnumInstance {

    one;
    private String data;

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }

    public static EnumInstance getInstance() {
        return one;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumInstance enumInstance = EnumInstance.getInstance();
        enumInstance.setData("jhw");

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
        oos.writeObject(enumInstance);

        File file = new File("singleton");
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));

        EnumInstance enumInstanceTwo = (EnumInstance) objectInputStream.readObject();

        Class jhwclass = EnumInstance.class;
        //Constructor aa = jhwclass.getDeclaredConstructor();
        Constructor aaa = jhwclass.getDeclaredConstructor(String.class, int.class);

        //aa.setAccessible(true);
        aaa.setAccessible(true);

        // EnumInstance enumInstanceTrd = (EnumInstance) aa.newInstance();
        EnumInstance enumInstanceTrd = (EnumInstance) aaa.newInstance("jj", 1);

        System.out.println(enumInstance.getData());
        System.out.println(enumInstanceTwo.getData());
        System.out.println(enumInstance.getData().equals(enumInstanceTwo.getData()));

    }
}

//打印结果 
jhw
jhw
true

//反射错误1
Exception in thread "main" java.lang.NoSuchMethodException: com.example.demo.singleton.EnumInstance.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.example.demo.singleton.EnumInstance.main(EnumInstance.java:48)

//反射错误2
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.example.demo.singleton.EnumInstance.main(EnumInstance.java:55)

     通过上面的代码我们可以看到,通过反射拿到的也是同一个对象。源码跟上面一样,readEnum方法中->readStrirng方法。因为枚举类里面的名字是唯一的,那么拿到的常量肯定也是唯一的。
     而Enum这个类也并没有无参的构造方法并且枚举类还不允许进行反射调用,上面的两个错误打印就是很好的说明。通过一些反编译的工具我们看一下enum,其中内部一些声明比如final,静态块是其优雅实现单例模式的基石。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
9
获赞与收藏
50

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消