zoukankan      html  css  js  c++  java
  • JAVA设计模式(6:单例模式详解)

    单例模式作为一种创建型模式,在日常开发中用处极广,我们先来看一一段代码:

    // 构造函数
    protected Calendar(TimeZone var1, Locale var2) {
            this.lenient = true;
            this.sharedZone = false;
            this.nextStamp = 2;
            this.serialVersionOnStream = 1;
            this.fields = new int[17];
            this.isSet = new boolean[17];
            this.stamp = new int[17];
            this.zone = var1;
            this.setWeekCountData(var2);
    	}
    
    // 提供 Calendar 类实例的方法
    public static Calendar getInstance(){
            return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
    	}
    

    看过上一篇博客Java设计模式(5:工厂模式详解)的朋友应该熟悉这段来自JDKCalendar类的代码,这就是单例模式的一种实现:

    1. Calendar类的构造函数被protected修饰,保证其不能被其他包下的类访问。
    2. getInstance()方法提供了获得Calendar类实例化对象的方法。

    从上述代码来看,我们可以认定实现单例模式需要满足两个基本原则:

    1. 类的构造函数私有化。
    2. 该类需要提供一个获得实例的全局访问点

    所以可以得出结论:单例模式是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局的访问点。

    得出结论后,再来看看实现。在java语言当中,两种方式构建单例模式:饿汉式单例懒汉式单例

    一、饿汉式单例

    // 饿汉式单例
    public class HungrySingleton {
        // 构造函数私有化
        private HungrySingleton() {}
    
        private static final HungrySingleton singleton = new HungrySingleton();
    
        // 提供一个全局的访问点
        public static HungrySingleton getInstance(){
            return singleton;
        }
    }
    

    饿汉式单例是在类加载的时候就立即初始化,并且创建了单例对象。在上述代码中,当HungrySingleton类在被类加载器加载时,它的实例对象singleton就已经创建完成了;并且根据类的加载机制,我们明白:singleton作为HungrySingleton类中的一个静态的声明对象,在HungrySingleton类第一次被类加载器加载时就已经创建完成,并且只会创建这一次。这就保证了无论getInstance()方法被调用多少次,返回的都是同一个singleton实例;保证了线程的绝对安全,不会出现访问安全的问题。

    但也正式因为singleton实例在HungrySingleton类第一次被类加载器加载时就已经创建完成,若getInstance()方法不被任何地方调用,那么singleton实例就会一直占着内存空间,白白浪费了资源。所以引申出了另一种构建单例模式的方式:懒汉式单例

    二、懒汉式单例

    懒汉式单例的特点是只有在类的全局访问点被访问的时候,类的实例化对象才会创建

    // 懒汉式单例
    public class LazySingleton {
    
        // 构造函数私有化
        private LazySingleton() {}
    
        private static LazySingleton lazySingleton = null;
    
        // 全局访问点
        public  static LazySingleton getInstance(){
            if (lazySingleton == null){
                lazySingleton = new LazySingleton();
            }
            return lazySingleton;
        }
    }
    

    在上述代码中,只有当getInstance()方法被调用时,才会去创建lazySingleton实例。这样就解决了饿汉式模式中的资源占用问题,但同样引申出了另一个问题:线程安全问题

    我们先来创建一个属于我们自己的线程类LazyThread

    // 线程
    public class LazyThread implements Runnable {
        @Override
        public void run() {
            LazySingleton instance = LazySingleton.getInstance();
            // 打印 线程名字 和 instance实例的内存地址
            System.out.println(Thread.currentThread().getName() + ":" +instance);
        }
    }
    

    调用:

    //  创建两个线程
    public static void main(String[] args) {
        Thread thread1 = new Thread(new LazyThread());
        Thread thread2 = new Thread(new LazyThread());
        thread1.start();
        thread2.start();
    }
    

    我们采用debug模式调试一下,先和下图一般,在LazySingleton类中打一个断点

    image20210624102816663.png

    再用鼠标右键点击断点的位置(红色圆点的位置),打开如下图的框之后,先选择红框中的Thread模式,再点击蓝框中的Done按钮。

    image20210624103111658.png

    做完上述的操作之后,我们来用debug模式运行一下main方法

    image20210624103832500.png

    上图红框中内容就是我们所创建的两个线程,目前是Thread-0线程在运行。我们将Thread-0线程运行到lazySingleton = new LazySingleton()这行代码的位置(图1),然后切换为Thread-1线程,并将Thread-1线程同样运行到此位置(图2):

    图1:

    image20210624104448611.png

    图2:

    image20210624104518577.png

    最后:切换回Thread-0线程,并全部放开,让代码一直运行下去;并对Thread-1做出同样的操作。打印出结果:

    image20210624105027443.png

    通过结果可以看出,两个线程获得的lazySingleton实例所对应的内存地址不相同,显然不符合单例模式中的只有一个实例的原则。

    那有什么办法可以保证懒汉式模式在线程环境下安全呢?有,而且很简单,加锁。我们来给getInstance()方法加上锁:

    // 懒汉式
    public class LazySingleton {
    
        // 私有化构造函数
        private LazySingleton() {}
    
        private static LazySingleton lazySingleton = null;
    
        // 加锁
        public synchronized static LazySingleton getInstance(){
            if (lazySingleton == null){
                lazySingleton = new LazySingleton();
            }
            return lazySingleton;
        }
    }
    

    我们再用上述的方式来debug调试一下:

    image20210624110350785.png

    在线程Thread-1进入getInstance()方法内部的时候,线程Thread-0处于MONITOR锁监控的状态。将线程Thread-1运行完后,Thread-0进入getInstance()方法内部,状态更新为RUNNING运行状态。

    image20210624110740849.png

    而此时我们可以看出lazySingleton已经有值了,所以我们将线程Thread-0运行完后,两个线程会打印出一样的结果:

    image20210624110931419.png

    由结果我们可以看出,在给getInstance()方法加上锁之后,线程安全的问题便解决了。但依然可以继续来优化这段懒汉式单例模式的代码。

    // 懒汉式
    public class LazySingleton {
    
        // 私有化构造函数
        private LazySingleton() {}
    
        // volatile 关键字 解决重排序的问题
        private volatile static LazySingleton lazySingleton = null;
    
        public static LazySingleton getInstance(){
            if (lazySingleton == null){
                // 锁代码块
                synchronized (LazySingleton.class) {
                    if (lazySingleton == null){
                        lazySingleton = new LazySingleton();
                    }
                }
            }
            return lazySingleton;
        }
    }
    

    这种方式被称为双重检查锁,它有着以下两点的好处:

    1. 线程由基于LazySingleton整个类的阻塞变为在getInstance()方法内部的阻塞。锁的颗粒度变得更细,锁的代码块变得更小了。
    2. 第一重的if判断,直接分流了一部分在lazySingleton实例化后在进入getInstance()方法的线程,提高了效率。

    但是,只要涉及到加锁的问题,对程序的性能或多或少都有影响,那么有没有不加锁的方式呢?当然也是有的,那就是以类的初始化角度来考虑,使用内部类的方式。

    三、静态内部类实现单例模式

    // 懒汉式模式 和 饿汉式模式 兼顾
    public class InnerClassSingleton {
    
        // 私有化构造函数
        private InnerClassSingleton(){}
    
        public static InnerClassSingleton getInstance(){
            return SingletonHolder.singleton;
        }
    
        // 静态内部类
        private static class SingletonHolder{
            private static final InnerClassSingleton singleton = new InnerClassSingleton();
        }
    }
    

    这种方式兼顾了懒汉式模式饿汉式模式,根据类的加载机制来说,静态内部类SingletonHolder不会随着外部类InnerClassSingleton的加载而加载,只会在被调用时才会加载。

    这里外部类InnerClassSingleton在被类加载器加载后,并不会去进一步加载SingletonHolder类,从而也不会去实例化singleton,也就避免了资源浪费的情况。而在getInstance()方法第一次被调用时,内部类SingletonHolder才会加载,SingletonHolder类中声明的静态对象singleton才会被实例化;后面每一次调用getInstance()方法时,返回的都是此singleton对象,保证了只有一个实例化对象的原则。

    四、用反射的方式来破坏单例

    讲完单例模式的几种实现方式之后,我们来讲一讲破坏单例的方式;虽然日常开发中不会怎么用到,但对面试来说,可以说是一个必考点。多了解了解,总会有意想不到的用处。

    public static void main(String[] args) {
        try {
            // 用反射获得  InnerClassSingleton 类的实例
            Class clazz = InnerClassSingleton.class;
            Constructor constructor = clazz.getDeclaredConstructor(null);
            // 强制访问
            constructor.setAccessible(true);
            InnerClassSingleton instance1 = (InnerClassSingleton)constructor.newInstance();
            
            // 单例模式获取
            InnerClassSingleton instance2 = InnerClassSingleton.getInstance();
            
            System.out.println("利用反射得到的实例对象:"+instance1);
            System.out.println("单例模式的实例对象:"+instance2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    

    上述的测试代码,我分别用反射的方式和单例的方式来获得InnerClassSingleton类的实例,最后打印出来,看一看结果:

    image20210624141829954.png

    可以看出,两次创建的InnerClassSingleton类的实例又不相同了。那怎么杜绝这种办法呢?我们可以来优化一下上述的静态内部类的代码:

    // 懒汉式模式 和 饿汉式模式 兼顾
    public class InnerClassSingleton {
    
        // 私有化构造函数
        private InnerClassSingleton(){
            if (SingletonHolder.singleton != null){
                throw new RuntimeException("不能以这种方式来获得实例对象......");
            }
        }
    
        public static InnerClassSingleton getInstance(){
            return SingletonHolder.singleton;
        }
    
        // 静态内部类
        private static class SingletonHolder{
            private static final InnerClassSingleton singleton = new InnerClassSingleton();
        }
    }
    

    主要看私有构造函数中的代码,我们将这里做了限制,当被外界调用时,直接抛出异常!测试的结果也如我们所愿:

    image20210624142422447.png

    五、用序列化的方式破坏单例

    除了反射之外,用序列化的方式也能破坏单例,达到创建不一样的类的实例的效果。

    先将InnerClassSingleton类实现序列化接口:

    // 懒汉式模式 和 饿汉式模式 兼顾
    public class InnerClassSingleton implements Serializable {
    	// .......   中间的代码查看上面的代码
    }
    

    编写测试代码:

    public static void main(String[] args) {
        try {
            InnerClassSingleton instance1 = InnerClassSingleton.getInstance();
    
            FileOutputStream fos  = new FileOutputStream("singleton.obj");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fos);
            objectOutputStream.writeObject(instance1);
            objectOutputStream.flush();
            objectOutputStream.close();
            fos.close();
    
            FileInputStream fis = new FileInputStream("singleton.obj");
            ObjectInputStream objectInputStream = new ObjectInputStream(fis);
            InnerClassSingleton instance2 = (InnerClassSingleton)objectInputStream.readObject();
            objectInputStream.close();
            fis.close();
    
            System.out.println("利用单例获得实例:"+instance1);
            System.out.println("利用序列化获取的实例:"+instance2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    

    在上面的代码中,我们先获得InnerClassSingleton类的实例instance1,再将instance1写入singleton.obj文件当中;然后再从中取出来,转化为实例instance2;最后将instance1instance2打印出来:

    image20210624143908874.png

    可以看出,两次创建的InnerClassSingleton类的实例又不相同了。那么这种方式的解决方案是什么呢?也不难,只需要加上一个方法就好了:

    public class InnerClassSingleton implements Serializable {
        // .......  代码省略
        // 加上 readResolve() 方法
        private Object readResolve(){
            return SingletonHolder.singleton;
        }
        
        // 静态内部类
        private static class SingletonHolder{
            private static final InnerClassSingleton singleton = new InnerClassSingleton();
        }
    }
    

    再加上readResolve()之后,再来测试一下:

    image20210624145929658.png

    可以看出,两次创建的实例完全相同,完美的解决了序列化的问题。那么为什么加上readResolve()就会解决这个问题呢?这里和JDK的源码有关,我这里就不贴源码了,不便于观看,我这里画了一个时序图,大家可以跟着这个时序图来对照JDK源码进行查看,了解内情。

    1、先从编写的测试代码里面进入ObjectInputStream类中的readObject()方法

    image20210624150733739.png

    2、实序图

    readResolve.png

    以实序图来看,其实方法内部还是创建了一次InnerClassSingleton类的实例,不过是后面用调用readResolve()方法获得的InnerClassSingleton类的实例将它替换掉了,所以打印出的结果依旧是相同的。总体来说,还是白白消耗了内存,那么再来看另一种创建单例的方式。

    六、注册式单例

    注册式单例又被称为登记式单例,大体分为枚举登记容器缓存两种。

    6.1 枚举登记

    public enum  EnumSingleton {
    
        INSTANCE;
    
        // 用来测试对象是否相同
        private Object data;
    
        public Object getData() {
            return data;
        }
    
        public void setData(Object data) {
            this.data = data;
        }
    
        public static EnumSingleton getInstance(){
            return INSTANCE;
        }
    }
    
    6.1.1 序列化破坏

    将上面的测试代码稍微更改一下:

    public static void main(String[] args) {
        try {
            EnumSingleton instance1 = EnumSingleton.getInstance();
    
            instance1.setData(new Object());
            // .......   查看 五、用序列化的方式破坏单例 的测试代码
            EnumSingleton instance2 = (EnumSingleton)objectInputStream.readObject();
            objectInputStream.close();
            fis.close();
    
            System.out.println("利用单例获得实例:"+instance1.getData());
            System.out.println("利用序列化获取的实例:"+instance2.getData());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    

    结果:

    image20210624160430744.png

    由结果可以看出是可行的,那么原理是什么呢?通过上述实序图的方式查看源码:

    1、ObjectInputStream类中的readObject0()方法:

    private Object readObject0(boolean unshared) throws IOException {
        // ......  省略代码
        // 如果是枚举类
        case TC_ENUM:
                        return checkResolve(readEnum(unshared));
        
        // ......
    }
    

    2、readEnum()方法

    private Enum<?> readEnum(boolean unshared) throws IOException {
        // ......
        if (cl != null) {
                try {
                    //  通过Class对象 c1 和 类名 name 来获得唯一的枚举对象
                    @SuppressWarnings("unchecked")
                    Enum<?> en = Enum.valueOf((Class)cl, name);
                    result = en;
                } catch (IllegalArgumentException ex) {
                    throw (IOException) new InvalidObjectException(
                        "enum constant " + name + " does not exist in " +
                        cl).initCause(ex);
                }
                if (!unshared) {
                    handles.setObject(enumHandle, result);
                }
            }
        // ......
    }
    

    通过查看源码发现,枚举类型其实通过Class 对象类和类名找到一个唯一的枚举对象;因此,枚举对象不可能被类加载器加载多次。

    6.1.2 反射破坏

    测试代码:

    public static void main(String[] args) {
        try {
            Class clazz = EnumSingleton.class;
            Constructor constructor = clazz.getDeclaredConstructor(null);
            // 强制访问
            constructor.setAccessible(true);
            EnumSingleton instance1 = (EnumSingleton)constructor.newInstance();
            EnumSingleton instance2 = EnumSingleton.getInstance();
            System.out.println("利用反射得到的实例对象:"+instance1);
            System.out.println("单例模式的实例对象:"+instance2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    

    结果:

    image20210624162535995.png

    它竟然报出java.lang.NoSuchMethodException,意思是没有找到对应的无参的构造函数,这是为什么呢?不急,让我们将EnumSingleton.class这个文件反编译一下(这里使用的是jad反编译工具,不会的同学去网上搜教程,这里不详细讲解了),得到一个EnumSingleton.jad文件,打开文件后发现这么一段代码:

    // .....
    private EnumSingleton(String s, int i){
            super(s, i);
        }
    
    // .....
    static {
            INSTANCE = new EnumSingleton("INSTANCE", 0);
            $VALUES = (new EnumSingleton[] {
                INSTANCE
            });
        }
    

    原来jvm在编译EnumSingleton枚举类时,给它创建了一个有参的构造函数,并再静态代码块里面实例化了INSTANCE对象。那这里,我们再将测试代码修改一下,强制传入两个参数会怎么样:

    public static void main(String[] args) {
        try {
            Class clazz = EnumSingleton.class;
            // 设置两个参数的类型
            Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);
            // 强制访问
            constructor.setAccessible(true);
            // 传入两个参数
            EnumSingleton instance1 = (EnumSingleton)constructor.newInstance("test",111);
            EnumSingleton instance2 = EnumSingleton.getInstance();
            System.out.println("利用反射得到的实例对象:"+instance1);
            System.out.println("单例模式的实例对象:"+instance2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    

    结果:

    image20210624164245873.png

    还是报错,不过这次的错误换成了Cannot reflectively create enum objects,不允许创建枚举类的对象。我们来看看JDK的源码:

    image20210624164418863.png

    constructor.newInstance("test",111)这行代码进入Constructor类中的newInstance()方法我们发现,这里有个判断,如果是对枚举类进行操作,那么直接报出错误;这么看来,是JDK源码帮助我们去拦截了来自反射技术的破坏,那么就可以放宽心了。

    6.2 容器缓存

    容器缓存最经典的例子就是Spring框架中的IOC容器,我们来模拟一下:

    // 容器缓存
    public class ContainerSingleton {
    
        // 私有化构造函数
        private ContainerSingleton(){}
    
        private static Map<String,Object> iocMap = new ConcurrentHashMap<>();
    
        // 传入 类名参数
        public static Object getBean(String className){
            if (className == null || "".equals(className)){
                return null;
            }
            synchronized (iocMap){
                // 判断容器中是否有该属性
                if (!iocMap.containsKey(className)){
                    Object object = null;
                    try {
                        object = Class.forName(className).newInstance();
                        iocMap.put(className,object);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                    return object;
                } else {
                    return iocMap.get(className);
                }
            }
        }
    }
    

    iocMap中的key存的是类名,value存的是该类的实例化对象,通过这种方式来保证每次获得的都是一个类的相同实例。

    七、ThreadLocal线程单例

    ThreadLocal方式创建的单例对象是最为特殊的,因为它是一个伪单例,它只能保证同一个线程内创建的类的实例是相同的,有着天生的线程安全;但不能保证创建的类的实例是全局唯一的;先来看看代码:

    public class ThreadLocalSingleton {
    
        public ThreadLocalSingleton() {}
    
        private static final ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal(){
            @Override
            protected Object initialValue() {
                return new ThreadLocalSingleton();
            }
        };
    
        public static ThreadLocalSingleton getInstance(){
            return threadLocal.get();
        }
    }
    

    线程代码:

    public class LazyThread implements Runnable {
        @Override
        public void run() {
            ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();
            System.out.println(Thread.currentThread().getName() + ":" +instance);
        }
    }
    

    测试代码:

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance());
        System.out.println("————————————————————————————————————————");
        Thread thread1 = new Thread(new LazyThread());
        Thread thread2 = new Thread(new LazyThread());
        thread1.start();
        thread2.start();
    }
    

    结果:

    image20210624171030320.png

    从结果可以看出,再主线程main中,无论我们调用多少次getInstance()方法,获得的ThreadLocalSingleton的实例都是相同的。而两个子线程Thread-0Thread-1都获得了不同的实例。那么这是怎么做到了呢?

    通过查看源码(别问我为啥不贴源码,问就是看不到,它的底层不是用Java写的【流泪】,感兴趣的小伙伴可以百度,有大神,我也是百度的,yyds!!!)我们发现,ThreadLocalThreadLocalSingleton类的实例对象全部放在了ThreadLocalMap中,为每一个线程提供了对象,实际上是以空间换时间来实现线程隔离的。这也使ThreadLocal技术频繁的使用了于用户登陆时,存储用户的登录信息方面。甚至于Mybatis中多个数据源切换的技术也是用它实现的。

    最后


    如果这篇文章对你有帮助的话,麻烦动动小手,点个赞,万分感谢!!!

    如果有小伙伴发现文章里面有错误,欢迎来指正,不胜感激!!!

  • 相关阅读:
    js 小程序获取本周七天
    js 时间处理函数 获取今天的前几天和后几天的任意一天
    路由配置
    3 vue-router 的默认hash 改mode:history去除#号 传参
    Eslint 配置及规则说明(报错)
    打开左侧菜单栏
    sublime 对vue的高亮显示
    gitlab 本地建库配置 config
    GIT 安装
    v-html的应用
  • 原文地址:https://www.cnblogs.com/caimm/p/14929615.html
Copyright © 2011-2022 走看看