• 设计模式(一):单例模式


    单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。

    单例模式一般体现在类声明中,单例的类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

    适用场合:

    • 需要频繁的进行创建和销毁的对象;
    • 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
    • 工具类对象;
    • 频繁访问数据库或文件的对象。

    比如:许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

    优点:

    • 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如网站首页页面缓存)。
    • 避免对资源的多重占用(比如写文件操作)。

    二、实现方式

    1、普通饿汉式(线程安全,不能延时加载

    所谓饿汉。这是个比较形象的比喻。对于一个饿汉来说,他希望他想要用到这个实例的时候就能够立即拿到,而不需要任何等待时间。

    public class Singleton {
    
        private final static Singleton INSTANCE = new Singleton();
    
        private Singleton(){}
    
        public static Singleton getInstance(){
            return INSTANCE;
        }
    }

    优点:写法简单 线程安全

    通过static的静态初始化方式,在该类第一次被加载的时候,就有一个SimpleSingleton的实例被创建出来了。这样就保证在第一次想要使用该对象时,他已经被初始化好了。

    同时,由于该实例在类被加载的时候就创建出来了,所以也避免了线程安全问题。

    JVM类加载机制中:

    “ 并发:

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

    特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行<clinit>()方法,因为在同一个类加载器下,一个类型只会被初始化一次。 ”

    缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。

    在类被加载的时候对象就会实例化。这也许会造成不必要的消耗,因为有可能这个实例根本就不会被用到。

    想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。

    解决不能Lazy Loading懒加载问题的办法:第一种是使用静态内部类的形式。第二种是使用懒汉式。下文会介绍。

    2、静态代码块饿汉式(线程安全,不能延时加载

    public class Singleton {
    
        private static Singleton instance;
    
        static {
            instance = new Singleton();
        }
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            return instance;
        }
    }

    和第一种一样,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。

    3、静态内部类(线程安全,延迟加载,效率高

    public class Singleton {
    
        private Singleton() {}
    
        private static class SingletonInstance {
            private static final Singleton INSTANCE = new Singleton();
        }
    
        public static Singleton getInstance() {
            return SingletonInstance.INSTANCE;
        }
    }

    加载类 Singleton 时不会实例化对象,加载类 SingletonInstance 时才会实例化对象(也就是调用Singleton的getInstance方法时),实现了延迟加载。

    关于类加载机制:JVM类加载机制

    优点:线程安全,延迟加载,效率高。

    4、枚举(线程安全,不能延时加载

    public enum Singleton {
        INSTANCE;
        public void whateverMethod() {
    
        }
    }

    这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

    由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过,但是不代表他不好。

    原理其实也是利用类加载机制实现线程安全。

    反编译后:

    public final class Singleton extends Enum<Singleton> {
        public static final Singleton INSTANCE = new Singleton("INSTANCE", 0);
        private static final Singleton[] $VALUES;
    
        public static Singleton[] values() {
            return (Singleton[])$VALUES.clone();
        }
    
        public static Singleton valueOf(String string) {
            return Enum.valueOf(Singleton.class, string);
        }
    
        private Singleton(String string, int n) {
            super(string, n);
        }
    
        public void whateverMethod() {
        }
    
        static {
            $VALUES = new Singleton[]{INSTANCE};
        }
    }

    关于枚举原理:JDK源码学习笔记——Enum枚举使用及原理

    优点:简单 线程安全

    缺点:不能延迟加载 使用较少

    5、普通懒汉式(线程不安全,可延时加载

    public class Singleton {
    
        private static Singleton singleton;
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            if (singleton == null) {
                singleton = new Singleton();
            }
            return singleton;
        }
    }

    优点:可以实现延迟加载

    缺点:线程不安全

    多个线程可能同时进入if 中,创建出多个实例

    6、synchronized 懒汉式(线程安全,可延时加载,效率低

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

    优点:可以实现延迟加载,线程安全

    缺点:效率低

    只有第一次创建实例的时候需要同步,其他情况都不需要。

    我们知道synchronized是一个效率比较低的加锁方式,而每次获取实例都会同步加锁(本身不需要同步,直接返回 instance 即可),效率会很低。

    7、双重校验锁懒汉式(线程安全,可延时加载,效率高

    详细可参考:Java并发(七):双重检验锁定DCL   Java并发(二):Java内存模型

    对于第六中方法进行优化,减小锁的粒度:

    public class Singleton {
            private static Singleton singleton;
            Integer a;
    
            private Singleton(){}
    
            public static Singleton getInstance(){
                if(singleton == null){                              // 1 只有singleton==null时才加锁,性能好
                    synchronized (Singleton.class){                 // 2
                        if(singleton == null){                      // 3
                            singleton = new Singleton();            // 4
                        }
                    }
                }
                return singleton;
            }
        }

    会因为重排序出现问题:

    线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。

    由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,更新变量并将其指向部分初始化的对象。

    线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有穿过B使用的内存(缓存一致性)),程序很可能会崩溃。

    利用volatile限制重排序:

    public class Singleton {
    
        private static volatile Singleton singleton;
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }

    三、单例与序列化

    1、序列化对单例的破坏

    双重检验锁实现单例:

    public class Singleton implements Serializable{
        private volatile static Singleton singleton;
        private Singleton (){}
        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }

    测试序列化对单例的影响:

    public class SerializableDemo1 {
        //为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
        //Exception直接抛出
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            //Write Obj to file
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
            oos.writeObject(Singleton.getSingleton());
            //Read Obj from file
            File file = new File("tempFile");
            ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
            Singleton newInstance = (Singleton) ois.readObject();
            //判断是否是同一个对象
            System.out.println(newInstance == Singleton.getSingleton());
        }
    }
    //false

    通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。

    2、分析

    ois.readObject();  调用的 readOrdinaryObject 方法

    private Object readOrdinaryObject(boolean unshared)
            throws IOException
        {
            //此处省略部分代码
    
            Object obj;
            try {
                obj = desc.isInstantiable() ? desc.newInstance() : null;
            } catch (Exception ex) {
                throw (IOException) new InvalidClassException(
                    desc.forClass().getName(),
                    "unable to create instance").initCause(ex);
            }
    
            //此处省略部分代码
    
            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) {
                    handles.setObject(passHandle, obj = rep);
                }
            }
    
            return obj;
        }

    isInstantiable:如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。针对serializable和externalizable我会在其他文章中介绍。

    desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象。

    hasReadResolveMethod:如果实现了serializable 或者 externalizable接口的类中包含readResolve则返回true

    invokeReadResolve:通过反射的方式调用要被反序列化的类的readResolve方法。

    原因:序列化会通过反射调用无参数的构造方法创建一个新的对象

    解决:在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

    3、解决

    public class Singleton implements Serializable{
        private volatile static Singleton singleton;
        private Singleton (){}
        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
        private Object readResolve() {
            return singleton;
        }
     }

    总结:一旦实现了Serializable接口之后,就不再是单例的了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象。解决办法就是使用readResolve()方法来避免此事发生。

    四、关于枚举实现单例的序列化问题

    为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:

    在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

    所以,枚举实现的单例不会有序列化问题

    参考资料 / 相关推荐:

    Java并发(二):Java内存模型

    Java并发(七):双重检验锁定DCL 

    JDK源码学习笔记——Enum枚举使用及原理

    JVM类加载机制

    单例模式的八种写法比较

    设计模式(二)——单例模式

    深度分析Java的枚举类型—-枚举的线程安全性及序列化问题

  • 相关阅读:
    通过注册表实现开机自启的取消
    数据库为什么要使用B+树
    PHP的一种缓存方案静态化
    wordpress源码阅读
    最近在搞的东西有点多Gradle,Python,java,groove搞的脑子都要炸了,还得了流感。满满的负能量。
    编写一个自己的PHP框架(一)写在前面
    cookie,session机制
    __autoload和spl_autoload_register区别
    _initialize()和__construct()
    在往数据库中插入复杂的字符串时,单双引号混用经常会搞的很乱
  • 原文地址:https://www.cnblogs.com/hexinwei1/p/10254415.html
Copyright © 2020-2023  润新知