单例模式是开发中最常用的设计模式之一,它的作用是保证类只有一个实例对象,节约内存资源。由于全局只能有一个实例对象,因此需要满足一下几个条件:
- 私有静态实例对象
- 私有构造方法,外部无法调用构造方法进行实例化
- 公有静态方法
getInstance()
,外部调用该方法获取实例对象
- 饿汉式
/**
* @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的初始化锁,因此又是线程安全的。