单例模式
在23种设计模式中,单例模式是较为常见的一种设计模式,
单例设计模式有如下特点:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
下面将介绍单例模式下的 饿汉式、懒汉式、以及枚举中的单例模式。
1. 恶汉式
饿汉式就是在对象一开始就把实例给创建了。
package com.xiaochao.single;
/**
* @program: demoCode
* @description: 恶汉式单例模式
* @author: 小超
* @create: 2021-04-12 10:31
**/
public class Hungry {
private final static Hungry HUNGRY=new Hungry();
private Hungry(){ }
public static Hungry getHungry(){
return HUNGRY;
}
}
上面的饿汉式我们可以看到 private final static Hungry HUNGRY=new Hungry(); 对象在一开始就被创建了,这种情况下会有一定的弊端,如果该对象里面还有其他很占用内存空间的属性,但是该单例对象又没有具体的地方用到,这时候被创建的单例对象就会很占用资源。接下来的懒汉式就解决了该问题。
2. 懒汉式
懒汉式就是我们在用的时候才会加载该对象。
package com.xiaochao.single;
/**
* @program: demoCode
* @description: 懒汉式单例
* @author: 小超
* @create: 2021-04-12 10:35
**/
public class LazyMan {
private LazyMan(){ }
private volatile static LazyMan lazyMan=null;
public static LazyMan getLazyMan(){
if (lazyMan==null){
lazyMan=new LazyMan();
}
return lazyMan;
}
}
上面就是一个简单的懒汉式单例模式,
- 首先私有化构造器,避免其他对象new该对象
- 其次私有化自身对象属性 private volatile static LazyMan lazyMan=null;
- 最后提供对外访问的静态方法 getLazyMan(),为外部提供唯一实例对象
上面的单例模式中我们能看到很多问题:
该单例模式在并发下是有问题的,当多个线程执行**getLazyMan()**方法时,将会创建多个实例,就不能保证单例模式的唯一性了。
解决办法就是加锁:
在getLazyMan方法上加同步
public static synchronized LazyMan getLazyMan(){
if (lazyMan==null){
lazyMan=new LazyMan();
}
return lazyMan;
}
上面给getLazyMan加上synchronized后就能保证多线程下该单例模式没有问题。
这种加锁在方法上虽然避免了多个线程多次new对象,但是每次有线程来执行该方法的时候都要抢锁,这种情况下效率会很低。
所以新的问题又来了:怎样提高效率呢?
双重检测锁定
package com.xiaochao.single;
/**
* @program: demoCode
* @description: 懒汉式单例
* @author: 小超
* @create: 2021-04-12 10:35
**/
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan(){ }
//双重检测锁模式的 懒汉式单例 DCL懒汉式
public static LazyMan getLazyMan(){
if (lazyMan==null){
synchronized (LazyMan.class){
//锁class
if (lazyMan==null){
lazyMan=new LazyMan();
}
}
}
return lazyMan;
}
}
上面就是双重检测锁定:这时候能看见我们将synchronized加到了代码块中,这样当有多个线程执行的时候首先就去判断该实例对象是否已经存在,如果存在就直接return,就不用每次执行该方法都去抢占锁了。提升了效率。
你以为这就完了吗???还不够~ ~ ~
我们在new一个对象的时候真就lazyMan=new LazyMan(); 这么一步就完成了吗?
其实创建对象他是分3步完成的:
- 分配内存空间
- 执行构造方法,初识化对象
- 把这个对象指向(1)的内存空间
如果按照1 2 3顺序执行那不会有问题,但是我们并没有禁止指令重排,如果按照 1 3 2这个顺序执行多线程下就会有问题。
下面我们画图演示问题所在
上面就展示了多线程下lazyMan对象未初始化的情况。
解决的办法就是防止指令重排:加上volatile关键字
private volatile static LazyMan lazyMan;
看了上面的懒汉模式是不是以为就完美了???
当然不是~ ~ ~
接下来就是炫技时刻:
静态内部类下的单例模式
package com.xiaochao.single;
/**
* @program: demoCode
* @description: 静态内部类
* @author: 小超
* @create: 2021-04-12 10:55
**/
public class Holder {
private Holder(){}
public static Holder getInstance(){
return InnerClass.HOLDER;
}
public static class InnerClass{
private final static Holder HOLDER=new Holder();
}
}
静态内部类实现的单例模式
在类创建的同时就已经加载了一个静态内部类new了一个Holder对象供系统使用。
既满足了线程安全,又避免了同步带来的性能影响。
3. 反射下单例模式的不安全
上面的单例模式,在反射下都是不安全的,虽然我们给构造器设置为私有属性,但是我们可以通过反射破坏掉构造器私有化:
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
//破坏构造器私有化
constructor.setAccessible(true);
//创建实例 lazyMan1 != lazyMan2违背唯一性
LazyMan lazyMan1=constructor.newInstance();
LazyMan lazyMan2=constructor.newInstance();
这时候我们的构造器就不再是私有的了,如果通过反射来创建LazyMan的实例,那么LazyMan的实例将不会唯一,这就导致单例模式不安全。
那么怎么解决反射破坏单例呢??
三重检测下的单例模式
package com.xiaochao.single;
/**
* @program: demoCode
* @description: 懒汉式单例
* @author: 小超
* @create: 2021-04-12 10:35
**/
public class LazyMan {
private static LazyMan lazyMan;
//用红绿灯变量来防止反射new对象
private static boolean xiaochao=false;
private LazyMan(){
synchronized (LazyMan.class){
if (!xiaochao){
xiaochao=true;
}else {
throw new RuntimeException("不要试图使用反射破坏单例");
}
}
}
//双重检测锁模式的 懒汉式单例 DCL懒汉式
public static LazyMan getLazyMan(){
if (lazyMan==null){
synchronized (LazyMan.class){
//锁class
if (lazyMan==null){
lazyMan=new LazyMan();
}
}
}
return lazyMan;
}
}
上面用到的就是三重检测下防止单例模式被破坏。
4. 枚举下的单例模式
上面说到用红绿灯变量来防止单例模式被反射破坏,但是治标不治本,因为通过反射依旧可以改变红绿灯变量的值,这样单例模式又可以被反射给破坏。
那怎样才能保证单例模式不被破坏呢???
这时候我们就应该用枚举来保证对象唯一性:
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
java中的枚举是不能被反射破坏的。
下面我们测试一下
class Test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingle instance1 = EnumSingle.INSTANCE;
EnumSingle instance2 = EnumSingle.INSTANCE;
//对比枚举下单例模式是否对象唯一
System.out.println(instance1);
System.out.println(instance2);
//尝试用反射破坏枚举,,,将会报错
Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
EnumSingle enumSingle = constructor.newInstance();
System.out.println(enumSingle);
}
}
输出结果
可以看出做后异常说不能用反射来创建枚举对象。