zoukankan      html  css  js  c++  java
  • 设计模式—单例模式的六种写法

    一、定义

      确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例

    二、UML结构图

    三、场景

    • 需要频繁的实例化和销毁的对象;
    • 有状态的工具类对象
    • 频繁访问数据库或文件对象;
    • 确保某个类只有一个对象的场景,比如一个对象需要消耗的资源过多,访问io、数据库,需要提供全局配置的场景 

    四、几种单例模式

    1、饿汉式

       声明静态时已经初始化,在获取对象之前就初始化

      优点:获取对象的速度快,线程安全(因为虚拟机保证只会装载一次,在装载类的时候是不会发生并发的)

      缺点:耗内存(若类中有静态方法,在调用静态方法的时候类就会被加载,类加载的时候就完成了单例的初始化,拖慢速度)

    /**
     * 单例模式:饿汉式
     * 在类加载的时候就已经完成了初始化,所以类加载较慢,但获取对象的速度快
     * @author Administrator
     *
     */
    public class EagerSingleton {
    
        //静态私有成员,已初始化
        private static EagerSingleton instance = new EagerSingleton();
        
        
        //私有构造函数
        private EagerSingleton() {
            
        }
        
            
        //静态,不用同步(类加载时已初始化,不会有多线程的问题)
        public static EagerSingleton getInstance() {
            return instance;
        }
        
    }

    2、懒汉式

      synchronized同步锁: 多线程下保证单例对象唯一性

      优点:单例只有在使用时才被实例化,一定程度上节约了资源

      缺点:加入synchronized关键字,造成不必要的同步开销。不建议使用。

    /**
     * 单例模式:懒汉式(线程安全的懒汉式)
     * 比较懒,在类加载时,不创建实例,因此类加载速度快,但运行时获取对象的速度慢
     * @author Administrator
     *
     */
    public class LazySingleton {
    
        //静态私有成员,没有初始化
        private static LazySingleton instance = null;
        
        
        //私有构造函数
        private LazySingleton() {
            
        }
        
        
        //静态,同步,公开访问点
        public static synchronized LazySingleton getInstace() {
            if(instance == null) {
                instance = new LazySingleton();
            }
            return instance;
        }
    }

    3、Double Check Lock(DCL)实现单例(使用最多的单例实现之一)

      双重锁定体现在两次判空

      优点:既能保证线程安全,且单例对象初始化后调用getInstance不进行同步锁,资源利用率高

      缺点:第一次加载稍慢,由于Java内存模型一些原因偶尔会失败,在高并发环境下也有一定的缺陷,但概率很小。

    /**
     * 单例模式:双重锁定式
     * @author Administrator
     *
     */
    public class SingletonKerriganD {
    
        //这里加volatitle是为了避免DCL失效
        private volatile static SingletonKerriganD instance = null;
        
        
        //私有构造函数
        private SingletonKerriganD() {
            
        }
        
        
        /**
         * DCL对instance进行了两次null判断
         * 第一层判断主要是为了避免不必要的同步
         * 第二层判断则是为了在null的情况下创建实例
         * @return
         */
        public static SingletonKerriganD getInstance() {
            if(instance == null) {
                synchronized (SingletonKerriganD.class) {
                    if(instance == null) {
                        instance = new SingletonKerriganD();
                    }
                }
            }
            return instance;
        }
    }

      什么是DCL失效问题?

      假如线程A执行到instance = new SingletonKerriganD(),大致做了如下三件事:

    1. 给实例分配内存
    2. 调用构造函数,初始化成员字段
    3. 将instance 对象指向分配的内存空间(此时sInstance不是null)

      如果执行顺序是1-3-2,那多线程下,A线程先执行3,2还没执行的时候,此时instance!=null,这时候,B线程直接取走instance ,使用会出错,难以追踪。JDK1.5及之后的volatile 解决了DCL失效问题(双重锁定失效)

    4、静态内部类单例模式
       在调用 SingletonHolder.instance 的时候,才会对单例进行初始化

      优点:线程安全、保证单例对象唯一性,同时也延迟了单例的实例化

      缺点:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久代的对象。

    /**
     * 单例模式:静态内部类式
     * @author Administrator
     *
     */
    public class SingletonInner {
    
        //私有构造函数
        private SingletonInner() {
            
        }
        
        
        //在调用SingletonHolder.instance的时候,才会对单例进行初始化
        public static class SingletonHolder{
            private final static SingletonInner instance = new SingletonInner();
        }
        
        
        public static SingletonInner getInstance() {
            return SingletonHolder.instance;
        }
    }

      这种方式如何保证单例且线程安全?

      当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,内部类SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。 这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

      这种方式能否避免反射入侵?

      答案是:不能。网上很多介绍到静态内部类的单例模式的优点会提到“通过反射,是不能从外部类获取内部类的属性的。 所以这种形式,很好的避免了反射入侵”,这是错误的,反射是可以获取内部类的属性(想了解更多反射的知识请看 java反射全解),入侵单例模式根本不在话下

    【注意】:上述四种方法要杜绝在被反序列化时重新声明对象,需要加入如下方法:
    private Object readResolve() throws ObjectStreamException{
        return sInstance;
    }

      为什么呢?因为当JVM从内存中反序列化地"组装"一个新对象时,自动调用 readResolve方法来返回我们指定好的对象

    5、枚举单例

      优点:线程安全,防止被反序列化

      缺点:枚举相对耗内存

    public enum  SingletonEnum {
        instance;
        public void doThing(){
            
        }
    
    }

      只要 SingletonEnum.INSTANCE 即可获得所要实例。

      这种方式如何保证单例?

      首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。 也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次

      上面示例中生成的字节码文件对instance的描述如下:
    ...
    public static final eft.reflex.SingletonEnum instance;
        flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
    
    
    ...

      可以看出,会自动生成 ACC_STATIC, ACC_FINAL这两个修饰符

      枚举类型为什么是线程安全的?

      我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。

    6、使用容器实现单例模式
      在程序的初始化,将多个单例类型注入到一个统一管理的类中,使用时通过key来获取对应类型的对象,这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行操作。这种方式是利用了Map的key唯一性来保证单例。
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 单例模式:容器模式
     * @author Administrator
     *
     */
    public class SingletonManager {
    
        private static Map<String, Object> map = new HashMap<String, Object>();
        
        private SingletonManager() {
            
        }
        
        public static void registerService(String key, Object instance) {
            if(!map.containsKey(key)) {
                map.put(key, instance);
            }
        }
        
        public static Object getService(String key) {
            return map.get(key);
        }
    }

    五、总结

    所有单例模式需要处理得问题都是:

    1. 将构造函数私有化
    2. 通过静态方法获取一个唯一实例
    3. 保证线程安全
    4. 防止反序列化造成的新实例等。

    推荐使用:DCL、静态内部类、枚举

    单例模式优点

    1. 只有一个对象,内存开支少、性能好(当一个对象的产生需要比较多的资源,如读取配置、产生其他依赖对象时,可以通过应用启动时直接产生一个单例对象,让其永驻内存的方式解决)
    2. 避免对资源的多重占用(一个写文件操作,只有一个实例存在内存中,避免对同一个资源文件同时写操作)
    3. 在系统设置全局访问点,优化和共享资源访问(如:设计一个单例类,负责所有数据表的映射处理)

    单例模式缺点

    1. 一般没有接口,扩展难
    2. android中,单例对象持有Context容易内存泄露,此时需要注意传给单例对象的Context最好是Application Context
  • 相关阅读:
    第三周:Filter 拦截用户请求部分代码分析
    Story Of Web Background
    XML的前景
    XML的工作原理和过程
    第二周:XML的定义和用途
    企业级应用与互联网应用的区别
    第一周:JavaEE——课程目标
    Java 容器小结
    使用java显示所有操作系统环境变量
    迭代器和Interator的常见用法
  • 原文地址:https://www.cnblogs.com/javahr/p/14179546.html
Copyright © 2011-2022 走看看