网课封校,大二就开始长毛了,闲着看这看那,然后看到讲解单例模式的视频。看完之后理解更好了些(可能是我比较菜吧,确实造诣不高)仔细的看完了一遍之后想着做个总结
单例模式最基本的就是构造器私有了
饿汉式单例
顾名思义,有一个汉子,他很饿,所以他一上来就想着吃饱,甭管你需不需要他干活,他都得先吃饱,然后在那呆着。
public class Single {
private Single(){
}
private static final Single SINGLE = new Single();
public static Single getInstance(){
return SINGLE;
}
}
要使唤他的时候通过 类名.getInstance() 就可以获取到该类的单例了
类加载初期就把单例创建好了, 而且是线程安全的(只在类加载时才会初始化,以后都不会),有一定的弊端,会造成内存的浪费,但微乎其微。所以这种设计模式在大多数场景中很常用且很简单。
留给饿汉式的篇幅暂且这么多,然后我们来看懒汉式单例
懒汉式单例
懒(是我本人)汉式,他太懒了 ,只有你喊他干活的时候他才干活。
public class Single {
private Single(){
}
private static Single SINGLE;
public static Single getInstance(){
if (SINGLE == null){
SINGLE = new Single();
}
return SINGLE;
}
}
由于这个懒汉不严谨,所以在多线程情况下会出现问题
public class Demo {
@Test
public void test() {
for ( int i = 0; i < 10; i++ ) {
new Thread(Single::getInstance).start();
}
}
}
class Single {
private Single(){
System.out.println(Thread.currentThread().getName() + "ok");
}
private static Single SINGLE;
public static Single getInstance(){
if (SINGLE == null){
SINGLE = new Single();
}
return SINGLE;
}
}
// 输出
Thread-1 ok
Thread-0 ok
非常明显,只是测试了一下,就已经创建了两个实例了
所以为了解决这个情况,出现了双重检测锁模式
public class Single {
private Single(){
}
private static Single SINGLE;
public static Single getInstance(){
// 判断一下SINGLE是否已经创建了,如果不做判断那么每次调用getInstance()都会创建一个SINGLE对象
if (SINGLE == null){
// 使用线程锁,当一个线程占用了资源时其他线程需要等待
synchronized (Single.class){
// 如果不做判断,当前线程即使创造了实例,下一个线程也不知道,就会继续创建一个实例
if (SINGLE == null){
SINGLE = new Single();
}
}
}
// 如果SINGLE已经创建了,那么就直接返回
return SINGLE;
}
}
那么问题解决了吗?奥夫考斯闹特!
指令重排
这种写法看似没什么问题,但是它是线程不安全的,多线程情况下,系统创建对象是分为三步走的
- 分配内存空间
- 初始化对象
- 将对象指向分配的空间
- 在正常情况下是顺序执行,但也可能发生指令重排的现象,比如执行132。
-
- 1、先分配了一块内存空间
- 2、把一个空对象指向这块空间
- 3、然后再初始化对象
- 当只有一条线程A时,这种情况是没问题的
- 当出现第二条线程B时,如果也是这样的132执行,执行到3的时候会认为单例对象已经创建完了,就会直接return该对象,此时该单例对象还没有完成创建
所以我们要给SINGLE实例加上 volatile 关键字
private static volatile Single SINGLE; // 避免指令重排
静态内部类
直接上代码吧
public class SingleInnerClass {
private SingleInnerClass(){
}
public static class Inner {
private static final SingleInnerClass singleInnerClass = new SingleInnerClass();
}
public static SingleInnerClass getInstance(){
return Inner.singleInnerClass;
}
}
虚拟机会保证一个类的类构造器<clinit>()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器<clinit>(),其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。然后执行<clinit>()那条线程完了之后,其他线程就不再执行<clinit>()方法了。(一群人追一个女神,女神跟其中一个在一起了,其他人就有点眼力见吧 / bushi,在一起了可别共享了啊喂)
静态内部类的优点
- 外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存
-
只有调用 getInstance( )时才会加载内部类 Inner,继而创建单例实例
-
这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
关于volatile
之所以这里变量定义的时候不需要volatile,因为只有一个线程会执行具体的类的初始化代码<clinit>,也就是即使有指令重排序,因为根本没有第二个线程给你去影响,所以无所谓。 ( 追女神的路上没有情敌 )
怎么实现单例的篇幅就描述这么多,当然还有很多方法
那么单例就一定是单例吗?( 我反射第一个不服 )
反射破坏单例
通过反射,我们来获取单例类的构造器,继而通过反射来创建对象
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Single instance1 = Single.getInstance();
Class<Single> aClass = Single.class;
Constructor<Single> declaredConstructor = aClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Single instance2 = declaredConstructor.newInstance();
System.out.println(instance1 == instance2); // false
}
这里已经可以发现,instance1和instance2是两个对象,已经不符合单例的初衷
通过无参构造来获取反射对象,那在单例类的无参构造中再锁一次
private Single() {
synchronized (Single.class){
if (SINGLE != null){
throw new RuntimeException("不要试图通过反射破坏单例");
}
}
}
反射创建对象时,控制台抛出 “不要试图通过反射破坏单例”,但还没有结束,如果两个对象都用反射创建,仅仅再加一层锁的处理是不够的。我们再加一层标志位判断
private static boolean flag = false;
private Single(){
synchronized (Single.class){
// 第一次调用Single()会把flag变为true
if (!flag){
flag = true;
// 再次调用的时候就会抛出异常,即保持单例
}else {
throw new RuntimeException("不要试图用反射破坏单例");
}
}
}
添加一个标志位,红绿灯判断。
- 如果第一次创建单例,flag变为true(理解为:变为单例)
- 再次调用构造函数时,flag为true,则走else:throw new RuntimeException("不要试图用反射破坏单例");
但如果我们获取了标志位的名称,然后 setAccessible(true); 访问权限打开了,再把标志位掷为 false,这种单例也被破坏了
Single instance2 = declaredConstructor.newInstance();
Field flag = aClass.getDeclaredField("flag");
flag.setAccessible(true);
flag.set(instance2,false);
枚举单例
在反射方法 newInstance() 中,可以发现这样的描述
如果类型是枚举类,则抛出异常
创建一个简单的枚举类
public enum EnumSingle {
INSTANCE;
public static EnumSingle getInstance() {
return INSTANCE;
}
}
再次通过反射来试图破坏单例时,抛出了这样的异常
本来以为会抛出 Cannot reflectively creat enum Objects 结果是没有这样的方法(通过无参构造器创建对象)然后是一堆操作
这里简述为
- idea的out目录下确实是无参构造器
- 然后通过 jad工具(视频中使用的,我也是第一次直到)编译出其实枚举类中的构造器是有参数的
-
private EnumSingle(String s, int i) { super(s,i); }
给构造器中添加 参数.class 得到了我们预期的结果 Cannot reflectively creat enum Objects
总结
到这里我了解的单例模式就结束了,实现单例模式的方法有很多,有次看到了个文章方法多达十种
每种方式都有适用的场景,哪种就一定好,哪种就一定不好,这种说法是不合适的
静态内部类这种方法不需要开发人员显式地编写线程安全代码,即可解决多线程环境下它是不安全的问题。
掌握几种经典的方式就可以了。