单例:保证一个类只有一个实例,并提供一个访问该实例的全局访问点
1、饿汉式
存在的问题:如果单例类存在需要开辟大量存储空间的属性,会造成空间浪费
class HungryMan{
private HungryMan(){}
private static final HungryMan HUNGRY = new HungryMan();
public static HungryMan getInstance(){
return HUNGRY;
}
}
2、懒汉式
2.1、简单的懒汉式
存在的问题:多线程下极易破坏单例模式原则,创建出多个实例
class LazyMan{
private LazyMan(){}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if(null == lazyMan){
lazyMan = new LazyMan();
}
return lazyMan;
}
}
- 测试
// 修改单例构造让其输出线程名
private LazyMan(){
System.out.println(Thread.currentThread().getName() + "===========================");
}
// 测试代码
for(int i = 0; i < 10; i++){
new Thread(()-> LazyMan.getInstance()).start();
}
- 其中的一次破坏单例原则的测试结果如下:
2.2、上锁的懒汉式
- 双重检测锁模式,简称DCL
class LazyMan{
private LazyMan(){}
private static volatile LazyMan lazyMan;
public static LazyMan getInstance(){
if(null == lazyMan){
synchronized (LazyMan.class){
if(null == lazyMan){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
老版本JDK不用
volatile
关键字可能出现的问题:lazyMan = new LazyMan()
不是一个原子操作,它需要经历了以下3步
- 分配内存空间
- 执行构造,初始化对象
- 对象指向开辟的存储空间
在执行这3步的时候可能会发生一个指令重排的现象,正常执行1、2、3,实际却可能是1、3、2。
如果A线程执行的时候走了1、3、2,在A线程走完第2步时,线程B恰好来获取单例对象,此时这个对象已经开辟空间,此时
null == lazyMan
就是false
,B线程就获得了一个‘虚空’对象
(我随便取的名)。为了避免指令重排,需要在声明属性时加上
volatile
关键字【注:高版本(>1.4)的 Java 已在 JDK 内部解决了这个问题,所以高版本的 Java 不需要关注这个问题】
- 拓展:使用
volatile
关键字避免指令重排- volatile 本身包含“禁止指令重排序”的语义
- 关于 volatile 可参考 volatile详解-博客园
2.3、静态内部类实现单例
public class Holder {
private Holder(){}
public static Holder getInstance(){
return InnerClass.HOLDER;
}
public static class InnerClass{
private static final Holder HOLDER = new Holder();
}
}
单例并不安全,因为有反射的存在。枚举实现单例的安全性更高(不是绝对安全的,多个类加载器会破坏枚举实现的单例)
更多内容可参考 单例模式的五种实现方式及优缺点
2.4、枚举实现单例
class EnumSingleton{
private EnumSingleton(){}
public static EnumSingleton getInstance(){
return Singleton.INSTANCE.getInstance();
}
private static enum Singleton{
INSTANCE;
private EnumSingleton singleton;
//JVM会保证此方法绝对只调用一次
private Singleton(){
singleton = new EnumSingleton();
}
public EnumSingleton getInstance(){
return singleton;
}
}
}
- 枚举为什么会安全?
// 简单枚举单例
public enum EnumSingle {
INSTSNCE;
public EnumSingle getInstance(){
return INSTSNCE;
}
}
// 测试代码
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
EnumSingle enumSingle = declaredConstructor.newInstance(); // 是这句报错了!
EnumSingle instsnce2 = enumSingle.INSTSNCE;
- 测试结果:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
为什么枚举构造有两个参数?
编译后的class文件明明是无参构造,但是直接调无参却显示没有这个方法,经过专业反编译软件反编译后发现其实是有两个参数的