单例模式的应用场景

​ 单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。 例如,公司 CEO、部门经理在一个公司只能存在一个等。在 J2EE 标准中,ServletContext、 ServletContextConfig 等;在 Spring 框架应用中 ApplicationContext;数据库的连接池也都是单例形式。单例模式可分为以下几种:

饿汉式单例

饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线 程还没出现以前就是实例化了,不可能存在访问安全问题。

  • 优点: 没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
  • 缺点: 类加载的时候就初始化,不管用与不用都占着空间,浪费了内存。

饿汉式写法简单,试用与单例对象少的情况,Spring 中 IOC 容器 ApplicationContext 本身就是典型的饿汉式单例。

  • 写法一
1
2
3
4
5
6
7
8
9
10
public class HungrySingleton {

public static final HungrySingleton INSTANCE = new HungrySingleton();

private HungrySingleton() {}

public static HungrySingleton getInstance() {
return INSTANCE;
}
}
  • 写法二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class HungryStaticSingleton {

public static final HungryStaticSingleton INSTANCE;

static {
INSTANCE = new HungryStaticSingleton();
}

private HungryStaticSingleton() {}

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

懒汉式单例

懒汉式单例在被外部类调用的时候内部类才会加载

  • 懒汉式单例简单实现

缺点:不加synchronized可能会出现线程安全问题,但是加上synchronized关键字以后,在线程数量比较多情况下,如果 CPU 分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class SimpleLazySingleton {

public static SimpleLazySingleton INSTANCE = null;

private SimpleLazySingleton() {
}

/**
* 线程不安全
*
* @return
*/
// public static SimpleLazySingleton getInstance() {
// if (INSTANCE == null) {
// INSTANCE = new SimpleLazySingleton();
// }
// return INSTANCE;
// }

/**
* 线程安全,虽然jdk对synchronized有优化,但是仍有一定的性能问题
* @return
*/
public synchronized static SimpleLazySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new SimpleLazySingleton();
}
return INSTANCE;
}
}
  • 双重校验锁实现单例

比上一个方法稍微好点,但是因为加了锁,性能仍有问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DoubleCheckLazySingleton {

// 防止指令重排序
public volatile static DoubleCheckLazySingleton INSTANCE = null;

private DoubleCheckLazySingleton() {}

/**
* 适中方案,双重校验锁
* @return
*/
public static DoubleCheckLazySingleton getInstance() {
if (INSTANCE == null) {
synchronized (DoubleCheckLazySingleton.class) {
if (INSTANCE == null) {
INSTANCE = new DoubleCheckLazySingleton();
}
}
}
return INSTANCE;
}
}
  • 静态内部类实现单例

这种形式兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class InnerClassLazySingleton {

// 使用的时候,会先初始化内部类
// 不使用不加载内部类
private InnerClassLazySingleton() {}

/**
* LazyHolder里面的方法要等到外部方法调用以后才执行
* 巧妙利用了内部类的特性
* JVM底层执行逻辑,完美避开了线程安全问题
* 每一个关键字都不是多余的
* static 是为了使单例的空间共享
* final 保证这个方法不会被重写,重载
* @return
*/
public static final InnerClassLazySingleton getInstance() {
//在返回结果以前,一定会先加载内部类
return LazyHolder.INSTANCE;
}

/**
* 默认不加载
*/
public static class LazyHolder {
public static final InnerClassLazySingleton INSTANCE = new InnerClassLazySingleton();
}
}

这种形式兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题。内部类一定是要在方 法调用之前初始化,巧妙地避免了线程安全问题。

单例被破坏

反射破坏单例

上述所有的单例模式的构造方法除了加上 private 以外,没有做任何处 理。如果我们使用反射来调用其构造方法,然后,再调用 getInstance()方法,应该就会两个不同的实例。

  • 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class InnerClassSingletonTest {

public static void main(String[] args) {
try {
// 反射会破坏单例
Class<?> clazz = InnerClassLazySingleton.class;
// 通过反射拿到私有的构造方法
Constructor<?> constructor = clazz.getDeclaredConstructor(null);
// 强制访问
constructor.setAccessible(true);
// 包里初始化
Object o = constructor.newInstance();
// 利用单例初始化
InnerClassLazySingleton o2 = InnerClassLazySingleton.getInstance();
System.out.println(o);
System.out.println(o2);
System.out.println(o == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
  • 运行结果
1
2
3
com.design.pattern.singleton.lazy.InnerClassLazySingleton@776ec8df
com.design.pattern.singleton.lazy.InnerClassLazySingleton@4eec7777
false

此时你会发现两次对象并不是同一个,也就是单例被破坏了,此时,为了避免这种情况,我们需要在构造方法中做一下限制,仍以静态内部类演示,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class InnerClassLazySingleton {

// 使用的时候,会先初始化内部类
// 不使用不加载内部类
private InnerClassLazySingleton() {
if (LazyHolder.INSTANCE != null) {
throw new RuntimeException("不允许重复创建");
}
}

/**
* LazyHolder里面的方法要等到外部方法调用以后才执行
* 巧妙利用了内部类的特性
* JVM底层执行逻辑,完美避开了线程安全问题
* 每一个关键字都不是多余的
* static 是为了使单例的空间共享
* final 保证这个方法不会被重写,重载
* @return
*/
public static final InnerClassLazySingleton getInstance() {
//在返回结果以前,一定会先加载内部类
return LazyHolder.INSTANCE;
}

/**
* 默认不加载
*/
public static class LazyHolder {
public static final InnerClassLazySingleton INSTANCE = new InnerClassLazySingleton();
}
}

再次运行测试类,则出现以下异常:

image-20200917201455658

序列化破坏单例

当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时 再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存, 即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例。来看一段演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SerializableSingleton implements Serializable {
//序列化就是说把内存中的状态通过转换成字节码的形式
//从而转换一个IO流,写入到其他地方(可以是磁盘、网络IO)
//内存中状态给永久保存下来了

//反序列化
//讲已经持久化的字节码内容,转换为IO流
//通过IO流的读取,进而将读取的内容转换为Java对象
//在转换过程中会重新创建对象new

public final static SerializableSingleton INSTANCE = new SerializableSingleton();

private SerializableSingleton() {}

public static SerializableSingleton getInstance() {
return INSTANCE;
}
}
  • 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SerializableSingletonTest {
public static void main(String[] args) {
SerializableSingleton s1 = null;
SerializableSingleton s2 = SerializableSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("SeriableSingleton.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();

FileInputStream fis = new FileInputStream("SeriableSingleton.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SerializableSingleton) ois.readObject();
ois.close();

System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
  • 运行结果
1
2
3
com.design.pattern.singleton.seriable.SerializableSingleton@306a30c7
com.design.pattern.singleton.seriable.SerializableSingleton@6d311334
false

运行结果中,可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两 次,违背了单例的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例?其实很简单,只需要增加**readResolve()**方法即可。修改后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class SerializableSingleton implements Serializable {

//序列化就是说把内存中的状态通过转换成字节码的形式
//从而转换一个IO流,写入到其他地方(可以是磁盘、网络IO)
//内存中状态给永久保存下来了

//反序列化
//讲已经持久化的字节码内容,转换为IO流
//通过IO流的读取,进而将读取的内容转换为Java对象
//在转换过程中会重新创建对象new

public final static SerializableSingleton INSTANCE = new SerializableSingleton();

private SerializableSingleton() {}

public static SerializableSingleton getInstance() {
return INSTANCE;
}

/**
* 加了可以避免反序列化破坏单例
*
* @return
*/
private Object readResolve() {
return INSTANCE;
}
}

再次运行测试类,得出如下结果:

1
2
3
com.design.pattern.singleton.seriable.SerializableSingleton@6d311334
com.design.pattern.singleton.seriable.SerializableSingleton@6d311334
true

此时我们发现反序列化后的对象和手动创建的对象是同一个对象(源码分析待补充)。虽然,增加 readResolve()方法返回实例,解决了单 例被破坏的问题。但是,我们通过分析源码以及调试,我们可以看到实际上实例化了两次,只不过新创建的对象没有被返回而已。那如果,创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大,难道真的就没办法从根本上解决问题吗?下面我们来注册式单例也许能帮助到你。

注册式单例

注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标 识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。

  • 枚举式单例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
}

此时用反序列化方式再进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class EnumSingletonTest {
public static void main(String[] args) {

EnumSingleton s1 = null;
EnumSingleton s2 = EnumSingleton.getInstance();
s2.setData(new Object());
FileOutputStream fos = null;
try {
fos = new FileOutputStream("EnumSingleton.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();


FileInputStream fis = new FileInputStream("EnumSingleton.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (EnumSingleton) ois.readObject();
ois.close();

System.out.println(s1.getData());
System.out.println(s2.getData());
System.out.println(s1.getData() == s2.getData());

} catch (Exception e) {
e.printStackTrace();
}
}
}
  • 运行结果
1
2
3
java.lang.Object@b81eda8
java.lang.Object@b81eda8
true

通过结果可以发现枚举式单例能完美避开反序列化带来的问题。

我们再用反射来进行测试:

1
2
3
4
5
6
7
8
9
10
11
public class EnumSingletonTest {
public static void main(String[] args) {
try {
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor();
c.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}

则会得到如下异常:

image-20200917204701199

报的是 java.lang.NoSuchMethodException 异常,意思是没找到无参的构造方法。打开JDK中的Enum类,我们只看到了一个这样的构造方法:

1
2
3
4
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}

那我们再来做一个这样的测试:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
try {
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor(String.class, int.class);
c.setAccessible(true);
EnumSingleton enumSingleton = (EnumSingleton) c.newInstance("Test", 1234);
} catch (Exception e) {
e.printStackTrace();
}
}

运行结果如下:

image-20200918095744482

这时错误已经非常明显了,告诉我们 Cannot reflectively create enum objects,不能 用反射来创建枚举类型。我们进入Constructor中的neInstance()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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;
}

在 newInstance()方法中做了强制性的判断,如果修饰符是 Modifier.ENUM 枚举类型, 直接抛出异常。

  • 容器缓存式单例

容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的

  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ContainerSingleton {
private ContainerSingleton() {}

private static Map<String, Object> ioc = new ConcurrentHashMap<>();

public static Object getInstance(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);
}
}
}
}

ThreadLocal 线程单例

ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @author D丶Cheng
* @Description: ThreadLocal单例
* 缺点:伪线程安全,线程单例
* 注:ThreadLocal的get方法中,有一个ThreadLocalMap,它set的时候用线程作为key,object作为值,get的时候其实是在当前线程 getValue。
* 可以用它来实现多数据源动态切换。
* @date 2020/9/16 4:48 下午
*/
public class ThreadLocalSingleton {

private ThreadLocalSingleton() {
}

private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};


public static ThreadLocalSingleton getInstance() {
return threadLocalInstance.get();
}
}
  • 简单测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ThreadLocalSingletonTest {

public static void main(String[] args) {
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println("end");

new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println("end");
}).start();

new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println("end");
}).start();
}
}
  • 运行结果
1
2
3
4
5
6
7
8
9
10
11
12
com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@24d46ca6
com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@24d46ca6
com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@24d46ca6
end
Thread-0:com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@4082029
Thread-0:com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@4082029
Thread-0:com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@4082029
end
Thread-1:com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@1e542a06
Thread-1:com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@1e542a06
Thread-1:com.design.pattern.singleton.threadlocal.ThreadLocalSingleton@1e542a06
end

通过运行结果可以发现,在主线程 main中无论调用多少次,获取到的实例都是同一个,都在两个子线程中也都分别获取到了不同的实例。那么 ThreadLocal 是如果实现这样的效果的呢?我们知道上面的单例模式为了达到线程安全的目的,给方法上锁,以时间换空间。ThreadLocal 将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以 空间换时间来实现线程间隔离的。