zoukankan      html  css  js  c++  java
  • 设计模式(一):单例模式

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

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

    适用场合:

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

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

    优点:

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

    二、实现方式

    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的枚举类型—-枚举的线程安全性及序列化问题

  • 相关阅读:
    Codeforces Round #649 (Div. 2) D. Ehab's Last Corollary
    Educational Codeforces Round 89 (Rated for Div. 2) E. Two Arrays
    Educational Codeforces Round 89 (Rated for Div. 2) D. Two Divisors
    Codeforces Round #647 (Div. 2) E. Johnny and Grandmaster
    Codeforces Round #647 (Div. 2) F. Johnny and Megan's Necklace
    Codeforces Round #648 (Div. 2) G. Secure Password
    Codeforces Round #646 (Div. 2) F. Rotating Substrings
    C++STL常见用法
    各类学习慕课(不定期更新
    高阶等差数列
  • 原文地址:https://www.cnblogs.com/hexinwei1/p/10254415.html
Copyright © 2011-2022 走看看