• 单例模式


    概述

    单例模式(Singleton Pattern)是23中设计模式之一属于“GOF”设计模式,指的是一个类在任何情况下只有一个实例,并提供一个全局访问点以便访问,可以解决重复设计的问题,使得软件能够重复使用。

    实现单例模式解决一下问题

    1. 隐藏构造函数,也就是构造函数私有化
    2. 提供全局访问函数(getInstance)

    饿汉式单例

    何为饿汉式单例从字面上理解就是类加载的时候就初始化创建单例对象。是线程安全的,因为在线程运行前对象就已经存在了,不存在访问安全问题

    看下示例代码

    /**
     * @Auther:gudazhi
     * @Description 饿汉式单例
     **/
    public class HungrySingleton {
        private static final HungrySingleton hungrySingleton = new HungrySingleton();
    
        private HungrySingleton(){}
    
        public static HungrySingleton getInstance(){
                return hungrySingleton;
        }
    }

    还有另外一种写法,利用静态代码块的机制:

    /**
     * @Auther:gudazhi
     * @Description 饿汉式单例-静态代码块
     **/
    public class HungryStaticSingleton {
        private static final HungryStaticSingleton hungrystaticSingleton;
    
        static {
            hungrystaticSingleton = new HungryStaticSingleton();
        }
        private HungryStaticSingleton(){}
    
        public static HungryStaticSingleton getInstance(){
                return hungrystaticSingleton;
        }
    }

    优点:线程安全的,没有任何锁执行效率高

    缺点:有没有看到不过你用不用这个对象这种单例模式都创建了对象占了内存,有可能占着茅坑不拉X

    懒汉式单例

     懒汉式单例顾名思义就是在外部类调用的时候单例才被创建,下面来看下懒汉式单例的简单实现

    /**
     * @Auther:gudazhi
     * @Description 懒汉式单例-简单
     **/
    public class LazySingleton {
        private static LazySingleton lazySingleton = null;
    
        private LazySingleton(){}
    
        public static LazySingleton getInstance()
        {
            if (lazySingleton == null)
                 lazySingleton = new LazySingleton();
            return lazySingleton;
        }
    }

     不知大家有没有发现问题,这样写是不是有线程问题我们写个线程测试一下

    /**
     * @Auther:gudazhi
     * @Description 测试简单懒汉式单例线程
     **/
    public class TestLazySingletonThread implements Runnable{
        @Override
        public void run() {
            LazySingleton lazySingleton = LazySingleton.getInstance();
            System.out.println(Thread.currentThread().getName()+":"+lazySingleton);
        }
    }

    测试示例

    public static void main(String[] args) {
        Thread t1 = new Thread(new TestLazySingletonThread());
        Thread t2 = new Thread(new TestLazySingletonThread());
        t1.start();
        t2.start();
        System.out.println("Main End");   
    }

    测试结果大家会发现有时对象相同有时不相同,显然这样违背类单例(推荐使用多线程来调试我用的Idea),那么我们就解决这个问题

    修改getInstance()方法,增加synchronized关键字

     public static synchronized LazySingleton getInstance()
    {
         if (lazySingleton == null)
              lazySingleton = new LazySingleton();
         return lazySingleton;
    }

    这样是可以解决线程安全的问题,解决对象多次创建的问题,不过有没有发现getInstance()方法会造成堵塞,也许我的对象不为空那我的调用对象仍然要获得锁,这样造成不必要的性能问题

    双重检查锁的单例模式

    /**
     * @Auther:gudazhi
     * @Description 双重检查锁单例模式
     **/
    public class LazyDoubleCheckSingleton {
        private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    
        private LazyDoubleCheckSingleton(){}
    
        public static LazyDoubleCheckSingleton getInstance()
        {
            if (lazyDoubleCheckSingleton == null)
            {
                synchronized (LazyDoubleCheckSingleton.class)
                {
                    if (lazyDoubleCheckSingleton == null)
                        lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
            return lazyDoubleCheckSingleton;
        }
    }

    这样我们会发现阻塞是在getInstance方法里面,相对与上面性能好了些,不过是要synchronized关键字势必会影响效率,那么就没有解决方式吗?当然是有的。可以从类初始化的角度来考虑

    采用静态内部类的方式

    /**
     * @Auther:gudazhi
     * @Description 懒汉单例-静态内部类
     **/
    public class LazyInnerClassSingleton {
        private LazyInnerClassSingleton(){}
    
        public static final LazyInnerClassSingleton getInstance()
        {
            return LazyHolder.LAZY;
        }
    
        private static class LazyHolder
        {
            private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
        }
    }

    小结:利用内部类的特性,内部类一定是在方法调用初始化,巧妙的避免了线程安全问题

    单例破坏场景

    反射破坏单例

    大家有没有发现,上面介绍的单例模式的构造方法除了加上 private 以外,没有做任何处理。如果我们调用者喜欢装逼使用反射来调用其构造方法,然后与调用 getInstance()相比就会有两个不同的实例。现在来看一段测试代码,以 LazyInnerClassSingleton 为例:

    public static void main(String[] args) {
        LazyInnerClassSingleton lazyInnerClassSingleton = LazyInnerClassSingleton.getInstance();
        System.out.println(lazyInnerClassSingleton);
        try {
             Class<?> clazz = LazyInnerClassSingleton.class;
             Constructor c = clazz.getDeclaredConstructor();
             c.setAccessible(true);
             LazyInnerClassSingleton lazyInnerClassSingleton2 = (LazyInnerClassSingleton)c.newInstance();
             System.out.println(lazyInnerClassSingleton2);
             System.out.println(lazyInnerClassSingleton == lazyInnerClassSingleton2);
         }catch (Exception e)
         {
             e.printStackTrace();
         }
    }

    通过测试我们会发现创建了两个对象,这显然违背了我们的单例模式,那么我们可以在构造方法里做限制代码如下

    public class LazyInnerClassSingleton {
        private LazyInnerClassSingleton(){
            if (LazyHolder.LAZY != null)
                throw new RuntimeException("不允许创建多个实例");
        }
    
        public static final LazyInnerClassSingleton getInstance()
        {
            return LazyHolder.LAZY;
        }
    
        private static class LazyHolder
        {
            private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
        }
    }

    我们抛出异常让调用者自行修改代码,让他装逼失败

    序列化破坏单例

    当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例。
    序列化就是说把内存状态的对象转换成字节码形式,从而转换一个IO流,写入到其他地方(可以是磁盘,网络IO),内存状态永久保存下来

    反序列化将已经持久化的字节码内容, 转换为 IO 流,通过 IO 流的读取, 进而将读取的内容转换为 Java 对象,在转换过程中会重新创建对象 new

    来看下面的代码

    public class SerializableSingleton implements Serializable {
        private static final SerializableSingleton INSTANCE = new SerializableSingleton();
    
        private SerializableSingleton(){}
    
        public static SerializableSingleton getInstance()
        {
            return INSTANCE;
        }
    }

    测试代码

    public class SerializableSingletonTest {
        public static void main(String[] args) {
            SerializableSingleton singleton1 = null;
            SerializableSingleton singleton2 = SerializableSingleton.getInstance();
    
            FileOutputStream fos = null;
            try{
                fos = new FileOutputStream("serializationSingleton.obj");
                ObjectOutputStream os = new ObjectOutputStream(fos);
                os.writeObject(singleton2);
                os.flush();
                os.close();
    
                FileInputStream fis = new FileInputStream("serializationSingleton.obj");
                ObjectInputStream is = new ObjectInputStream(fis);
                singleton1 = (SerializableSingleton)is.readObject();
                is.close();
    
                System.out.println(singleton1);
                System.out.println(singleton2);
                System.out.println(singleton1 == singleton2);
            }catch (Exception e)
            {
                e.printStackTrace();
            }
        }
    }

    我们会发现两次对象不相同,那么我们如何保证序列化的情况下也能够实现单例?其实很简单,只需要增加 readResolve()方法即可。来看优化代码

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

    这样就会发现结果对象是相同的,大家一定会关心这是什么原因呢?为什么要这样写?看上去很神奇的样子,也让人有些费解,大家可以看一下JDK源码ObjectInputStream的readObject方法会发现最后通过反射来查询类里是否有readResolve方法有就调用,不过虽然我们看到对象相同实际上在源码中我们看到对象还是被创建了只是没有返回。那如果创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大,难道真的就没办法从根本上解决问题吗?下面我们来注册式单例也许能帮助到你

    注册式单例

    注册式单例也叫登记式单例,就是把每一个示例都登记到一个地方,使用唯一标示获取。注册式单例有两种:一种是容器式单例,一种为枚举式单例,先来看看枚举式的写法

    /**
     * @Auther:gudazhi
     * @Description 枚举式单例
     **/
    public enum EnumSingleton {
        INSTANCE;
    
        public static EnumSingleton getInstance()
        {
            return INSTANCE;
        }
    }

    序列化破坏测试

    public class EnumSingletonTest {
        public static void main(String[] args) {
            EnumSingleton enumSingleton1 = EnumSingleton.getInstance();
            EnumSingleton enumSingleton2 = null;
            FileOutputStream fos = null;
            try{
                fos = new FileOutputStream("EnumSingleton.obj");
                ObjectOutputStream os = new ObjectOutputStream(fos);
                os.writeObject(enumSingleton1);
                os.flush();
                os.close();
    
                FileInputStream fis = new FileInputStream("EnumSingleton.obj");
                ObjectInputStream is = new ObjectInputStream(fis);
                enumSingleton2 = (EnumSingleton)is.readObject();
                is.close();
    
                System.out.println(enumSingleton1 == enumSingleton2);
            }catch (Exception e)
            {
                e.printStackTrace();
            }
        }
    }

    我们会发现结果为true,那么这是为什么呢,我们查看JDK源码我们发现枚举类型其实通过类名和 Class 对象类找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。那么反射是否能破坏枚举式单例呢?来看一段测试代码:

    try {
            Class clazz = EnumSingleton.class;
            Constructor c = clazz.getDeclaredConstructor();
            c.setAccessible(true);
            c.newInstance();
        }catch (Exception e)
        {
            e.printStackTrace();
        }

    运行结果

     发现无法找到构造器,我们查看java.lang.Enum 的源码发现只有一个protect构造器

    protected Enum(String name, int ordinal) {
            this.name = name;
            this.ordinal = ordinal;
        }

    重新写测试类

    try {
             Class clazz = EnumSingleton.class;
             Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
             c.setAccessible(true);
             c.newInstance("singleton",666);
        }catch (Exception e)
        {
             e.printStackTrace();
        }

    运行结果

    发现反射失败,我们习惯性看下JDK源码发现

     if ((clazz.getModifiers() & Modifier.ENUM) != 0)
      throw new IllegalArgumentException("Cannot reflectively create enum objects");

    原来我们JDK底层就做了类型的判断如果是枚举类型就直接抛异常,所以在 JDK 枚举的语法特殊性,以及反射也为枚举保驾护航,让枚举式单例成为一种比较优雅的实现。
    下面来看容器式单例

    /**
     * @Auther:gudazhi
     * @Description 容器式单例
     **/
    public class ContainerSingleton {
        private static Map<String,Object> ios = new ConcurrentHashMap<>();
    
        private ContainerSingleton(){}
    
        public static Object getInstance(String className)
        {
            if (!ios.containsKey(className)) {
                synchronized (ios) {
                    if (!ios.containsKey(className)) {
                        try {
                            Object obj = Class.forName(className).newInstance();
                            ios.put(className, obj);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            return ios.get(className);
        }
    }

    容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的。到此注册式单例介绍完毕。我们还可以来看看 Spring 中的容器式单例的实现代码:

    public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {
      /** Cache of unfinished FactoryBean instances: FactoryBean name --> BeanWrapper */
      private final Map<String, BeanWrapper> factoryBeanInstanceCache = new ConcurrentHashMap<>(16);
      ...
    }

    ThreadLocal线程单例

    最后介绍ThreadLocal单例,讲讲线程单例实现 ThreadLocal。ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全。下面我们来看代码:

    /**
     * @Auther:gudazhi
     * @Description TreadLocal单例
     **/
    public class ThreadLocalSingleton {
        private ThreadLocalSingleton(){}
    
        private static final ThreadLocal<ThreadLocalSingleton> threadLocalSingleton = new ThreadLocal<ThreadLocalSingleton>(){
            @Override
            protected ThreadLocalSingleton initialValue() {
                return new ThreadLocalSingleton();
            }
        };
    
        public static ThreadLocalSingleton getInstance()
        {
            return threadLocalSingleton.get();
        }
    }

    测试代码

    public class ThreadLocalSingletonTest {
        public static void main(String[] args) {
            System.out.println(ThreadLocalSingleton.getInstance());
            System.out.println(ThreadLocalSingleton.getInstance());
            System.out.println(ThreadLocalSingleton.getInstance());
            System.out.println(ThreadLocalSingleton.getInstance());
    
            Thread t1 = new Thread(){
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+":"+ThreadLocalSingleton.getInstance());
                    System.out.println(Thread.currentThread().getName()+":"+ThreadLocalSingleton.getInstance());
                    System.out.println(Thread.currentThread().getName()+":"+ThreadLocalSingleton.getInstance());
                }
            };
            Thread t2 = new Thread(){
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+":"+ThreadLocalSingleton.getInstance());
                    System.out.println(Thread.currentThread().getName()+":"+ThreadLocalSingleton.getInstance());
                    System.out.println(Thread.currentThread().getName()+":"+ThreadLocalSingleton.getInstance());
                }
            };
            t1.start();
            t2.start();
        }
    }

    测试结果

    可以发现在同一个线程里可以保证对象都是同一个,实际上ThreadLocal是以线程为唯一标示来管理对象的,同一线程下对象只会有一个。

    单例模式小结

    单例例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用。单例模式看起来非常简单,实现起来其实也非常简单。 我们一定要学习技术的深度。祝学习愉快

    参考:https://en.wikipedia.org/wiki/Singleton_pattern

  • 相关阅读:
    sudo: no tty present and no askpass program specified 解决方法
    中间件kingshard入门(一):基本安装
    mysql主从配置
    haproxy+keepalive双主高可用实现负载均衡
    lvs负载均衡配置
    docker可视化集中管理工具shipyard安装部署
    安装nodejs
    redis数据导入与导出以及配置使用
    搭建ss5环境
    thinkphp5+python.apscheduler实现计划任务
  • 原文地址:https://www.cnblogs.com/gudazhi/p/10579549.html
Copyright © 2020-2023  润新知