一、单例模式简介
单例模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
我们用静态类直接调用方法跟单例模式差不多,区别就是静态类直接调用是一种基于对象的思想,而单例模式则是一种面向对象的思想
概括意思就是:
- 单例类只能有一个实例
- 必须自己创建自己的唯一实例
- 必须给其他对象提供这一实例
二、单例模式代码示例
单例模式的编写其实就是遵循三点:
- 将构造函数私有化
- 在类的内部创建实例
- 提供获取唯一实例的方法
1.最基础的饿汉式
public class Singleton {
//私有构造函数,限制用户主动创建实例
private Singleton() {}
private static Singleton instance = null; //单例对象
//静态工厂方法,用于获取Singleton实例
public static Singleton getInstance() {
//如果单例的初始值为null,则还为构建,构建单例对象并返回
//否则直接返回
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这段代码不是线程安全的:
我们假设Singleton类刚被初始化,此时instance对象为空,两个线程同时访问getInstance()
方法。此时两个线程经过判断都是instance == null
,两个线程同时去执行 instance = new Singleton();
,这样instance就被构建了两次,不符合我们说的单例类只有一个实例。
2.直接加synchronized的写法
既然说了getInstance()
方法会导致两个线程同时去创建,那我们直接加个synchronized
关键字就好了:
public class Singleton {
//私有构造函数,不可通过new来创建
private Singleton() {}
private static Singleton instance = null; //单例对象
//静态工厂方法,用于获取Singleton实例
public static synchronized Singleton getInstance() {
//如果单例的初始值为null,则还为构建,构建单例对象并返回
//否则直接返回
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
显而易见,这个写法效率比较低,于是有了下面的写法
3.双重校验锁写法
相对于直接给getInstance()
加synchronized
,这种写法在多线程的情况下也能保证高性能
public class Singleton {
//私有构造函数,不可通过new来创建
private Singleton() {}
//注意,此处需要加volatile来确保线程安全
private static volatile Singleton instance;
//静态工厂方法,用于获取Singleton实例
public static Singleton getInstance() {
//当singleton为空时就创建,否则返回
if (instance== null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
注意
private static volatile Singleton instance;
这一行,给instance
加了一个volatile
才能保证安全,如果不加其实仍然有可能创建两个对象。
因为 instance = new Singleton();
这个操作并不是原子操作,它分为以下三个操作:
1.分配内存空间
2.初始化对象
3.设置instance变量指向刚才分配的内存地址
由于指令重排序的存在,经过JVM和CPU的优化,有可能排成下面的顺序:
1.分配内存空间
3.设置instance变量指向刚才分配的内存地址
2.初始化对象
当线程A执行完1和3后,instance对象还未完成初始化,但此时已经不再指向null。此时线程B抢占到CPU资源,执行if (instance == null)
的结果返回false,从而返回一个未初始化完全的对象。
4.饿汉式单例
一旦创建Singleton就实例化,所以天生线程安全
public class HungrySingleton {
private HungrySingleton() {}
private static HungrySingleton hungrySingleton = new HungrySingleton();
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
5.静态内部类的写法
效率最高,实现简单。利用了类加载机制来保证初始化instance时只有一个线程。
跟饿汉模式不同的是,饿汉模式只要Singleton被装载了那么instance就被初始化。而静态内部类的方式是懒加载的,Singleton被装载了,instance不一定被初始化(只有通过调用getInstance时,才会显示的装载Inner类来实例化instance)。如果实例化instance很耗费资源,所以想让它延迟加载,或者是不希望在Singleton加载时就实例化,因为不确定Singleton是否在其他地方被主动使用而被加载,这样采用这个方式就很合理.
public class Singleton {
private static class Inner {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return Singleton.INSTANCE;
}
}
6.使用ThreadLocal实现的线程安全的单例
使用ThreadLocal实现单例模式,ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。
对于多线程资源共享的问题,同步机制采用了以时间换空间的方式,而ThreadLocal采用了一空间换时间的方式。
前者只提供一个变量让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
public class Singleton {
private static final ThreadLocal<Singleton> INSTANCE = new ThreadLocal<>() {
@Override
protected Singleton initialValue() {
return new Singleton();
}
};
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE.get();
}
}
7.使用CAS的单例
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
private Singleton() {}
public static Singleton getInstance() {
for (; ; ) {
Singleton current = INSTANCE.get();
if (current != null) {
return current;
}
current = new Singleton();
//通过CAS,如果是空则设置为current
if (INSTANCE.compareAndSet(null, current)) {
return current;
}
}
}
}
8.一行代码的单例(枚举)
public enum Singleton {
INSTANCE;
}
枚举类可以有效的防止多次实例化,而且JVM会阻止反射获取枚举类的私有构造方法。这种方式的缺点是跟饿汉式一样没有使用懒加载,其单例对象是在枚举类被加载的时候进行初始化的。
三、小结
在示例8中我们提到了反射机制可能会破坏单例并创建多个对象,我们对上面的8种单例模式进行分别反射测试,并总结其特性,结果如下:
单例模式 | 是否线程安全 | 是否懒加载 | 是否防止反射构建 |
---|---|---|---|
懒汉式单例 | 否 | 是 | 否 |
直接加synchronized | 是 | 是 | 否 |
双重锁校验 | 是 | 是 | 否 |
饿汉式单例 | 是 | 否 | 否 |
静态内部类 | 是 | 是 | 否 |
使用ThreadLocal | 是 | 是 | 否 |
CAS实现 | 是 | 是 | 否 |
枚举类实现 | 是 | 否 | 是 |
测试所用到的代码如下:
public class Test {
public static void main(String[] args) throws Exception {
//获取构造器
Constructor constructor = Singleton.class.getDeclaredConstructor();
//设置为可访问
constructor.setAccessible(true);
//构建两个对象
Singleton singleton1 = (Singleton) constructor.newInstance();
Singleton singleton2 = (Singleton) constructor.newInstance();
//验证是否相同
System.out.println(singleton1.equals(singleton2));
}
}