【java设计模式】-04单例模式

单例模式

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

类型: 创建类模式

类图:

image

单例模式特点

1、单例类只能有一个实例。

2、单例类必须自己创建自己的唯一实例。

3、单例类必须给所有其他对象提供这一实例。

单例模式应该是23种设计模式中最简单的一种模式了。它有以下几个要素:

  • 私有的构造方法
  • 指向自己实例的私有静态引用
  • 以自己实例为返回值的静态的公有的方法

单例模式的实现方式

单例模式根据实例化对象时机的不同分为两种:

扫描二维码关注公众号,回复: 2452189 查看本文章

一种是饿汉式单例,一种是懒汉式单例。

饿汉式单例在单例类被加载时候,就实例化一个对象交给自己的引用;

懒汉式在调用取得实例方法的时候才会实例化对象。

1)饿汉式单例

是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
/**
 * 饿汉式单例
 * <p>
 * 饿汉式是典型的空间换时间,当类装载的时候就会创建类的实例,
 * 不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断,节省了运行时间。
 *
 * @author kaifeng
 */
public class HungrySingleton {
    private static HungrySingleton hungrySingleton = new HungrySingleton();

    /**
     * 私有构造函数
     */
    private HungrySingleton() {
    }

    /**
     * 获取实例对象
     */
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

2)懒汉式单例

是否 Lazy 初始化:是
是否多线程安全:是
实现难度:易
描述:这种单例是典型的时间换空间,只有使用的时候才会实例化,但是每次获取实例的时候都会进行判断,是否已经实例,花费了一些判断时间,如果一直没有使用就不会实例化,节省了内存空间
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
/**
 * 懒汉式单例
 * <p>
 * 这种单例是典型的时间换空间,只有使用的时候才会实例化,但是每次获取实例的时候都会进行判断,
 * 是否已经实例,花费了一些判断时间,如果一直没有使用就不会实例化,节省了内存空间
 *
 * @author kaifeng
 */
public class LazySingleton {
    private static LazySingleton instance = null;

    /**
     * 私有构造函数
     */
    private LazySingleton() {
    }

    /**
     * 静态工厂方法,实例化对象
     */
    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

3)双检锁/双重校验锁(DCL,即 double-checked locking)

是否 Lazy 初始化:是
是否多线程安全:是
实现难度:较复杂
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
/**
 * 双检锁/双重校验锁(DCL,即 double-checked locking)
 *
 * @author kaifeng
 */
public class DCLSingleton {

    private volatile static DCLSingleton instance;

    private DCLSingleton() {
    }

    public static DCLSingleton getInstance() {
        //第一次检查实例是否存在,如果不存在才进入下面的同步块,否则直接返回实例
        if (instance == null) {
            //同步代码块,线程安全的创建实例
            synchronized (DCLSingleton.class) {
                //第二次检查实例是否存在,如果不存在才真正的创建实例
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

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

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

4)Initialization-on-demand holder idiom

是否 Lazy 初始化:是
是否多线程安全:是
实现难度:一般
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
/**
 * 在这个类级内部类里面去创建对象实例,只要不使用到这个类级内部类,
 * 那就不会创建对象实例,从而同时实现延迟加载和线程安全。
 *
 * @author kaifeng
 */
public class Singleton {
    private Singleton() {
    }

    /**
     * 类级的内部类,就是静态的成员式内部类,该内部类的实例与外部类的实例
     * 没有绑定关系,而且只有被调用时才会装载,从而实现了延迟加载。
     */
    private static class SingletonHolder {
        /**
         * 静态初始化器,由JVM来保证线程安全
         */
        private static Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

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

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

5)枚举

是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
/**
 * @author kaifeng
 */
public enum EnumSingleton {
    /**
     * 定义一个枚举的元素,它就代表了Singleton的一个实例
     */
    instance;

    /**
     * 单例可以有自己的操作
     */
    public void singletonCustom() {
        //自定义操作
    }
}

单例模式的优点:

  • 在内存中只有一个对象,节省内存空间。
  • 避免频繁的创建销毁对象,可以提高性能。
  • 避免对共享资源的多重占用。
  • 可以全局访问。

适用场景:

  • 需要频繁实例化然后销毁的对象。
  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  • 有状态的工具类对象。
  • 频繁访问数据库或文件的对象。
  • 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器

单例模式注意事项:

  • 只能使用单例类提供的方法得到单例对象,不要使用反射,否则将会实例化一个新对象。
  • 不要做断开单例类对象与类中静态引用的危险操作。
  • 多线程使用单例使用共享资源时,注意线程安全问题。

猜你喜欢

转载自blog.csdn.net/u010647035/article/details/81269461