① 如何保证线程安全
直接在idea上打开生成的SingletonEnum.class
文件:
好吧,没看到有用的信息,再用JDK自带反编译工具javap编译下:
可以看到继承自 Enum类
,但是代码不够全,再用jad工具反编译下:
反编译后的代码如下:
import java.util.concurrent.atomic.AtomicLong; public final class SingletonEnum extends Enum { public static SingletonEnum[] values() { return (SingletonEnum[]) $VALUES.clone(); } public static SingletonEnum valueOf(String name) { return (SingletonEnum) Enum.valueOf(SingletonEnum, name); } private SingletonEnum(String s, int i) { super(s, i); } public long getId() { return id.incrementAndGet(); } public static final SingletonEnum INSTANCE; private final AtomicLong id = new AtomicLong(0L); private static final SingletonEnum $VALUES[]; static { INSTANCE = new SingletonEnum("INSTANCE", 0); $VALUES = (new SingletonEnum[]{ INSTANCE }); } }
可以看到**INSTANCE
的初始化发生在static静态代码块**中,即在类加载阶段执行,保证了线程安全,但跟饿汉式一样,没有懒加载。
② 如何保证克隆安全
而防克隆则是要来到父类**Enum
类**中,直接实现了clone()函数:
调用此函数直接返回 CloneNotSupportedException
异常。
③ 如何保证反射安全
将反射部分代码中的Singleton改成SingletonEnum,接着运行下,抛出下述异常
在获取构造函数时抛出的异常,没有此构造方法,呕吼,看回jad反编译的代码:
这里使用的不是无参构造方法,而是有两个参数,改下反射代码,往getDeclaredConstructor()传入这个两个参数:
再次运行,还是报异常:
定位到 Constructor类
的 newInstance()
反射通过newInstance()创建对象时,会检查该类是否**ENUM
**修饰,是则抛出异常,反射失败。
④ 如何保证序列化安全
Java规范中规定:每一个枚举类型及其定义的枚举变量在JVM中都是唯一的。
因此在枚举类型的序列化和反序列化上,Java做了特殊的规定:
- 序列化时 → 仅仅将枚举对象的name属性输出到结果中;
- 反序列化时 → 通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。
定位到Enum类的valueOf()方法:
调用enumType
(Class对象的引用)的enumConstantDirectory
获取一个Map集合,集合中存放的键值对:
枚举name : 枚举示例变量
根据name即可拿到枚举实例,所以枚举单例序列化并不会重新创建新实例!
0x5、Kotlin中的单例
说完Java的单例,顺带提提Kotlin中的单例,使用一行代码即可创建安全单例:
object KotlinSingleton
就是这么简短,依次点击 Tools
→ Kotlin
→ Show Kotlin Bytecode
→ Decompile
反编译下:
呕吼,static静态代码块,饿汉式变种,线程安全,Android项目不考虑其他三个问题的话,可以大胆放心使用。
0x6、如何实现一个多例
单例
指的是:一个类只能创建一个对象,对应的 多例
则是:
一个类可以创建多个对象,但个数是有限的,比如只能创建5个对象。
实现方式也比较简单,通过一个Map来存储对象类型及对象间的对应关系,来控制对象的个数。示例如下:
import java.util.HashMap; import java.util.Map; import java.util.Random; public class BikeServer { private final long bikeNo; // 共享单车编号 private final String address; // 共享单车地址 private static final int BIKE_COUNT = 5; // 单车数量 private static final Map<Long, BikeServer> bikeInstances = new HashMap<>(); // 单车实例集合 // 私有化构造方法 private BikeServer(long bikeNo, String address) { this.bikeNo = bikeNo; this.address = address; } // 静态代码块中初始化实例 static { bikeInstances.put(1L, new BikeServer(1L, "罗湖区")); bikeInstances.put(2L, new BikeServer(2L, "南山区")); bikeInstances.put(3L, new BikeServer(3L, "福田区")); bikeInstances.put(4L, new BikeServer(4L, "宝安区")); bikeInstances.put(5L, new BikeServer(5L, "龙华区")); } // 根据编号获取单车实例 public static BikeServer getInstance(long bikeNo) { return bikeInstances.get(bikeNo); } // 随机获取单车实例 public static BikeServer getRandomInstance() { Random r = new Random(); return bikeInstances.get(r.nextInt(BIKE_COUNT) + 1L); } @Override public String toString() { return "BikeServer{" + "bikeNo=" + bikeNo + ", address='" + address + '\'' + '}'; } }
调用下:
public class BikeTest { public static void main(String[] args) { System.out.println(BikeServer.getInstance(2L).toString()); System.out.println(BikeServer.getRandomInstance().toString()); } } // 输出结果: BikeServer{bikeNo=2, address='南山区'} BikeServer{bikeNo=5, address='龙华区'}
注:本节讨论的单例都是进程内唯一,进程间不唯一,即不适用于多进程(集群)单例。
参考文献:
- 单例模式(上):为什么说支持懒加载的双重检测不比饿汉式更优?
- Java中单例模式的安全性分析
- 阿里面试官没想到,一个Volatile我能跟他扯半个小时
- Java多线程之volatile关键字和内存屏障