zoukankan      html  css  js  c++  java
  • Java与设计模式之单例模式(上)六种实现方式

     

           阎宏博士在《JAVA与模式》中是这样描述单例模式的:作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。     

          单例模式可以说是最常使用的设计模式了,它的作用是确保某个类只有一个实例,自行实例化并向整个系统提供这个实例。在实际应用中,线程池、缓存、日志对象和对话框对象等常被设计成单例。

          应聘的时候,面试官经常会问,怎么保证某个类在程序运行过程中只有一个对象存在?此问题实际就是面试官在了解面试者对单例模式的了解。

          总之,选择单例模式就是为了走出多头管理、政出多门的怪圈。本文介绍单例模式的六种实现方式,在《Java与设计模式之单例模式(下)》中分析如何使用Java实现线程安全的单例模式。

    单例模式的结构

      单例模式的特点:

    • 单例类只能有一个实例;
    • 单例类必须自己创建自己的唯一实例;
    • 单例类必须给所有其他对象提供这一实例。

         单例模式有多种写法,各有利弊,现在我们来看看各种模式写法。

     1. 饿汉式单例

         结构图:

      

          实现代码:

    /**
     * 饿汉式单例,线程安全
     */
    public class EagerSingleton {
        // 自行实例化,并用 static 和 final 修饰
       private static final EagerSingleton instance = new EgerSingleton();
        // 私有化构造方法
       private EagerSingleton() {
        }
        // 对外发布,并用static修饰。静态公有工厂方法,返回唯一实例
        public static EagerSingleton getInstance() {
            return instance;
        }
    }

           Singleton通过将构造方法限定为private避免了类在外部被实例化。在同一个虚拟机范围内,想调用其中的方法getInstance就必须使用static修饰,这样就可以通过类名.方法名访问EagerSingleton的唯一实例了;又因为静态方法里只能用静态成员,所以instance必须static化。

          成员变量instance前可以不加final,因为静态方法只在编译期间执行一次初始化,也就是只会有一个对象。

    饿汉式单例是线程安全的。当类被加载时,静态变量instance会被初始化;此时类的私有构造函数会被调用,从而单例类的唯一实例将被创建,以后不再改变,无需关注多线程问题,写法简单明了。

          饿汉式是典型的空间换时间,当类装载的时候就会创建类的实例,然后每次调用的时候,就不需要再判断,节省了运行时间。如果是一个工厂模式、缓存了很多实例、那么就得考虑效率问题,因为这个类一加载则把所有实例不管用不用都创建。 

    2. 懒汉式单例

            将上面的饿汉式改为懒汉式: 
    /**
     * 懒汉式单例模式(线程安全)
     */
    public class LazySingleton {
    
        private static volatile LazySingleton instance;
    
        private LazySingleton() {
        }
    
        public static synchronized LazySingleton getInstance() {
            if (null == instance) {
                instance = new LazySingleton();
            }
            return instance;
        }
    }

           之所以叫做懒汉式单例模式,主要是因为此种方法lazy loading,简单说就是什么时候调用,什么时候创建。它是典型的时间换空间。如果从源码中删除关键字synchronized,则是线程不安全的懒汉式单例模式。并发其实是一种特殊情况,同步锁锁的是对象,每次取对象的时候都加锁会浪费资源,因此,这种方式写出来的结构效率很低,不推荐。

     3. 双重检查加锁单例

          使用“双重检查加锁”的方式来实现单例可以既实现线程安全,又使性能不受很大的影响。那么什么是“双重检查加锁”机制呢?

          所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查;进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

           “双重检查加锁”机制的实现会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

           注意:在java1.4及以前版本中,很多JVM对于volatile关键字的实现的问题,会导致“双重检查加锁”的失败,因此“双重检查加锁”机制只只能用在java5及以上的版本。

           因为上面的懒汉式单例模式每次请求时都会添加同步锁,很浪费性能,所以在加锁之前先进行非空校验。

    /**
     * 双重检查加锁单例
     */
    public class DoubleCheckSingleton {
        private static volatile DoubleCheckSingleton singleton = null;
        private DoubleCheckSingleton() {
        }
    
        public static DoubleCheckSingleton getSingleton() {
            if (null == singleton) {
                synchronized (DoubleCheckSingleton.class) {
                    if (null == singleton) {
                        singleton = new DoubleCheckSingleton();
                    }
                }
            }
            return singleton;
        }
    }

           先分析去掉volatile关键字的双重检查加锁。看似简单的一段赋值语句:

    1 instance = new Singleton(); // 其实JVM内部已经转换为多条指令:
    2 memory = allocate(); //1:分配对象的内存空间
    3 ctorInstance(memory); //2:初始化对象
    4 instance = memory; //3:设置instance指向刚分配的内存地址

         但是经过重排序后如下:

    1 memory = allocate(); //1:分配对象的内存空间
    2 instance = memory; //3:设置instance指向刚分配的内存地址,此时对象还没被初始化
    3 ctorInstance(memory); //2:初始化对象

           可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面,在线程A初始化完成这段内存之前,线程B虽然进不去同步代码块,但是在同步代码块之前的判断就会发现instance不为空,此时线程B获得instance对象进行使用就可能发生错误。

           Volatile关键字的作用是禁止进行指令的重排序。相比于去掉volatile关键字的双重校验锁, 加上之后保证了线程安全,但是,性能降低了。这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。它只是第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。

           提示:由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。

    4. 静态内部类单例

          这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了线程安全和类似懒汉式单例模式的延迟加载。

     

           什么是类级内部类?

           简单点说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。 

           类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。

          类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。

          类级内部类相当于其外部类的成员,只有在第一次被使用的时候才被会装载。

          多线程缺省同步锁的知识

          大家都知道,在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地为您执行了同步,这些情况下就不用自己再来进行同步控制了。这些情况包括:

    1. 由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
    2. 访问final字段时
    3. 在创建线程之前创建对象时
    4. 线程可以看见它将要处理的对象时
    /**
     * 静态内部类
     */
    public class InnerStaticSingleton {
    
        // 私有的静态内部类
        private static class Holder {
            private static InnerStaticSingleton instance = new InnerStaticSingleton();
        }
    
        private InnerStaticSingleton() {
            System.out.println("Singleton has been loaded.");
        }
    
        public static InnerStaticSingleton getInstance() {
            return Holder.instance;
        }
    }

           当getInstance方法第一次被调用的时候,它第一次读取Holder.instance,导致Holder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建InnerStaticSingleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。

           这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。 

    5. 枚举单例

    /**
     * 枚举单例
     */
    public enum EnumSingleton {
        INSTANCE;
        private String name;
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public void otherMethod(){
            System.out.println("Do something.");
        }
    }

           这种方式是《Effective Java》作者Josh Bloch 提倡的方式。它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,推荐!是更简洁、高效、安全的实现单例的方式。

           测试用例:

       public static void main(String[] args) {
            EnumSingleton.INSTANCE.otherMethod();
            System.out.println("-----------------------");
        }

     

    6. 登记式单例

           登记式单例实际上维护的是一组单例类的实例,将这些实例存储到一个Map(登记簿)中,对于已经登记过的单例,则从工厂直接返回,对于没有登记的,则先登记,而后返回。 

    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 登记式单例类.<br/>
     * 类似Spring里面的方法,将类名注册,下次从里面直接获取。
     * @author east7
     */
    public class RegisterSingleton {
        //使用一个map来当注册表
        private static Map<String, RegisterSingleton> map = new HashMap<String, RegisterSingleton>();
    
        //静态块,在类被加载时自动执行,把 RegisterSingleton 自己也纳入容器管理
        static {
            RegisterSingleton single = new RegisterSingleton();
            map.put(single.getClass().getName(), single);
        }
    
        //受保护的默认构造函数,如果为继承关系,则可以调用,克服了单例类不能为继承的缺点
        protected RegisterSingleton() {
        }
    
        //静态工厂方法,返回此类惟一的实例
        public static RegisterSingleton getInstance(String name) {
            if (name == null) {
                name = RegisterSingleton.class.getName();
                System.out.println("name == null" + "--->name=" + name);
            }
            if (map.get(name) == null) {
                try {
                    map.put(name, (RegisterSingleton) Class.forName(name).newInstance());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return map.get(name);
        }
    
        //一个示意性的商业方法
        public String about() {
            return "Hello, I am RegisterSingleton.";
        }
    
        public static void main(String[] args) {
            RegisterSingleton single3 = RegisterSingle-ton.getInstance(null);
            System.out.println(single3.about());
        }
    }

    Reference

    1. https://www.cnblogs.com/alter888/p/9163612.html
    2. https://blog.51cto.com/13477015/2177185
    3. https://www.cnblogs.com/java-my-life/archive/2012/03/31/2425631.html
    4. https://www.cnblogs.com/twoheads/p/9723543.html
    5. JAVA与模式
  • 相关阅读:
    Atitit.ati orm的设计and架构总结 适用于java c# php版
    Atitit.ati dwr的原理and设计 attilax 总结 java php 版本
    Atitit.ati dwr的原理and设计 attilax 总结 java php 版本
    Atitit. 软件设计 模式 变量 方法 命名最佳实践 vp820 attilax总结命名表大全
    Atitit. 软件设计 模式 变量 方法 命名最佳实践 vp820 attilax总结命名表大全
    Atitit 插件机制原理与设计微内核 c# java 的实现attilax总结
    Atitit 插件机制原理与设计微内核 c# java 的实现attilax总结
    atitit.基于  Commons CLI 的命令行原理与 开发
    atitit.基于  Commons CLI 的命令行原理与 开发
    atitit.js 与c# java交互html5化的原理与总结.doc
  • 原文地址:https://www.cnblogs.com/east7/p/11922065.html
Copyright © 2011-2022 走看看