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与模式

 

posted @ 2019-11-26 20:50  楼兰胡杨  阅读(605)  评论(0编辑  收藏  举报