什么是Singleton类?
Singleton类在整个应用程序中,只有一个实例,并且会提供getInstance()
方法去实例化Singleton类。在JDK中,有很多地方都使用了Singleton模式,例如,java.lang.Runtime
就是使用的单例模式,它通过getRuntime()
方法实例化Runtime类。
应该将哪些类设计为单例模式?
如果一个类,你想在整个应用程序中都能获得这个类,且该类只需存在一个实例,那么,你可以将这个类设计为单例模式。Runtime
类就是一个很好的例子,因为在整个java应用程序中,只有一个运行时环境(Runtime Environment),所以可以将Runtime
类设计为单例模式。
Singleton类的写法?
单例模式的实现方式主要有懒汉方式,饿汉方式,枚举等。下面会分别介绍各种实现方式。
使用枚举实现单例模式
public enum Singleton{
INSTANCE;
}
//可以通过`Singleton.INSTANCE`来访问
使用双重检查锁实现单例模式(懒汉方式)
懒汉方式是指全局的单例实例在第一次被使用时构建。饿汉式单例模式的实现的本质其实就是依赖类加载机制保证构造方法只会被执行一次。JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
public class Singleton{
private static volatile Singleton INSTANCE;
private Singleton(){}
public static Singleton getInstance(){
if(INSTANCE==null){
synchronized(Singleton.class){
if(INSTANCE==null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
为什么使用volatile
关键字修饰INSTANCE变量呢?
private static volatile Singleton INSTANCE;
我们都知道,volatile关键字有两个特性,一是可见性,也就是不管在哪个线程中,都可以读取最新的值。二是禁止指令重排序。
刚开始我以为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。
因为instance = new Singleton()
这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
使用静态工厂方法实现单例模式(饿汉方式)
饿汉方式是指全局的单例实例在类装载时构建。
public class Singleton{
private static final Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}