我的公众号:不颓废的程序员
一、什么是单例模式?
一句话概括: 保证一个类有且仅有一个实例,并提供该实例的全局访问方法
所以为了达到单例的效果,一般要包含三个要素
-
私有的静态的实例对象 private static Singleton instance
-
私有的构造方法 private Singleton(){} ,使得在类的外部无法通过new的方式来创建对象
-
公有的、静态的、访问该实例对象的方法 public static Singleton getInstance(){}
一般单例模式分为:饿汉式单例和懒汉式单例
举个小栗子:我有两条🐟,一条给饿汉,一条给懒汉
饿汉是先把🐟烤熟,不管吃不吃,先烤熟再说,等到饿的时候就直接拿来吃(相当于类加载的时候,就创建实例对象,使用的时候就直接拿来使用,不用再去创建对象)
而懒汉则是非常懒惰,他是等到自己饿的时候才去烤🐟,然后再拿来吃(相当于类加载的时候,不去创建实例对象,等到需要使用实例对象时,才去创建对象)
通过这个例子,大概了解了一下饿汉式单例和懒汉式单例,接下来使用代码来正式讲解单例模式吧
二、饿汉式单例
public class Singleton {
//私有构造方法使得:在类的外部无法通过new的方式来创建对象
private Singleton(){}
//在类加载的时候就创建实例对象了
private static Singleton instance = new Singleton();
//提供一个入口,也就是提供该实例的全局访问方法,使得外界能访问到该实例
public static Singleton getInstance(){
return instance;
}
public static void main(String[] args) {
System.out.println(Singleton.getInstance());
}
}
三、懒汉式单例
第一版
public class Singleton{
//私有构造方法使得:在类的外部无法通过new的方式来创建对象
private Singleton(){}
//初始值设置为null
private static Singleton1 instance = null;
public static Singleton getInstance(){
//线程不安全,因为当两个线程(A和B)同时访问if这段代码时,都返回true,就会创建两个不同的对象
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
在多线程下,可能会导致创建多个实例对象,因为当多条线程同时访问 if(instance == null) 这行代码时,都会返回true,就会导致多条线程访问到 instance = new Singleton(); 从而创建多个实例对象
第二版
public class Singleton {
//私有构造方法使得:在类的外部无法通过new的方式来创建对象
private Singleton(){}
//初始值设置为null
private static Singleton instance = null;
public static Singleton getInstance(){
//双重检测机制(DCL,Double Check Lock)
if(instance == null){ //第一次判空操作
//使用同步锁
synchronized(Singleton.class){
if(instance == null){ //第二次判空操作
instance = new Singleton();
}
}
}
return instance;
}
}
使用双重检测机制,在第一版的基础上改进了,但也不是绝对的线程安全,还会存在指令重排的问题
那指令重排又是什么意思呢?
比如在Java中的 instance = new Singleton();,在我们看来,它只是一句简单的代码,可是它会被编译器编译成如下JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
但是这些指令顺序有可能会经过JVM和CPU的优化,重排成不同的顺序:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
当线程A执行完1,3,时,instance对象还未完成初始化,但此时instance已经不再指向null。如果线程B此时抢占到CPU资源,执行if(instance == null)的结果会是false,这样就会出现一个问题:返回一个没有初始化完成的instance对象。
在这里,我看到有些人会有疑问?
截取某博客的评论:虽然书里也是说要加volatile关键字,但是还是和你有同样的疑问。new Instance()确实非原子操作,但是synchronized不正是为了保证非原子操作的线程安全才用的么?同一时刻,能进到synchronized代码块里的不应该只有一个线程么,怎么会有线程A没执行完new Instance()指令,线程B就进到了synchronized代码块里的情况呢?
这里说一下,线程B是在执行第一个 if(instance == null) 的时候,得到结果为false,于是就直接执行了return instance; 返回了一个没有初始化完成的instance对象,而不是进到 synchronized 代码块里,去执行第二个 if(instance == null) ,所以说线程B没有进入 synchronized 代码块里,线程B是在第一次判空操作时就已经返回 instance 了。
第三版
就是将第二版的 private static Singleton instance = null;
改为 private volatile static Singleton instance = null;
也就是加了volatile关键字修饰
volatile关键字在此不展开讲述,改天专门写篇文章介绍一下volatile,在这里简单说说它的几个特性吧
- 保证变量的可见性
- 不保证变量的原子性
- 禁止指令重排
第三版中添加了 volatile ,就可以避免指令重排的问题了,这时候线程是安全的了,可是使用Java的反射,照样可以创建多个实例对象,等会我们再聊聊反射
第四版
这一版不是对第三版的改进,只是提供另一种方法去实现懒汉式单例,这种方法是使用静态内部类去创建实例对象
public class Singleton {
//静态内部类
private static class LazyHolder{
private static final Singleton instance = new Singleton();
}
//私有构造方法
private Singleton(){}
public static Singleton getInstance(){
return LazyHolder.instance;
}
}
从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象instance , instance对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法的时候,因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。第四版和第三版一样会被反射破坏单例。
这里简单用几行代码演示一下 :利用反射来破坏单例
//利用反射获取类的构造方法
Constructor<Singleton> d = Singleton.class.getDeclaredConstructor();
//设置构造方法为可访问的
d.setAccessible(true);
//创建对象
Singleton singleton1 = (Singleton)d.newInstance();
Singleton singleton2 = (Singleton)d.newInstance();
//判断两个利用反射创建的对象是否相等
//结果为false,不相等
System.out.println(singleton1.equals(singleton2));
第五版
使用枚举来实现单例,可以阻止反射去构造对象
public enum Singleton {
INSTANCE;
public Singleton getInstance(){
return INSTANCE;
}
}
使用枚举实现单例之后,我们再来测试一下,利用反射创建对象会出现什么情况?
1.执行下面的代码
//利用反射获取类的构造方法
Constructor<Singleton> d =Singleton.class.getDeclaredConstructor(String.class,int.class);
//设置构造方法为可访问的
d2.setAccessible(true);
//创建对象
Singleton instance = d.newInstance();
我们会发现代码会报错
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.hgq.singleton.Test.main(Singleton.java:39)
我们点击Constructor.java:417,进入到Constructor中的newInstance方法中,看到这两行代码
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
newInstance方法的完整代码如下:
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
//从这里可以看到,如果使用了枚举,当我们使用反射来创建对象时,会抛出异常
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
2.如果执行这段代码
//利用反射获取类的构造方法
Constructor<Singleton> d =Singleton.class.getDeclaredConstructor();
//设置构造方法为可访问的
d2.setAccessible(true);
//创建对象
Singleton instance = d.newInstance();
我们发现代码报了另一个错 java.lang.NoSuchMethodException
Exception in thread "main" java.lang.NoSuchMethodException: com.hgq.singleton.Singleton_enum.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.hgq.singleton.Test.main(Singleton.java:34)
到这里,我们知道了,如果是使用枚举来创建单例,我们使用反射是无法创建新的对象的,也就是使用反射无法破坏单例,所以使用枚举实现单例也是一个不错的选择哦!