单例模式
设计模式是人们在开发中总结抽象的一些常用的程序设计范式。23种设计模式汇总如下图所示:
单例模式是这样一种设计模式,使得一个类只能有一个实例。单例模式主要有以下5种实现方法。
饿汉式
饿汉式单例模式在类加载过程中就实例化类对象,因此称为“饿汉”。由于Java类加载的过程本身是线程安全的,因此饿汉式的优点是无需额外同步即线程安全,并发效率高,但由于无法延迟加载,导致占用空间资源。
package singleton;
/**
* 饿汉式单例模式
* 优点:类加载过程是线程安全的,对象在类加载过程中实例化,因此也是线程安全的
* 缺点:没有使用对象时对象已经创建,消耗空间资源
* @author weiyx15
*
*/
public class SingletonHungry {
private static SingletonHungry instance = new SingletonHungry(); // 类初始化时,立即加载对象
private SingletonHungry() {} // 私有构造方法,外部不能new
public static SingletonHungry getInstance() {
return instance;
}
}
懒汉式
懒汉式单例模式在第一次调用getInstance方法时才实例化对象,因此属于延迟加载的实现方式。但getInstance方法需要用synchronized关键字同步,会降低并发效率。如果不用synchronized关键字修饰getInstance方法,在多线程的条件下会出现实例化多个对象的情况。
package singleton;
/**
* 懒汉式单例模式
* 优点:使用时再加载对象,空间资源利用效率高
* 缺点:{@code getInstance}方法需要同步,并发效率低
* @author weiyx15
*
*/
public class SingletonLazy {
private static SingletonLazy instance;
private SingletonLazy() {}
public static synchronized SingletonLazy getInstance() {
if (instance == null) // 如果未实例化,则先实例化
{
instance = new SingletonLazy(); // 调用getInstance方法后再实例化对象
}
return instance;
}
}
双重检测锁
双重检测锁式单例模式是懒汉式单例模式的改进。双重检测锁的getInstance方法有两次检测,第一次检测没有加锁,第二次检测对DoubleCheckingLock.class对象加锁。如果有多个线程同时通过了第一次检测,则第二次检测外面的锁将控制只有一个进程能进入第二次检测。由于大部分情况只需要不加锁的第一次检测就能返回,因此这种更加细粒度的加锁方式相比于直接在getInstance方法上加锁,提高了性能。
综上,双重检测锁是兼备高并发效率和延迟加载的优点。
另外一点要注意的是instance属性要加volatile关键字,这是因为Java对象的实例化过程不是原子操作,具体来说,实例化过程大致包含如下步骤:
- 开辟内存
- 对象的初始化(默认初始化和动态块)
- 执行对象的构造方法
- 将栈区的引用指向堆区的内存空间
其中2,3,4步可能发生重排,4可能在发生在2,3之前。因此,可能线程A获得了线程B尚未初始化完成的实例就返回了。volatile关键字其中一个作用就是禁止编译器的指令重排(编译器做指令重排的目的是单线程条件下重排指令可以在不改变程序执行结果的条件下优化运行速度)。
但是在Java 5之前,由于编译器对volatile关键字的实现并不能完全屏蔽编译器的指令重排,因此双重检测锁的单例模式在多线程环境下偶尔会出现上述问题。Java 5及其以后版本,应该没有这个问题了。
package singleton;
/**
* 双重检测锁
* 优点:在懒汉式的基础上把同步块从方法下放到{@code if}语句
* 缺点:Java 5之前由于编译器对volatile关键字的实现,偶尔会出问题,不建议使用
* @author weiyx15
*
*/
public class DoubleCheckingLock {
private volatile static DoubleCheckingLock instance;
private DoubleCheckingLock() {}
public static DoubleCheckingLock getInstance() {
if (instance == null)
{
synchronized (DoubleCheckingLock.class) {
if (instance == null)
{
instance = new DoubleCheckingLock();
}
}
}
return instance;
}
}
静态内部类
静态内部类式单例模式利用静态内部类的类加载过程是线程安全的特点,在静态内部类的属性中实例化外部类的单例。由于静态内部类的属性单例的实例化发生在静态内部类的属性调用时,因此静态内部类式单例模式兼备高并发效率和延迟加载。
package singleton;
/**
* 静态内部类式单例模式
* 优点:静态内部类的加载也是天然线程安全的,且不会在外部类加载时加载,
* 因此静态内部类式单例模式兼备并发高效率和延迟加载节省空间资源的优势
* @author weiyx15
*
*/
public class SingletonStaticInnerClass {
private static class SingletonClassInstance {
private static final SingletonStaticInnerClass instance = new SingletonStaticInnerClass();
}
private SingletonStaticInnerClass() {}
public static SingletonStaticInnerClass getInstance() {
return SingletonClassInstance.instance;
}
}
枚举类
枚举式单例模式利用Java的枚举类天然的单例特性构造单例。优点是并发效率高,缺点是无法延迟加载。
package singleton;
/**
* 枚举式单例模式
* 优点:枚举类由JVM保证是单例,且无法被反射机制破解(其他4种单例模式的实现都可以被反射机制调用私有构造方法)
* 缺点:类加载时就实例化,占用空间资源
* @author weiyx15
*
*/
public enum SingletonEnum {
INSTANCE;
/**
* 添加自定义的对于{@code INSTANCE}的操作
*/
public void SingletonOperation() {
}
}