什么是单例?为什么要用单例?
一个类被设计出来,就代表它表示具有某种行为(方法),属性(成员变量),而一般情况下,当我们想使用这个类时,会使用new关键字,这时候jvm会帮我们构造一个该类的实例。而我们知道,对于new这个关键字以及该实例,相对而言是比较耗费资源的。所以如果我们能够想办法在jvm启动时就new好,或者在某一次实例new好以后,以后不再需要这样的动作,就能够节省很多资源了。
哪些类可以使用单例?
一般而言,我们总是希望无状态的类能够设计成单例,那这个无状态代表什么呢? 简单而言,对于同一个实例,如果多个线程同时使用,并且不使用额外的线程同步手段,不会出现线程同步的问题,我们就可以认为是无状态的,再简单点:一个类没有成员变量,或者它的成员变量也是无状态的,我们就可以考虑设计成单例。
实现方法
好了,我们已经知道什么是单例,为什么要使用单例了,那我们接下来继续讨论下怎么实现单例。
一般来说,我们可以把单例分为行为上的单例和管理上的单例。行为上的单例代表不管如何操作(此处不谈cloneable,反射),至始至终jvm中都只有一个类的实例,而管理上的单例则可以理解为:不管谁去使用这个类,都要守一定的规矩,比方说,我们使用某个类,只能从指定的地方’去拿‘,这样拿到就是同一个类了。
而对于管理上的单例,相信大家最为熟悉的就是spring了,spring将所有的类放到一个容器中,以后使用该类都从该容器去取,这样就保证了单例。
所以这里我们剩下的就是接着来谈谈如何实现行为上的单例了。一般来说,这种单例实现有两种思路,私有构造器,枚举。
枚举实现单例
枚举实现单例是最为推荐的一种方法,因为就算通过序列化,反射等也没办法破坏单例性,例子:
public enum SingletonEnum {
INSTANCE;
public static void main(String[] args) {
System.out.println(SingletonEnum.INSTANCE == SingletonEnum.INSTANCE);
}
}
结果自然是true,而如果我们尝试使用反射破坏单例性:
public enum BadSingletonEnum {
/**
*
*/
INSTANCE;
public static void main(String[] args) throws Exception{
System.out.println(BadSingletonEnum.INSTANCE == BadSingletonEnum.INSTANCE);
Constructor<BadSingletonEnum> badSingletonEnumConstructor = BadSingletonEnum.class.getDeclaredConstructor();
badSingletonEnumConstructor.setAccessible(true);
BadSingletonEnum badSingletonEnum = badSingletonEnumConstructor.newInstance();
System.out.println(BadSingletonEnum.INSTANCE == badSingletonEnum);
}
}
结果如下:
Exception in thread "main" java.lang.NoSuchMethodException: cn.jsbintask.BadSingletonEnum.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at cn.jsbintask.BadSingletonEnum.main(BadSingletonEnum.java:18)
异常居然是没有init方法,这是为什么呢? 那我们反编译查看下这个枚举类的字节码:
// class version 52.0 (52)
// access flags 0x4031
// signature Ljava/lang/Enum<Lcn/jsbintask/BadSingletonEnum;>;
// declaration: cn/jsbintask/BadSingletonEnum extends java.lang.Enum<cn.jsbintask.BadSingletonEnum>
public final enum cn/jsbintask/BadSingletonEnum extends java/lang/Enum {
// compiled from: BadSingletonEnum.java
// access flags 0x4019
public final static enum Lcn/jsbintask/BadSingletonEnum; INSTANCE
// access flags 0x101A
private final static synthetic [Lcn/jsbintask/BadSingletonEnum; $VALUES
}
结果发现这个枚举类继承了抽象类java.lang.Enum,我们接着看下Enum,发现构造器:
/**
* Sole constructor. Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum type declarations.
*
* @param name - The name of this enum constant, which is the identifier
* used to declare it.
* @param ordinal - The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is assigned
* an ordinal of zero).
*/
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
那我们接着改变代码,反射调用这个构造器:
public enum BadSingletonEnum {
/**
*
*/
INSTANCE();
public static void main(String[] args) throws Exception{
System.out.println(BadSingletonEnum.INSTANCE == BadSingletonEnum.INSTANCE);
Constructor<BadSingletonEnum> badSingletonEnumConstructor = BadSingletonEnum.class.getDeclaredConstructor(String.class, int.class);
badSingletonEnumConstructor.setAccessible(true);
BadSingletonEnum badSingletonEnum = badSingletonEnumConstructor.newInstance("test", 0);
System.out.println(BadSingletonEnum.INSTANCE == badSingletonEnum);
}
}
结果如下:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at cn.jsbintask.BadSingletonEnum.main(BadSingletonEnum.java:21)
这次虽然方法找到了,但是直接给我们了一句Cannot reflectively create enum objects,不能够反射创造枚举对象,接着我们继续看下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;
}
关键代码就是:if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects");,所以就是jdk从根本上拒绝了使用反射去创建(知道为啥java推荐使用enum实现单例了吧),另外,我们再观察下Enum类的clone和序列化方法,如下:
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
throw new InvalidObjectException("can't deserialize enum");
}
private void readObjectNoData() throws ObjectStreamException {
throw new InvalidObjectException("can't deserialize enum");
}
一眼看出,直接丢出异常,不允许这么做!(真亲儿子系列)。
所以,结论就是:枚举是最靠谱的实现单例的方式!
私有构造器
另外一个实现单例最普通的方法则是私有构造器,开放获取实例公共方法,虽然这种方法还是可以用clone,序列化,反射破坏单例性(除非特殊情况,我们不会这么做),但是却是最容易理解使用的。而这种方式又分了饱汉式,饿汉式。
饿汉式
看名字就知道,饥渴!(咳咳,开个玩笑),它指的是当一个类被jvm加载的时候就会被实例化,这样可以从根本上解决多个线程的同步问题,例子如下:
public class FullSingleton {
private static FullSingleton ourInstance = new FullSingleton();
public static FullSingleton getInstance() {
return ourInstance;
}
private FullSingleton() {
}
public static void main(String[] args) {
System.out.println(FullSingleton.getInstance() == FullSingleton.getInstance());
}
}
结果自然是true,虽然这种做法很方便的帮我们解决了多线程实例化的问题,但是缺点也很明显,因为这句代码private static FullSingleton ourInstance = new FullSingleton();的关系,所以该类一旦被jvm加载就会马上实例化,那如果我们不想用这个类怎么办呢? 是不是就浪费了呢?既然这样,我们来看下替代方案! 饱汉式。
饱汉式
既然是饱,就代表它不着急,那我们可以这么写:
public class HungryUnsafeSingleton {
private static HungryUnsafeSingleton instance;
public static HungryUnsafeSingleton getInstance() {
if (instance == null) {
instance = new HungryUnsafeSingleton();
}
return instance;
}
private HungryUnsafeSingleton() {}
}
用意很容易理解,就是用到getInstance()方法才去检查instance,如果为null,就new一个,这样就不怕浪费了,但是这个时候问题就来了:现在有这么一种情况,在有两个线程同时 运行到了 instane == null这个语句,并且都通过了,那他们就会都实例化一个对象,这样就又不是单例了。既然这样,哪有什么解决办法呢? 锁方法
- 直接同步方法
这种方法比较干脆利落,那就是直接在getInstance()方法上加锁,这样就解决了线程问题:
public class HungrySafeSingleton {
private static HungrySafeSingleton instance;
public static synchronized HungrySafeSingleton getInstance() {
if (instance == null) {
instance = new HungrySafeSingleton();
}
return instance;
}
private HungrySafeSingleton() {
System.out.println("HungryUnsafeSingleton.HungryUnsafeSingleton");
}
public static void main(String[] args) {
System.out.println(HungrySafeSingleton.getInstance() == HungrySafeSingleton.getInstance());
}
}
很简单,很容易理解,加锁,只有一个线程能实例该对象。但是,此时问题又来了,我们知道对于静态方法而言,synchronized关键字会锁住整个 Class,这时候又会有性能问题了(尼玛墨迹),那有没有优化的办法呢? 双重检查锁:
public class HungrySafeSingleton {
private static volatile HungrySafeSingleton instance;
public static HungrySafeSingleton getInstance() {
/* 使用一个本地变量可以提高性能 */
HungrySafeSingleton result = instance;
if (result == null) {
synchronized (HungrySafeSingleton.class) {
result = instance;
if (result == null) {
instance = result = new HungrySafeSingleton();
}
}
}
return result;
}
private HungrySafeSingleton() {
System.out.println("HungryUnsafeSingleton.HungryUnsafeSingleton");
}
public static void main(String[] args) {
System.out.println(HungrySafeSingleton.getInstance() == HungrySafeSingleton.getInstance());
}
}
用意也很明显,synchronized关键字只加在了关键的地方,并且通过本地变量提高了性能(effective java),这样线程安全并且不浪费资源的单例就完成了。
总结
本章,我们一步一步从什么是单例,到为什么要使用单例,再到怎么使用单例,并且从源码角度分析了为什么枚举是最适合的实现方式,然后接着讲解了饱汉式,饿汉式的写法以及好处,缺点。
例子源码:https://github.com/jsbintask22/design-pattern-learning.git
本文原创地址:https://jsbintask.cn/2019/01/29/designpattern/singleton/,转载请注明出处。
如果你觉得有用,欢迎关注,分享!