1. 基本概念
java 进程 内存中只有一个 对象实例。
- 实现的基本原则:
- 构造器私有化,不允许外部创建对象。
- 提供public static 的访问点,返回创建的对象。
- 应用场景:
- spring ioc 容器默认单例。
- 全局配置对象,全局一份
- 框架封装,结合其他设计模式一起使用。
2. 实现方式
- 饿汉式
- 懒汉式
双重检查锁
静态内部类
- 3.ThreadLocal
- 枚举
- CSA 原子类
- 注册式单例
3. 破坏单例的方式
- 暴力反射
- 序列化和反序列化
4.代码实现
上面我们简单的总结了一下有关单例模式的相关知识点,接下来我们就来用代码实现一下常见单例的几种写法。以及如何保证单例的线程安全。
4.1 饿汉式
public class HungarySingleton {
//类加载时进行初始化
private static final HungarySingleton instance = new HungarySingleton();
// 构造器初始化
private HungarySingleton() {}
// 全局访问点
public static HungarySingleton getInstance() {
return instance;
}
}
优点:饿汉式在类加载时就初始对象,并且只初始化一次,所以是线程安全的。
缺点:这种写法是强引用,在JVM 里面永远不会被回收。同时在jvm 启动时也会消耗一定的资源,不管是否使用,都已经创建了,存在资源的浪费,如果在jvm 里面饿汉式单例太多了,就很浪费资源了,并且被创建的对象也无法被垃圾回收。后面我们会讲懒加载,这里我们首先来测试一下单例破坏的反射和序列化。
- 反射破坏:
/**
* 反射破坏
* @throws Exception
*/
public static void test2() throws Exception {
//得到默认构造器
Constructor<HungarySingleton> declaredConstructor = HungarySingleton.class.getDeclaredConstructor();
//强制访问
declaredConstructor.setAccessible(true);
// 创建对象
HungarySingleton hungarySingleton = declaredConstructor.newInstance();
System.out.println(hungarySingleton);
HungarySingleton instance = HungarySingleton.getInstance();
System.out.println(instance);
}
- 测试结果
com.example.designpattern.singleton.HungarySingleton@7440e464
com.example.designpattern.singleton.HungarySingleton@49476842
Process finished with exit code 0
从上面的测试结果看出来,饿汉式创建的单例可以被发射破坏。为了解决这个问题我们可以 在构造器那里做一下手脚:因为是反射调用构造器,所以我们可以在构造器中判断一下,如果对象已经存在了就抛出异常,防止再创建一次对象。
- 修改构造器:
public class HungarySingleton {
//类加载时进行初始化
private static final HungarySingleton instance = new HungarySingleton();
// 构造器初始化
private HungarySingleton() {
if (instance!=null) {//判断对象是否已经被创建
throw new RuntimeException("请不要重复创建对象");
}
}
// 全局访问点
public static HungarySingleton getInstance() {
return instance;
}
}
- 测试结果:
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.example.designpattern.singleton.HungarySingleton.test2(HungarySingleton.java:62)
at com.example.designpattern.singleton.HungarySingleton.main(HungarySingleton.java:48)
Caused by: java.lang.RuntimeException: 请不要重复创建对象
at com.example.designpattern.singleton.HungarySingleton.<init>(HungarySingleton.java:22)
... 6 more
Process finished with exit code 1
从上面的测试结果我们可以看出,反射是可以破坏单例的,当然针对饿汉式单例的反射破坏我们也可以有一些措施。接下来我们来看看 序列化和反序列化是如何破坏单例的。
- 序列化和反序列化破坏单例
/**
* 序列化和反序列化破坏单例
* */
public static void test3() throws IOException, ClassNotFoundException {
//将对象写到磁盘
HungarySingleton hungarySingleton = HungarySingleton.getInstance();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("hungarySingleton.obj"));
outputStream.writeObject(hungarySingleton);
System.out.println(hungarySingleton);
//然后再将对象从磁盘读取出来
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("hungarySingleton.obj"));
HungarySingleton object = (HungarySingleton)inputStream.readObject();
System.out.println(object);
}
- 测试结果
饿汉式=com.example.designpattern.singleton.HungarySingleton@5451c3a8
序列化和反序列化=com.example.designpattern.singleton.HungarySingleton@3d494fbf
上述测试结果表明 序列化和反序列化也能够对单例造成破坏,可以阅读源码找到原因。注意这里我是实现了 Serializable 接口的。
- 应对策略
- 不要实现 Serializable 接口。序列化和反序列化要求必须实现 Serializable 接口,所以为了 防止 序列化和反序列化对单例的破坏,可以不要实现 Serializable 接口。
- 如果业务要求必须要实现Serializable 接口,那么就只有下面这一种方式可以应对:重写 readResolve() 方法。
/**
* 防止序列化和反序列化对单例的破坏,返回单例对象
* @return
*/
public Object readResolve() {
return instance;
}
上面这个返回会在 HungarySingleton object = (HungarySingleton)inputStream.readObject(); 这句话执行的时候 回调,直接就返回你自己 返回的对象,所以可以 应对 对单例的破坏。具体的可以看看 jdk 的源码,不能够找到答案。上面我们实现了饿汉式单例,并且分析了饿汉式单例的优缺点,以及反射和序列化,反序列化对单例的破坏。以及相应的应对策略。下面的内容我们就只针对 单例的一些实现展开谈论,对单例的破坏就不做分析了,可以自己去测试。
4.2 懒加载
- 双重检查锁
在double check 之前我们先来看看 为什么会出现 double check 这种写法。
最简单的懒加载:
public class SimpleSingleton {
//1.静态成员
private static SimpleSingleton instance;
//2. 构函数私有化
private SimpleSingleton() {}
//3. 提供全局访问方法
public static SimpleSingleton getInstance() {
if (instance == null) {
instance = new SimpleSingleton();
}
return instance;
}
}
- 单线程测试:
//单线程测试
public static void test() {
SimpleSingleton instance = SimpleSingleton.getInstance();
SimpleSingleton instance2 = SimpleSingleton.getInstance();
SimpleSingleton instance3 = SimpleSingleton.getInstance();
System.out.println(instance);
System.out.println(instance2);
System.out.println(instance3);
}
com.example.designpattern.singleton.SimpleSingleton@7440e464
com.example.designpattern.singleton.SimpleSingleton@7440e464
com.example.designpattern.singleton.SimpleSingleton@7440e464
从上面的单线程测试中没有发现问题,接下来进行多线程测试,这里我们就使用简单的多线程测试,也可以并发测试。
- 多线程测试
//多线程测试
public static void test2() {
for (int i=0 ;i<3; i++) {
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
SimpleSingleton instance = SimpleSingleton.getInstance();
System.out.println(instance);
}).start();
}
}
com.example.designpattern.singleton.SimpleSingleton@21279f82
com.example.designpattern.singleton.SimpleSingleton@25f209f5
com.example.designpattern.singleton.SimpleSingleton@6e154e44
这里我们看到直接就产生了3个不同的对象,显然违背单例的设计思想。针对线程安全问题,我们可以使用 锁来解决这个问题。于是就有了下面几种线程安全的写法。
- 静态同步方法和同步代码块
// 静态方法上面 加 synchronized 关键字
public static synchronized SimpleSingleton getInstance() {
if (instance == null) {
instance = new SimpleSingleton();
}
return instance;
}
// 同步代码块
public static SimpleSingleton getInstance() {
synchronized(SimpleSingleton.class) {//这里的锁 一般这样写,但也可以是 静态对象 作为锁
if (instance == null) {
instance = new SimpleSingleton();
}
return instance;
}
}
上面的 两种写法 效果完全一样,效率也一样,锁的范围也一样,都是 类锁。至于为什么是 类级别的锁,不是 对象级别的,有下面几个原因:
1.静态方法本身是可以使用类直接调用,也就在类级别 ,在静态方法上面加锁的化 自然也就是类级别的锁了;
2 . 静态方法里面的 同步代码块为啥 是 类级别的锁。注意这里因为我用的 SimpleSingleton.class 作为锁,所以说上面的两种写法是等效的。一般使用 SimpleSingleton.class作为锁是 避免创建 其他锁对象,这里是不能使用 this 作为锁的,这也是 因为有 static 关键字的原因。
3 . 在这里能不能使用 对象锁呢?答案是可以的,但是必须是 static 对象 如:
// 创建一个对象作为锁
final static Object object = new Object();
public static SimpleSingleton getInstance() {
synchronized(object) {// 使用对象锁
if (instance == null) {
instance = new SimpleSingleton();
}
return instance;
}
}
//显然这样的写法没有 SimpleSingleton.class 作为锁简单,
//因为你自己又单独 创建了一个 Object 对象,而且还是 饿汉式创建的。
分析完了上面的 锁的问题,我们再来分析一下 这种写法的优缺点,是否值得我们平时的项目中使用:
1 .优点:synchronized 关键字保证了线程安全,同时也是懒加载的。
2 .缺点:从上面的代码中我们可以看到 我们的锁都是全局锁,也就是说 每一个线程来访问我们的方法的时候 被要先去 获得锁,方法执行完了以后再去释放锁。我们知道,单例对象只在第一次访问的时候 创建就ok 了 ,也就是 下面这个逻辑 只在 第一次 访问该方法的 时候 instance == null ,然后创建 对象。如果在 线程安全的情况下 后续的线程 的 instance 都是不为空的,就不会去创建对象了,也就保证了线程安全了,那么我们对整个方法 都加上锁 就很低效了。
if (instance == null) {// 第一次访问的时候 满足
instance = new SimpleSingleton();
}
上面我们分析出 静态同步方法和同步代码块 虽然能够保证线程安全,但是也带来可一些性能问题,那么我们就 优化一下性能就ok 了。既然instance 只有在 第一次 访问的 时候 是 null 那么 我们就在 if 里面来 加一个锁 也就是在 第一次创建的时候 保证 线程安全就ok了,后面的线程 都不会 进入 if ,所以就有了下面的优化方案:
public static SimpleSingleton getInstance() {
if (instance == null) {
synchronized (SimpleSingleton.class) {
instance = new SimpleSingleton();
}
}
return instance;
}
- 测试:
public static void test2() {
for (int i=0 ;i<3; i++) {
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
SimpleSingleton instance = SimpleSingleton.getInstance();
System.out.println(instance);
}).start();
}
}
com.example.designpattern.singleton.SimpleSingleton@6e154e44
com.example.designpattern.singleton.SimpleSingleton@2fd04fd1
com.example.designpattern.singleton.SimpleSingleton@21279f82
测试结果我们发现,我擦,怎么会呢?不是加了锁了吗??我们仔细来分析一下为啥会是这样的:
public static SimpleSingleton getInstance() {
if (instance == null) {// 1. 当第一次 到这里的就只有一个 线程 1 ,那么这个是线程安全的;2 . 当线程 1 和线程 2 同时到 这里了,不管是线程1 和线程 2 谁获得了锁 都会是 执行 创建对象的 语句,也就有了多个对象。
synchronized (SimpleSingleton.class) {
instance = new SimpleSingleton();
}
}
return instance;
}
针对上面的问题 我们自然就有了 下面的写法,也就是我们在 同步代码块里面再判断一次,就可以保证线程安全了,也就有了 双重检查锁的写法:
public static SimpleSingleton getInstance() {
if (instance == null) {// 当线程1 和线程2 都执行到了这里,假设线程 1 获取了锁
synchronized (SimpleSingleton.class) {
if (instance == null) {
instance = new SimpleSingleton();// 线程1 创建了 对象,执行完毕 退出 同步代码块,释放锁,这时候 线程 2 获取了锁,然后 读取了 instance 的值 发现 不为空 ,第二个 if 条件就不满足了,不会执行 对象创建的语句。
}
}
}
return instance;
}
到这里我们就 把为啥会出现 double check 的过程分析了一下,但是 上面 的写法都还不是 线程安全的,因为 instance = new SimpleSingleton(); 在jvm 创建对象的指令 中 不是原子的,也就说 jvm 创建对象 至少有 下面几条指令:1. 申请一块内存空间; 2. 创建一个对象;3. 将地址值赋值个 变量;在并发量高的情况下,可能会发生可见性问题和指令重排序问题,
为了解决这个问题 我们可以 使用 volatile 来 修饰 instance。下面我们来看看 完整的 double check 是怎么写的。
public class DoubleCheckSingleton {
// volatile 保证可见性,防止指令重排序
private static volatile DoubleCheckSingleton singleton;
public static DoubleCheckSingleton doubleCheckSingleton() {
if (singleton == null) {
synchronized (DoubleCheckSingleton.class) {
if (singleton == null ) {//为了防止 两个线程都进了 第一个if导致的线程安全问题,所以可以再加一次判断
singleton = new DoubleCheckSingleton();
}
}
}
return singleton;
}
}
上面我们分析了double check ,一切看上去都很完美,但是就是有一点,使用了线程同步机制,来保证线程的安全性,那么有没有一种不使用线程同步机制 也可以 实现线程安全和懒加载呢?答案是肯定的,那就是静态 内部类。另外,在这里我们没有分析 反射 ,序列化和反序列化对单例的破坏。答案是这两种都是可以破坏 double check 的单例,可以自己测试一下。接下来我们来分析一下 静态内部类的单例。
4.2静态内部类
public class InnerStaticSingleton {
private InnerStaticSingleton() {}
//但外部调用 SingletonHolder.innerStaticSingleton 时才会加载这里的静态内部类
private static class SingletonHolder{
private final static InnerStaticSingleton innerStaticSingleton = new InnerStaticSingleton();
}
public static InnerStaticSingleton getInstance() {
return SingletonHolder.innerStaticSingleton;
}
}
上面是静态内部类的实现方式,这里我解释一下,为什么是懒加载的。在外部类加载到jvm 时,静态内部类是不会被加载的,也就不会执行 private final static InnerStaticSingleton innerStaticSingleton = new InnerStaticSingleton(); 只有当 外部类的 static 方法被调用时,才会 加载 内部类,并实例化对象。静态在整个 jvm 运行周期中都只加载一次,所以是可以保证单例的,根据前面的分析,只有在调用时才会去初始化对象,所以是懒加载的。至于线程安全,也是利用jvm 内部机制保证的,到底是如何保证的,由于笔者水平有限,暂无法解释,希望大家留言谈论。该方式通常被认为是最优的 单例实现方式,但是也有一个缺点,就是参数传递的问题。所以到底要使用哪一种实现方式,是取决于 实际应用场景的。虽然该方式 优雅,但是同样可以被反射和序列化破坏。那么到底有没有一种单例是能够防止反射和序列化的破坏,答案是肯定的,那就是枚举式单例,终极杀招。
4.3枚举式单例
public enum EnumSingleton {
INSTANCE;
}
这就是枚举式单例,是不是非常简单。下面我们来测试一下反射和序列化得破坏结果,看看是否能达到我们的预期。
- 反射破坏
public static void test() throws Exception{
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
EnumSingleton enumSingleton = declaredConstructor.newInstance();
System.out.println(enumSingleton);
}
- 测试结果
Exception in thread "main" java.lang.NoSuchMethodException: com.example.designpattern.singleton.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.example.designpattern.singleton.EnumSingleton.test(EnumSingleton.java:23)
at com.example.designpattern.singleton.EnumSingleton.main(EnumSingleton.java:18)
上面是反射的测试结果,直接给我们异常了,说没有默认构造器,后面我们会 分析一下 底层的原来,看看枚举单例到底是怎么回事。这里我们再来测试一下序列化。
- 序列化测试
public static void test2() throws Exception{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("EnumSingleton.obj"));
EnumSingleton instance = EnumSingleton.INSTANCE;
System.out.println(instance);
outputStream.writeObject(instance);
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("EnumSingleton.obj"));
EnumSingleton enumSingleton =(EnumSingleton)objectInputStream.readObject();
System.out.println(enumSingleton);
}
- 测试结果:
INSTANCE
INSTANCE
结果返回了同一个对象,说明jdk 也为我们屏蔽了序列化对单例的影响。到这来是不是觉得很牛叉,枚举都帮我们做了,首先是线程安全的,其次反射和序列化也不能破坏它,但是是不是懒加载的呢?肯定不是,因为枚举也是在jvm 加载的时候就会初始化的。在 Effective java 那本书里面,作者就推荐使用 枚举式单例。当然到底使用哪一种,我们还是要根据业务场景来选择。好了,既然枚举这么牛掰,我们能不能看看jvm 在加载 枚举的时候,到时是怎么做到的?接下来我们就来看看 枚举的神秘面纱。首先,从代码层面看不出啥东西,那么,我们就要想办法看看他编译后的样子。
反编辑工具 jad
- 反编译 EnumSingleton.class
jad EnumSingleton.class 。会生成一个 EnumSingleton.jad 的文件。
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumSingleton.java
package com.example.designpattern.singleton;
import java.io.*;
import java.lang.reflect.Constructor;
public final class EnumSingleton extends Enum
{
public static final EnumSingleton INSTANCE;
private static final EnumSingleton $VALUES[];
// 构造器,私有化,没有无参构造器,所以我们在测试的时候 会抛出异常,说没有无参构造器。
private EnumSingleton(String s, int i)
{
super(s, i);
}
// 静态代码块初始化对象,饿汉式写法,是线程安全的。
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
public static EnumSingleton[] values()
{
return (EnumSingleton[])$VALUES.clone();
}
public static EnumSingleton valueOf(String name)
{
return (EnumSingleton)Enum.valueOf(com/example/designpattern/singleton/EnumSingleton, name);
}
}
上面是 EnumSingleton.class 反编译的结果。我们看到 EnumSingleton 枚举 继承的 Enum 对象,该对象是 jdk 自带的java.lang下面的抽象类
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
protected Enum(String name, int ordinal) {//只有这一个构造函数
this.name = name;
this.ordinal = ordinal;
}
}
上面解释了我们在反射 调用无参构造函数的时候,为啥会有异常抛出,那是因为枚举本身就没有无参构造函数。好了,到这里可能你又发现了,虽然没有无参构造函数,但是有 两个带参数的构造函数,我们能不能调用呢?我们来测试一下就知道了。
public static void test() throws Exception{
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class); //得到带有参数的构造函数
declaredConstructor.setAccessible(true);
EnumSingleton enumSingleton = declaredConstructor.newInstance("测试",007);// 调用,创建对象
System.out.println(enumSingleton);
}
- 测试结果:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.example.designpattern.singleton.EnumSingleton.test(EnumSingleton.java:25)
at com.example.designpattern.singleton.EnumSingleton.main(EnumSingleton.java:18)
上面结果说,不能通过反射来创建 枚举对象。他说在 java.lang.reflect.Constructor.newInstance(Constructor.java:417) 417行 抛出的异常。我们就去看一下 :
- Constructor
@CallerSensitive
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;
}
好了,上面我们就解释了,为啥枚举能够防止反射破坏单例,原来是jdk 帮我们做了这个事情了。我们还有一个序列化破坏没有找到原因。接下来我们就来看看序列化的原因,由于篇幅太长,可能已经忘记了 序列化测试代码,我们再来贴一下:
public static void test2() throws Exception{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("EnumSingleton.obj"));
EnumSingleton instance = EnumSingleton.INSTANCE;
System.out.println(instance);
outputStream.writeObject(instance);
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("EnumSingleton.obj"));
EnumSingleton enumSingleton =(EnumSingleton)objectInputStream.readObject();
System.out.println(enumSingleton);
}
上面代码就两个意思,1 . 把对象写到磁盘;2 . 从磁盘读出来对象。既然读的时候得到的是一个对象,那么我们就直观 的先从读 开始,看看能不能找到答案,如果不能,我们再去 分析写。
EnumSingleton enumSingleton =(EnumSingleton)objectInputStream.readObject();
看看 readObject()里面
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {// 这里是false
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false); //那么 对象就是从这里出来的,我们进去看看
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
----- enableOverride 我们使用的这个构造器
public ObjectInputStream(InputStream in) throws IOException {
verifySubclass();
bin = new BlockDataInputStream(in);
handles = new HandleTable(10);
vlist = new ValidationList();
serialFilter = ObjectInputFilter.Config.getSerialFilter();
enableOverride = false; // false
readStreamHeader();
bin.setBlockDataMode(true);
}
------ Object obj = readObject0(false);
private Object readObject0(boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
/*
* Fix for 4360508: stream is currently at the end of a field
* value block written via default serialization; since there
* is no terminating TC_ENDBLOCKDATA tag, simulate
* end-of-custom-data behavior explicitly.
*/
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}
byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}
depth++;
totalObjectRefs++;
try {
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM: // 前面的 啥逻辑 我们也看不太懂,但是这里可以看到是和枚举相关的 ,那么我们 去看看 readEnum(unshared)
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}
case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
------ readEnum(unshared)
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);// 这里就是得到对象的 描述 ObjectStreamClass 对象,
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); // 得到 Enum 对象,我们知道 枚举 是继承 Enum 的,这里也是 返回的 Enum 。通过 枚举 class 对象 和 name 就得到 了一个唯一的对象,这个name 就是 我们通常自己定义的 枚举的对象的name。我们可以继续 下去,看看 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;
----- name 枚举对象的name
System.out.println(EnumSingleton.INSTANCE.name()); // INSTANCE
----- Enum.valueOf((Class)cl, name);
Enum :
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name); // 是从这里取取来的。
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
----------- T result = enumType.enumConstantDirectory().get(name);
Class<T> enumType:
Map<String, T> enumConstantDirectory() {
if (enumConstantDirectory == null) {
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
Map<String, T> m = new HashMap<>(2 * universe.length);
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant); // 保存 枚举对象
enumConstantDirectory = m;
}
return enumConstantDirectory;
}
// 该map 不能被序列化
private volatile transient Map<String, T> enumConstantDirectory = null;
// 到这里我们也就看到了 原来 他还是一个 map 集合,map 里面就保存了枚举对象,在被调用的时候,就判断一下,如果是空,就存储枚举对象,然后返回,然后通过name 取获取,每次都是获取到的一个 对象,这个也叫做 注册式单例,spring 就是典型的注册式单例。
上面我们分析了 为啥反序列化得时候 ,得到的也是相同的枚举对象,就是要因为 jvm 自己讲枚举对象存在了一个map 集合里面,然后每次都是去 map 里面取,对象也就只创建了一次,后面都是 读出来的自然也就 是单例了,这也就 注册式单例。到这里我们就分析完了,枚举能够防止反射和序列化破坏的原因了。
4.4 ThreadLocal 单例
上面枚举单例里面提及到了注册式单例,现在我们来看看另外一种注册式单例--ThreadLocal 单例。
- ThreadLocalSingleton
public class ThreadLocalSingleton {
private ThreadLocalSingleton () {}
private static final ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {//初始化值
return new ThreadLocalSingleton();
}
};
public static ThreadLocalSingleton getInstance() {
return threadLocal.get();
}
}
ThreadLocal 本身的特点是变量和线程存在绑定的映射关系,我们先来看测试结果,就明白是啥意思了。为了方便理解ThreadLocal 的特点,我们使用线程池来测试。
- 线程池测试
public static void test() throws InterruptedException {
// 创建有5个线程的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0 ;i<10; i++) {// 开启10 个线程,有线程池去处理。
executorService.submit(()->{
countDownLatch.countDown();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + "___" + instance);
});
}
countDownLatch.await();
}
- 测试结果
pool-1-thread-5___com.example.designpattern.singleton.ThreadLocalSingleton@6c1553f
pool-1-thread-1___com.example.designpattern.singleton.ThreadLocalSingleton@6f08d27f
pool-1-thread-5___com.example.designpattern.singleton.ThreadLocalSingleton@6c1553f
pool-1-thread-3___com.example.designpattern.singleton.ThreadLocalSingleton@3f46008f
pool-1-thread-2___com.example.designpattern.singleton.ThreadLocalSingleton@d85fdfa
pool-1-thread-4___com.example.designpattern.singleton.ThreadLocalSingleton@447a380d
pool-1-thread-5___com.example.designpattern.singleton.ThreadLocalSingleton@6c1553f
pool-1-thread-3___com.example.designpattern.singleton.ThreadLocalSingleton@3f46008f
pool-1-thread-1___com.example.designpattern.singleton.ThreadLocalSingleton@6f08d27f
pool-1-thread-2___com.example.designpattern.singleton.ThreadLocalSingleton@d85fdfa
/**
* 注册式单例
*/
public class RegisterSingleton {
private static Map<String,RegisterSingleton> map = new ConcurrentHashMap<>(1);
private RegisterSingleton() {}
private static volatile RegisterSingleton instance;
public static RegisterSingleton getInstance() {
instance = map.get("instance");
if (null == instance) {
synchronized (RegisterSingleton.class) {
instance = map.get("instance");
if (null == instance) {
instance = new RegisterSingleton();
map.put("instance",instance);
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i=0;i<100; i++) {
new Thread(()->{
RegisterSingleton instance = RegisterSingleton.getInstance();
System.out.println(instance);
}).start();
}
}
}
我们观察相同线程 获取到的对象是一样的,这个就是ThreadLocal 本身的特点,也就说 同一个线程获取到的对象始终是一个,对单个线程来说 这也就是单例了。但是对于不同的线程来说 是获取到不同的对象。这个和ThreadLocal 本身的数据结构有关系。我们可以去看一看jdk 的源码,这里我们就不展开了,内部是维护了一个 ThreadLocalMap 静态内部来存储当前线程的值,所以每个线程都有一个ThreadLocalMap 对象与之对应,获取到值也只自己线程的。到此,我们可以总结出一个结论:注册式单例就是 对象创建一次,然后存放到 Map 中,后面去Map 里面直接获取就ok。
上面我们说了注册式单例和 ThreadLocal 的单例,注册式单例的实现方式有很多种,但是唯一不变的就是 底层的数据结构一定是 Map 的,然后保证 在访问Map 的时候 是线程安全的就行。最后介绍一种CAS实现的 单例。
- CAS 单例--原子引用类
public class CASSingleton {
private CASSingleton() {
}
private final static AtomicReference<CASSingleton> atomicReference = new AtomicReference<>();
public static CASSingleton getInstance() {
for (; ; ) {//自旋
CASSingleton casSingleton = atomicReference.get();
if (casSingleton != null) {
return casSingleton;
}
casSingleton = new CASSingleton();
// CSA 操作:如果当前的值是 null,就更新 为 casSingleton
boolean compareAndSet = atomicReference.compareAndSet(null, casSingleton);
if (compareAndSet) {
return casSingleton;
}
}
}
- 测试:
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
CASSingleton instance = CASSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + instance);
}).start();
}
CASSingleton instance = CASSingleton.getInstance();
CASSingleton instance2 = CASSingleton.getInstance();
CASSingleton instance3 = CASSingleton.getInstance();
CASSingleton instance4 = CASSingleton.getInstance();
System.out.println(instance);
System.out.println(instance2);
System.out.println(instance3);
System.out.println(instance4);
}
com.example.designpattern.singleton.CASSingleton@52cc8049
com.example.designpattern.singleton.CASSingleton@52cc8049
com.example.designpattern.singleton.CASSingleton@52cc8049
com.example.designpattern.singleton.CASSingleton@52cc8049
Thread-1:com.example.designpattern.singleton.CASSingleton@52cc8049
Thread-0:com.example.designpattern.singleton.CASSingleton@52cc8049
Thread-3:com.example.designpattern.singleton.CASSingleton@52cc8049
Thread-2:com.example.designpattern.singleton.CASSingleton@52cc8049
Thread-4:com.example.designpattern.singleton.CASSingleton@52cc8049
有关CAS 相关知识如果不熟悉的话,可以去学习一下并发编程相关的知识。
总结一下:
到此就介绍了单例的常见的实现方式:double check ,静态内部类,枚举,饿汉式,ThreadLocal,CAS 单例,注册式单例,当然肯定还有其他的变种写法,但是根本的原则不会改变-- JVM 中整个生命周期中只存在一个对象实例。同时,也分析了单例被破坏的情况,反射和序列化。当然项目中不会故意去破坏,但是无意的破坏是可能的,比如反射破坏。好了,这就是笔者对单例模式的理解,如果不足之处,欢迎留言讨论!