Skip to content

Latest commit

 

History

History
267 lines (225 loc) · 8.09 KB

设计模式-单例模式.md

File metadata and controls

267 lines (225 loc) · 8.09 KB

单例模式

1.简介

单例模式是开发中最常用的设计模式之一,它的作用是保证类只有一个实例对象,节约内存资源。由于全局只能有一个实例对象,因此需要满足一下几个条件:

  • 私有静态实例对象
  • 私有构造方法,外部无法调用构造方法进行实例化
  • 公有静态方法getInstance(),外部调用该方法获取实例对象

2.实现方式

  • 饿汉式
/**
 * @description: 饿汉式实现单例,在类初始化时就创建单例
 * 优点:线程安全(JVM只加载一次单例类)
 * 初始化速度快
 * 占用内存小
 * 缺点:单例创建时机无法控制
 * @author: zhukai
 * @date: 2019/2/11 10:54
 */
public class Singleton {

    // 加载该类时,单例就会自动被创建
    private static Singleton instance = new Singleton();

    /**
     * 私有构造函数
     * 禁止外部创建对象实例
     */
    private Singleton() {
    }

    /**
     * 通过调用静态方法获得创建的单例
     *
     * @return
     */
    public static Singleton getInstance() {
        return instance;
    }
}

在类初始化时就创建实例。由于JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化,在初始化期间,JVM会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化,因此可以避免多线程调用时引发线程安全的问题。

  • 懒汉式

1)基础实现

/**
 * @description: 懒汉式实现单例(基础实现)
 * 优点:按需加载单例
 * 节省资源
 * 缺点:线程不安全
 * @author: zhukai
 * @date: 2019/2/11 13:16
 */
public class Singleton {

    // 类加载时先不创建单例
    private static Singleton instance = null;

    /**
     * 私有构造函数
     * 禁止外部创建对象实例
     */
    private Singleton() {
    }

    /**
     * 通过调用静态方法获得创建的单例
     *
     * @return
     */
    public static Singleton getInstance() {
        // 先判断单例是否为空,以避免重复创建
        // 因对象初始化需要时间,当两个线程同时调用时,有可能instance == null均判断为true,造成实例的重复创建
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

调用getInstance()方法时才创建实例对象,缺点是线程不安全,可能会导致对象重复创建。

2)同步锁

/**
 * @description: 懒汉式实现单例(同步锁)
 * 优点:按需加载单例
 * 线程安全
 * 缺点:造成过多的同步开销
 * @author: zhukai
 * @date: 2019/2/11 13:19
 */
public class Singleton {

    // 类加载时先不创建单例
    private static Singleton instance = null;

    /**
     * 私有构造函数
     * 禁止外部创建对象实例
     */
    private Singleton() {
    }

    /**
     * 通过调用静态方法获得创建的单例
     * 加同步锁,防止多个线程同时调用,造成单例多次被创建
     * 每次访问都要进行线程同步,造成过多的同步开销,实际上只有第一次调用方法时需要同步,实例创建成功后不需要再进行同步
     *
     * @return
     */
    // 写法1
    public static synchronized Singleton getInstance() {
        // 先判断单例是否为空,以避免重复创建
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    // 写法2
//    public static Singleton getInstance() {
//        synchronized (Singleton.class) {
//            // 先判断单例是否为空,以避免重复创建
//            if (instance == null) {
//                instance = new Singleton();
//            }
//        }
//        return instance;
//    }
}

这种方式解决了线程不安全问题,但是会造成额外的开销,因为实际上不需要每次调用getInstance()方法都加锁,当对象创建完成之后就不需要同步锁了,直接返回单例即可。

3)同步锁+双重检查

/**
 * @description: 懒汉式实现单例(双重校验锁)
 * 优点:按需加载单例
 * 线程安全
 * 节省资源
 * 缺点:实现复杂
 * @author: zhukai
 * @date: 2019/2/11 13:37
 */
public class Singleton {

    /**
     * 类加载时先不创建单例
     * volatile关键字防止指令重排序,保证所有的写(write)操作都将发生在读(read)操作之前
     */
    private volatile static Singleton instance = null;

    /**
     * 私有构造函数
     * 禁止外部创建对象实例
     */
    private Singleton() {
    }

    /**
     * 通过调用静态方法获得创建的单例
     * 加同步锁,防止多个线程同时调用,造成单例多次被创建
     *
     * @return
     */
    public static Singleton getInstance() {
        // 先判断单例是否为空,以避免重复创建
        // 若单例已创建,则不需要进行同步加锁
        // 第一个if的作用:避免重复加锁
        if (instance == null) {
            synchronized (Singleton.class) {
                // 第二个if的作用:防止实例重复创建
                if (instance == null) {
                    // new Singleton()是一个非原子操作,编译器可能会重排序
                    // 构造函数可能在整个对象初始化完成前执行完毕,即赋值操作(只是在内存中开辟一片存储区域后直接返回内存的引用)在初始化对象前完成
                    // 因此线程B在线程A赋值完时判断instance有可能就不为null了
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

第一个if的作用是判断单例是否已创建,若已创建,则不需要进行同步加锁。第二个if的作用是防止对象的重复创建,当线程A获取到同步锁,但是还未完成单例的创建时,线程B执行getInstance()方法,进入到第一个if判断,等待线程A释放同步锁,这时如果不加第二个if判断,会导致线程B重复创建单例对象。

volatile关键字的作用是禁止指令重排序,因为 instance = new Singleton()是一个非原子性操作,可以分解为一下3个操作:

memory=allocate();        // 1、分配对象的内存空间
ctorInstance(memory);     // 2、初始化对象
instance = memory;          // 3、将instance指向刚分配的内存地址

2和3可能会发生指令的重排序,导致instance != null,但是此时单例还未完成初始化,当线程B执行getInstance()方法时,直接返回了这个还未初始化的对象。

  • 枚举
/**
 * @description: 利用枚举实现单例模式
 * 优点:线程安全
 * 实现简单
 * 解决反射调用私有构造器和序列化问题
 * 缺点:单例创建时机无法控制
 * @author: zhukai
 * @date: 2019/2/11 10:59
 */
public enum Singleton {
    // 每个枚举元素相当于一个实例
    INSTANCE;
}

枚举单例模式可以解决反射调用私有构造器和序列化问题,而且使用简单,推荐使用这种方式来创建单例。可以参考文章为什么要用枚举实现单例模式

  • 静态内部类
/**
 * @description: 静态内部类实现单例
 * 优点:按需加载单例
 * 线程安全
 * 节省资源
 * 实现简单
 * 缺点:
 * @author: zhukai
 * @date: 2019/2/11 13:43
 */
public class Singleton {

    // 创建静态内部类
    private static class InnerSingleton {
        // 在静态内部类里创建单例
        private static Singleton instance = new Singleton();
    }

    /**
     * 私有构造函数
     * 禁止外部创建对象实例
     */
    private Singleton() {
    }

    // 延迟加载、按需创建
    public static Singleton getInstance() {
        return InnerSingleton.instance;
    }
}

在静态内部类里创建单例,在装载该内部类时才会去创建单例,并且只初始化一次,保证了实例对象只会被创建一次,同时由于JVM的初始化锁,因此又是线程安全的。