• 设计模式(一)之单例模式(Singleton Pattern)深入浅出


    单例模式介绍:单例模式是指确保一个类在任何情况下都绝对只有一个实例,并且提供一个全局的访问点。隐藏其所有构造方法,属于创新型模式。

    常见的单例有:ServletContext、ServletConfig、ApplicationContext、DBPool

    单例模式的优点:

    • 在内存中只有一个实例,减少内存开销。
    • 可以避免对资源的占用
    • 设置全局访问点,严格控制访问

    单例模式的缺点:

    • 没有接口,扩展困难
    • 如果要扩展单例对象,只有修改代码,没有其他捷径

    以下是单例模式的种类及优缺点分析

    饿汉式单例

    在单例类首次加载时就创建实例

     第一种写法:

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

     第二种写法: 

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

      缺点:单例实例在类装载时就构建,浪费资源空间

    懒汉式单例

    一、懒汉式第一种:

    首先先简单实现以下

    public class LazySimpleSingleton {
    
        private static LazySimpleSingleton lazySingleton = null;
    
        private LazySimpleSingleton() {
        }
    
        public static LazySimpleSingleton getInstance() {
    
            if (lazySingleton == null) {
                lazySingleton = new LazySimpleSingleton();
            }
            return lazySingleton;
        }
    }

     我们用线程测一下在多线程场景下会不会出现问题

    先创建一个线程类

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

         测试

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

      运行结果

     结果发现创建的对象不一样

    如果在方法上加入锁会解决问题

    public class LazySimpleSingleton {
    
        private static LazySimpleSingleton lazySingleton = null;
    
        private LazySimpleSingleton() {
        }
    
        public synchronized static LazySimpleSingleton getInstance() {
    
            if (lazySingleton == null) {
                lazySingleton = new LazySimpleSingleton();
            }
            return lazySingleton;
        }
    }
    

      虽然jdk1.6之后对synchronized性能优化了不少,但是还是存在一定的性能问题,这种写法会造成整个类被锁住,大大降低了性能

    于是我们有了新的写法

    二、懒汉式第二种:

    public class LazyDoubleCheckSingleton {
    
        private volatile static LazyDoubleCheckSingleton lazySingleton = null;
    
        private LazyDoubleCheckSingleton() {
        }
        //    适中方案
        //    双重检查锁
        public static LazyDoubleCheckSingleton getInstance() {
    
            if (lazySingleton == null) {
                synchronized (LazyDoubleCheckSingleton.class) {
                    if (lazySingleton == null) {
                        lazySingleton = new LazyDoubleCheckSingleton();
                    }
                }
            }
            return lazySingleton;
        }
    }

    知识补充:

      1、线程安全性开发遵循三个原则:

    • 原子性:即一个操作或者多个操作要么全部执行,要么都不执行
    • 可见性:多个线程访问同一变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值
    • 有序性:程序执行的顺序按照代码的先后顺序执行

      通过对这段代码线程的debug发现,这里双重检查体现了可见性

      2、JVM:CPU在执行的时候会转换成JVM指令

                 lazySingleton = new LazySimpleSingleton(); 这行代码实际进行了如下操作

    •     第一步、分配内存给对象
    •     第二步、初始化对象
    •     第三步、将初始化对象和内存地址关联(赋值)
    •     第四步、用户初次访问

    在多线程环境中,第二步和第三步可能会发生颠倒,这就需要指令重排序,于是我们在变量声明上加上volatile关键字就很好的解决了问题

    volatile相关博客

    三、懒汉式第三种

    通过内部类的方式实现

    public class LazyInnerClassSingleton {
    
        private LazyInnerClassSingleton() {
        }
    
        public static final LazyInnerClassSingleton getInstance() {
            return LazyHolder.LAZY;
        }
    
        private static class LazyHolder {
            private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
        }
    }
    

      性能上最优的一种写法,全程没有用到synchronized,

      通过懒加载饿汉式写法达到了懒汉式的目的,LazyHolder里面的逻辑要等到外面调用才执行,巧妙地运用了内部类的特性

      有人会问这个不用考虑线程安全吗?其实这是利用了JVM底层的执行逻辑,完美的避开了线程安全性的问题

    但是我们会考虑另一个问题,该类构造器虽然私有了,但是还是会被反射攻击,难逃反射法眼

    我们来测试一下

    public class LazyInnerClassSingletonTest {
    
        public static void main(String[] args) {
    
            try {
    //          调用者装B,不走寻常路,显然搞坏了单例
                Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class;
                Constructor<LazyInnerClassSingleton> constructor = clazz.getDeclaredConstructor();
                constructor.setAccessible(true);//强吻(问)
                LazyInnerClassSingleton instance = constructor.newInstance();
                System.out.println(instance);
    //          正常调用
                LazyInnerClassSingleton instance2 = LazyInnerClassSingleton.getInstance();
                System.out.println(instance2);
                System.out.println(instance == instance2);
    
            } 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流,在转换过程中重新创建对象new。

    我们拿饿汉式单例测试一下

    单例类:

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

    测试类:

    public class SeriableSingletonTest {
    
        public static void main(String[] args) {
    
            FileOutputStream fso = null;
            SeriableSingleton s1 = null;
            SeriableSingleton s2 = SeriableSingleton.getInstance();
    
            try {
                fso = new FileOutputStream("SeriableSingleton.obj");
                ObjectOutputStream oos = new ObjectOutputStream(fso);
                oos.writeObject(s2);
                oos.flush();
                oos.close();
    
                FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
                ObjectInputStream ois = new ObjectInputStream(fis);
                s1 = (SeriableSingleton) ois.readObject();
                ois.close();
    
                System.out.println(s1);
                System.out.println(s2);
                System.out.println(s1 == s2);
    
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

      运行结果

    显然反序列化破坏了单例

    现在我们通过源码寻找答案

    进入readObject()方法

    方法通过调用readObject0(false)返回结果,再次进入readObject0()方法

     找到object类型,调用了checkResolve(readOrdinaryObject(unshared)),进入readOrdinaryObject方法

    在这里我们找到了实例化对象的语句

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

    意思是如果这个对象能被初始化就实例化对象,否则等于null

    在这里打个断点调试一下,确实实例化了对象,存在私有构造方法也会实例化对象

    接着往下看

    如果desc.hasReadResolveMethod()返回true,就调用Object rep = desc.invokeReadResolve(obj);返回obj

    进入hasReadResolveMethod

     源码分析过后发现这个hasReadResolveMethod()方法是用来判断readResolve方法是否存在,如果存在返回true,不存在返回false

    再看invokeReadResolve()方法

     返回了readResolve这个方法的返回值,

    所以经过这个判断会重新加载对象并返回

     接下来我们得出结论,代码增加重写方法readResolve

    public class SeriableSingleton implements Serializable {
    
        private static final SeriableSingleton singleton = new SeriableSingleton();
    
        private SeriableSingleton() {
        }
    
        public static SeriableSingleton getInstance() {
            return singleton;
        }
    
        protected Object readResolve() {
            return singleton;
        }
    }
    

      再次运行解决了序列化的问题

    但是值得我们注意的是,重写readResolve方法只不过是覆盖了反序列化出来的对象,对象还是创建了2次,

    由于发生再JVM层面,相对来说比较安全,在之前没有被引用的对象会被GC回收(JVM知识点)

    注册式单例

     一、第一种写法

    使用枚举类实现单例模式,也是《Effictive Java》这本书推荐的写法

    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;
        }
    }

     1、首先判断线程安全性

      通过反编译工具JAD得到枚举类的源码,附:JAD下载地址

    public final class EnumSingleton extends Enum
    {
    
        public static EnumSingleton[] values()
        {
            return (EnumSingleton[])$VALUES.clone();
        }
    
        public static EnumSingleton valueOf(String name)
        {
            return (EnumSingleton)Enum.valueOf(com/zc/singleton/register/EnumSingleton, name);
        }
    
        private EnumSingleton(String s, int i)
        {
            super(s, i);
        }
    
        public Object getData()
        {
            return data;
        }
    
        public void setData(Object data)
        {
            this.data = data;
        }
    
        public static EnumSingleton getInstance()
        {
            return INSTANCE;
        }
    
        public static final EnumSingleton INSTANCE;
        private Object data;
        private static final EnumSingleton $VALUES[];
    
        static 
        {
            INSTANCE = new EnumSingleton("INSTANCE", 0);
            $VALUES = (new EnumSingleton[] {
                INSTANCE
            });
        }
    }

    通过代码发现,静态代码块实例化了单例类,属于饿汉式单列,不存在线程安全性问题,这个实例化过程发生在JVM层面,所以可以认为懒加载

    2、测试序列化

    public class EnumSingletonTest {
    
    public static void main(String[] args) { FileOutputStream fso = null; EnumSingleton s1 = null; EnumSingleton s2 = EnumSingleton.getInstance(); s2.setData(new Object()); try { fso = new FileOutputStream("EnumSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fso); oos.writeObject(s2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("EnumSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (EnumSingleton) ois.readObject(); ois.close(); System.out.println(s1.getData()); System.out.println(s2.getData()); System.out.println(s1.getData() == s2.getData()); } catch (Exception e) { e.printStackTrace(); } } }

    运行结果 :

     

    枚举类是怎样避免不被序列化破坏的呢?我们来查看源码

    首先进入枚举类型case

     进入readEnum方法

    通过枚举类对象根据注册的类名获取实例然后返回,所以不会创建新的对象

    3、测试反射

    //        反射
            try {
                Class<EnumSingleton> clazz = EnumSingleton.class;
                Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor();
                constructor.setAccessible(true);
                constructor.newInstance();
    
            }catch (Exception e){
                e.printStackTrace();
            }

    运行结果 :

    报错:没有找到这样的构造方法

    反编译获取的类有这样的一个构造方法

    private EnumSingleton(String s, int i)
        {
            super(s, i);
        }

    我们用这个构造再实例化一次看看

    测试:

    //        反射
            try {
                Class<EnumSingleton> clazz = EnumSingleton.class;
                Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor(String.class,int.class);
                constructor.setAccessible(true);
                EnumSingleton instance = constructor.newInstance("zhou", 666);
                System.out.println(instance);
    
            }catch (Exception e){
                e.printStackTrace();
            }

    运行结果:

    报错:不能通过反射创建这个枚举对象

    查看源码:

     得知如果该类的类型为枚举类,就抛出异常

    总结:从JDK层面就为枚举类不被实例化和反射保驾护航

    二、第二种写法

    容器式单例,Spring容器中单例的写法

    public class ContainerSingleton {
    
        private ContainerSingleton() {}
    
        private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
    
        public static Object getBean(String className){
    
            synchronized (ioc){
    
                if (!ioc.containsKey(className)){
                    Object obj = null;
                    try {
                        obj = Class.forName(className).newInstance();
                        ioc.put(className,obj);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                    return obj;
                }
                return ioc.get(className);
            }
    
        }
    }

    优点:对象方便管理,其实也是属于懒加载

    ThreadLocal

    使用ThreadLocal实现单例模式

    //伪线程安全
    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 ExectorTread implements Runnable {
    
    
        @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());
            System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
            System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
        }
    }
    public class ThreadLocalSingletonTest {
    
        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.currentThread().getName() +  ":" +ThreadLocalSingleton.getInstance());
            System.out.println(Thread.currentThread().getName() +  ":" +ThreadLocalSingleton.getInstance());
    
            Thread t1 = new Thread(new ExectorTread());
            t1.start();
            Thread t2 = new Thread(new ExectorTread());
            t2.start();
        }
    }

    打印结果

     结论:该单例在单个线程中可以保持单例,但是每个其他线程互相都不一样

        原理:每次获取实例会从ThreadLocalMap中取值,而每个单例的key就是线程名

    属于注册式单例(容器形式)

    应用场景:Spring的orm框架中

     以上对单例模式的介绍到此结束,欢迎批评指正。 附:源码地址

  • 相关阅读:
    关于敏捷软件开发的一些感悟
    求出矩阵中,所有元素相加和最大的分块矩阵。
    小组作业提交报告
    结对项目实训——电梯调度
    关于代码测试方面的一些想法和感悟
    用c语言实现文本文件中的字符筛选分析(二)
    用c语言实现文本文件中的字符筛选分析(一)
    4月19日会议(整理——郑云飞)
    4月18日会议总结(整理—祁子梁)
    每日任务看板展示—第一周
  • 原文地址:https://www.cnblogs.com/itzhoucong/p/13325014.html
Copyright © 2020-2023  润新知