一文彻底搞懂单例模式
前言:何谓单例模式?
单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例。本文介绍3种常见懒汉式+2种常见饿汉式+1种静态内部类实现方式(懒汉式)+枚举实现(饿汉式)。
一、3种常见懒汉式
* 第一版(线程不安全)(懒汉式)
public class Singleton {
private Singleton() {} //私有构造函数
private static Singleton instance = null; //单例对象
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
以上例子,显然在并发的时候是线程不安全的,因为假如两个线程同时判断【instance==null】,那么都会走到new Singleton()这一步,然后拿到两个不同的对象引用。
* 第二版(线程安全,但有可能返回一个没有初始化完成的instance对象)(懒汉式)
public class Singleton {
private Singleton() {} //私有构造函数
private static Singleton instance = null; //单例对象
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) { //双重检测机制
synchronized (Singleton.class){ //同步锁
if (instance == null) { //双重检测机制
instance = new Singleton();
}
}
}
return instance;
}
}
像这样两次判空的机制叫做双重检测机制。有人可能会问,为啥不直接对getInstance方法加锁,这样就不用双重检测,只要一个检测了?其实这里是为了提高效率,如果不为null,就没有必要再去获取锁释放锁了。但是仍然有一个小问题。这里涉及到JVM指令重排。
java中简单的一句instance = new Singleton()会被编译器编译为如下指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
指令顺序有可能经过jvm和cpu的指令重排,导致2和3对调,这样的话可能出现对象不为null但是实际上还未完成初始化,这样的对象return回去,也会出现问题(其实当多个线程要共用一个对象时都应该注意这个问题)。为了避免这种情况,出现了以下第三版这种写法。
* 单例模式第三版(加个volatile修饰,防止重排序)(懒汉式)
public class Singleton {
private Singleton() {} //私有构造函数
private volatile static Singleton instance = null; //单例对象
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) { //双重检测机制
synchronized (Singleton.class){ //同步锁
if (instance == null) { //双重检测机制
instance = new Singleton();
}
}
}
return instance;
}
}
以上,第三版就解决了指令重排的问题。
二、2种常见饿汉式
以上三种都说懒汉式,另外,还有两种是饿汉式的(其实都是利用classloader在初始化的时候先加载static属性或static块的机制来实现的),如下:
饿汉1:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
这种基于classloder机制避免了多线程的同步问题,初始化的时候就给装载了。但是现在,没有懒加载的效果了。这是最简单的一种实现。
饿汉2(变种):
public class Singleton {
private static Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
和上面饿汉1差不多,都是在本类初始化即实例化instance。
三、1种静态内部类实现方式
那么除了以上实现方式,单例是否还有其他的实现方式呢?答案是肯定的:可以通过静态内部类实现单例模式。
用静态内部类实现单例模式:(懒汉式)
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
这里有几个需要注意的点:
1.从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE。
2.INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。(在调用的时候才会加载静态内部类)
四、用枚举实现单例模式
扩展:
1. 单例模式有个公共的问题,无法防止反射来重复构建对象(因为反射可以获取到类的私有构造方法),这个怎么避免呢?
答案是可以用枚举来实现单例(饿汉式),如下:
class Resource{
}
public enum SomeThing {
INSTANCE;
private Resource instance;
SomeThing() {
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}
上面的类Resource是我们要应用单例模式的资源,具体可以表现为网络连接,数据库连接,线程池等等。获取资源的方式很简单,只要 SomeThing.INSTANCE.getInstance() 即可获得所要实例。(其实就是利用了1.枚举类的构造函数只会执行一次;2.枚举类可以有效的避免通过反射来实例化;这两个特点来实现安全的单例)