手撕单例模式

大家好我是miHotel,今天来复习一下设计模式中的单例模式,下面是思维导图。
请添加图片描述

什么是单例模式

单例模式顾名思义,就是一个类从始至终只能创建一个对象,并且提供了一个全局访问点。ServletContext、ServletContextConfig、ApplicationContext、数据库连接池都是单例模式。

饿汉单例模式

首先是饿汉单例模式,所谓的“饿汉”就是不采用延迟加载,在类加载的时候就初始化,并创建单例对象。饿汉单例模式是绝对线程安全的,因为在线程还没有出现的时候就初始化了。
第一种写法:

public class HungrySingleton {
    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton() {};

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

利用静态代码块的第二种写法:

public class HungryStaticSingleton {
    private static final HungryStaticSingleton hungrySingleton;
    static {
        hungrySingleton = new HungryStaticSingleton();
    }

    private HungryStaticSingleton() {};

    public static HungryStaticSingleton getInstance() {
        return hungrySingleton;
    }
}

饿汉模式没有加锁,执行效率高,因而用户体验更好。但是很可能出现这个单例对象一直未被使用的情况,也就是“占坑不拉屎”浪费内存。为了解决这个问题下面来看看懒汉单例模式:

懒汉单例模式

懒汉式就是在需要使用对象的时候再去创建对象,下面就是一种写法:

public class LazySimpleSingleton {
    private static  LazySimpleSingleton lazySimpleSingleton = null;
    private LazySimpleSingleton() {}
    public static LazySimpleSingleton getInstance() {
        if (lazySimpleSingleton == null) {
            lazySimpleSingleton = new LazySimpleSingleton();
        }
        return lazySimpleSingleton;
    }
}

这种写法存在的问题就是线程不安全,当多个线程同时访问getInstance方法时,有可能同时进入if语句,从而创建了多个对象,后创建的对象会覆盖第一个对象。通过加锁可以解决线程不安全问题:

public class LazySimpleSingleton {
    private static LazySimpleSingleton lazySimpleSingleton = null;
    private LazySimpleSingleton() {}
    public synchronized static LazySimpleSingleton getInstance() {
        if (lazySimpleSingleton == null) {
            lazySimpleSingleton = new LazySimpleSingleton();
        }
        return lazySimpleSingleton;
    }
}

给getInstance整个方法进行加锁,这样当一个线程进入该方法时另外一个线程会从RUNNING状态变成MONITOR状态,第一个线程执行完成后,第二个线程再进入getInstance方法后lazySimpleSingleton≠null,因此只会创建一个对象。这种写法虽然解决了线程安全问题,但是性能差,当有大量线程访问是会照成很多线程发生阻塞,因此需要对代码进行改进,下面是双检的写法:

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazy;
    private LazyDoubleCheckSingleton() {}
    public static LazyDoubleCheckSingleton getInstance() {
        if (lazy == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazy == null) {
                    lazy = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazy;
    }
}

通过改进,多个线程都可以进入getInstance方法,也可以通过第一层检查,然后就会竞争一个锁,第一个拿到锁的线程会对lazy进行实例化,后面拿到锁的线程就不会通过第二层检查。这里给静态成员变量加上volatile关键字是防止编译器对lazy = new LazyDoubleCheckSingleton()这段代码的指令进行优化,这段代码的指令分为三条:

  1. 分配内存空间
  2. 初始化对象
  3. 将变量lazy指向刚刚分配的内存空间(这步后lazy不为null)

volatile可以防止第二条和第三条指令发生重排序,指令顺序变为1->3->2,也就是lazy不为null但是没有初始化。在 3 执行完毕、2 未执行之前,被另一个抢占了,这时 lazy已经是非 null 了(但却没有初始化),所以该线程会直接返回 lazy,然后使用,然后顺理成章地报空指针。

双检的写法毕竟是需要加锁的,所以还是会对性能产生影响。利用类的加载性质,可以使用静态内部类实现懒汉单例模式,首先我们知道一个类的初始化时机有以下6种:

  • (1) 创建类的实例( new)
  • (2) 访问某个类或接口的静态变量,或对静态变量赋值
  • (3) 调用类的静态方法
  • (4) 反射 (Class.forname(“全限定类名”))
  • (5) 初始化一个类的子类(先初始化父类)
  • (6) JVM启动时标明的启动类,就是类名和文件名相同的那个类 注意: 访问常量(static final)不会导致类的初始化; 使用Class.loader()方法加载类时也不会对类初始化

可以看到访问某个类的静态的静态变量这个类就会被初始化,类的加载过程不会马上加载内部类,而是在使用时进行加载,利用这个性质就能实现懒加载,而JVM已经保证了线程的安全性,静态内部类在方法调用前就会被初始化,代码如下:

public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton() {}
    public static LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY;
    }
    private static class LazyHolder {
        private static LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

反射破坏单例模式

上面代码的构造器除了加了private修饰符以外没有做任何限制,因此可以通过反射来破坏单例模式

@Test
public void test04() {
    try {
        Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class;
        Constructor<LazyInnerClassSingleton> c = clazz.getDeclaredConstructor(null);
        c.setAccessible(true);// 强制访问
        LazyInnerClassSingleton o1 = c.newInstance();
        LazyInnerClassSingleton o2 = c.newInstance();
        System.out.println(o1 == o2);
    }catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
}

输出:false, 因此需要对构造器进行处理,代码如下:

public class LazyInnerClassSingleton2 {

    private LazyInnerClassSingleton2() {
        if (LazyHolder.LAZY != null) {
            throw new RuntimeException("不允许创建多个实例");
        }
    }
    public static LazyInnerClassSingleton2 getInstance() {
        return LazyHolder.LAZY;
    }
    public static class LazyHolder {
        public static LazyInnerClassSingleton2 LAZY = new LazyInnerClassSingleton2();
    }
}

通过反射调用构造器创建对象时,执行LazyHolder.LAZY语句时,静态内部类会初始化,导致LazyHolder.LAZY != null,从而抛出异常,无法创建多个对象。

序列化破坏单例对象

序列化是指将一个对象序列化后写入磁盘,下次使用时再从磁盘种读取对象并进行反序列化,转化为内存对象。反序列化的对象会重新分配内存,因此如果反序列化的目标对象是单例对象就会破坏单例,下面来看代码:
首先写一个饿汉单例模式:

public class SeriableSingleton implements Serializable {

    private static SeriableSingleton instance = new SeriableSingleton();

    private SeriableSingleton() {}

    public static SeriableSingleton getInstance() {
        return instance;
    }
}

下面我们测试反序列化是否能破坏单例模式:

@Test
public void test06() {
    try {
        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();
        FileOutputStream fos = new FileOutputStream("SeriableSingleton.obj");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s2);
        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
        ObjectInputStream ooi = new ObjectInputStream(fis);
        s1 = (SeriableSingleton) ooi.readObject();
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s1 == s2);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

控制台输出如图所示:
在这里插入图片描述
可以看到反序列化后的对象和手动创建的对象是不一样的,实例化了两次,也就破坏了单例。那么如何解决这个问题呢?其实我们只需要增加readResolve方法即可,代码如下:

public class SeriableSingleton implements Serializable {

    private static SeriableSingleton instance = new SeriableSingleton();

    private SeriableSingleton() {}

    public static SeriableSingleton getInstance() {
        return instance;
    }
    
    private Object readResolve() {
        return instance;
    }
}

再来测试一下,看看运行结果,如下图所示。
在这里插入图片描述
可以看到神奇的事情发生了, 加上了readResolve方法后反序列化并没有破坏单例模式,那这到底是什么原因呢,我们来看看ObjectInputStream类的readObject方法:

private final Object readObject(Class<?> type)
        throws IOException, ClassNotFoundException
    {
        ....
        try {
            Object obj = readObject0(type, false);
            .....
						return obj;
        } 
				.......
    }

再进入readObject0方法

private Object readObject0(Class<?> type, boolean unshared) throws IOException {
	...
	switch (tc) {
		case TC_OBJECT:
    ....
    return checkResolve(readOrdinaryObject(unshared));
	}
	...
}

可以看到在TC_OBJECT中调用了readOrdinaryObject方法, 我们再进入该方法:

private Object readOrdinaryObject(boolean unshared)
        throws IOException {
	...
	Object obj;
  try {
      obj = desc.isInstantiable() ? desc.newInstance() : null;
  } catch (Exception ex) {
      throw (IOException) new InvalidClassException(
          desc.forClass().getName(),
          "unable to create instance").initCause(ex);
  }
	...
	if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
						Object rep = desc.invokeReadResolve(obj);
            .....
            if (rep != obj) {
                .....
                handles.setObject(passHandle, obj = rep);
            }
        }
	...
	return obj;
}

通过阅读源码,我们可以知道,isInstantiable()是判断构造器是否为空,如果不为空就返回true,因此就会创建一个新的对象。再往下看我们可以看到还调用了hasReadResolveMethod方法来判断反序列化的对象有没有readResolve方法,如果有则返回true,然后就会invokeReadResolve方法,看这方法名就知道是通过反射来调用readResolve方法,我们进入该方法看看源码:

Object invokeReadResolve(Object obj)
    throws IOException, UnsupportedOperationException
{
	....
	return readResolveMethod.invoke(obj, (Object[]) null);
	....
}

到这是不是有点恍然大悟,rep就是调用我们添加的readResolve方法返回的单例对象,源码中做了一个判断,如果创建的obj对象与rep对象不同,则将rep赋值给obj,最后返回,这样反序列化返回的就是单例对象。通过源码分析可以看出readResolve虽然解决了单例模式被破坏的问题,但是实际上该对象还是创建了两次,只是最后返回了相同的引用,如果创建对象的频率加快,内存开销也会增大。

注册式单例模式

注册式单例模式顾名思义就是将每个实例用唯一标识符登记再某个“本子”上,如果要使用某个实例时,就去”本子“上查。注册式单例模式可以分为枚举式和容器式。

1.枚举式单例模式

首先我们来看一下枚举式单例模式的写法:

public enum EnumSingleton {
    INSTANCE;
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}

然后我们测试一下反序列化能否打破单例模式

@Test
public void test07() {
    try {
        EnumSingleton instance1 = null;
        EnumSingleton instance2 = EnumSingleton.getInstance();
        instance2.setData(new Object());
        FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(instance2);
        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("EnumSingleton.obj");
        ObjectInputStream ooi = new ObjectInputStream(fis);
        instance1 = (EnumSingleton) ooi.readObject();
        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance1 == instance2);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

运行结果如图所示
在这里插入图片描述

为啥枚举式单例模式不会被反序列化破坏呢,我们使用反编译工具jad来看一下EnumSingleton.class的反编译代码, 可以看到如下代码

static 
{
    INSTANCE = new EnumSingleton("INSTANCE", 0);
    $VALUES = (new EnumSingleton[] {
        INSTANCE
    });
}

这说明枚举型单例模式是饿汉模式的实现,我们再看下JDK源码,搞清楚反序列化为啥不能破坏枚举式单例模式。下面是java.io.ObjectInputStream#readObject0的源码

private Object readObject0(Class<?> type, boolean unshared) throws IOException {
	....
	switch (tc) {
		....
		case TC_ENUM:
    if (type == String.class) {
        throw new ClassCastException("Cannot cast an enum to java.lang.String");
    }
    return checkResolve(readEnum(unshared));
		....
	}
	....
}

再进入readEnum方法

private Enum<?> readEnum(boolean unshared) throws IOException {
        if (bin.readByte() != TC_ENUM) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        if (!desc.isEnum()) {
            throw new InvalidClassException("non-enum class: " + desc);
        }

        int enumHandle = handles.assign(unshared ? unsharedMarker : null);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(enumHandle, resolveEx);
        }

        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

        handles.finish(enumHandle);
        passHandle = enumHandle;
        return result;
    }

可以发现,反序列化的枚举类型是通过类名和类对象来找到唯一的枚举对象,因此枚举对象不会被类加载器加载多次。再试试反射能否破坏单例模式, 首先java.lang.Enum只有一个构造器:

protected Enum(String name, int ordinal) {
      this.name = name;
      this.ordinal = ordinal;
}

我们尝试通过反射来调用这个构造器,创建对象

@Test
    public void test08() {
        try {
            Class<EnumSingleton> clazz = EnumSingleton.class;
            Constructor<EnumSingleton> c = clazz.getDeclaredConstructor(String.class, int.class);
            c.setAccessible(true);
            EnumSingleton miHoltel = c.newInstance("miHoltel", "666");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

运行结果如下图所示:

在这里插入图片描述

错误提示不能通过反射创建枚举对象,我们再来看看JDK源码,进入Contructor类的newInstance方法

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

可以看到在newInstance方法中做了强制判断,如果是枚举类型则直接抛出异常。

容器式单例模式

下面来看一下注册式单例模式的另外一种写法

public class ContainerSingleton {
    
    private ContainerSingleton() {}
    
    private static Map<String, Object> ioc = new ConcurrentHashMap<>();

    public static Object getBean(String className) {
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            } else {
                return ioc.get(className);
            }
        }
    }
}

容器式单例模式适用于实例非常多的情况。Spring中应用容器式单例模式的代码如下:

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {
	.....
	private final ConcurrentMap<String, BeanWrapper> factoryBeanInstanceCache;
	
	public AbstractAutowireCapableBeanFactory() {
		...
		this.factoryMethodCandidateCache = new ConcurrentHashMap();
		...
	}
	.....
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值