浅学一下单例模式

网课封校,大二就开始长毛了,闲着看这看那,然后看到讲解单例模式的视频。看完之后理解更好了些(可能是我比较菜吧,确实造诣不高)仔细的看完了一遍之后想着做个总结

单例模式最基本的就是构造器私有

饿汉式单例

顾名思义,有一个汉子,他很饿,所以他一上来就想着吃饱,甭管你需不需要他干活,他都得先吃饱,然后在那呆着。

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;
	}
}

那么问题解决了吗?奥夫考斯闹特!

指令重排

这种写法看似没什么问题,但是它是线程不安全的,多线程情况下,系统创建对象是分为三步走的

  1. 分配内存空间
  2. 初始化对象
  3. 将对象指向分配的空间
  • 在正常情况下是顺序执行,但也可能发生指令重排的现象,比如执行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

总结

到这里我了解的单例模式就结束了,实现单例模式的方法有很多,有次看到了个文章方法多达十种

每种方式都有适用的场景,哪种就一定好,哪种就一定不好,这种说法是不合适的

静态内部类这种方法不需要开发人员显式地编写线程安全代码,即可解决多线程环境下它是不安全的问题。

掌握几种经典的方式就可以了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值