一、概述
单例模式可能是大家听到的最多的模式,但是你真的了解了吗。单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。单例模式具有以下特点:
(1) 单例类只能有一个实例。
(2) 单例类必须自己创建自己的唯一实例。
(3) 单例类必须给所有其他对象提供这一实例。
在本博文中一步一步分析,深入剖析单例模式的演变过程。
二、源码分析
1. 饿汉式
饿汉式的字面含义就是很饿,所以呢必须一开始就准备好,所以符合这种场景的单例源码如下:
/**
* 饿汉式
* 当这个类第一次加载时,就创建一个实例对象instance
* 每次想要获取这个类的实例对象的时候就返回instance
* @author frr
*
*/
public class single01 {
private static single01 instance = new single01();
private single01() {}
//static修饰的方法可以在不创建实例的情况下直接用类名进行调用
public static single01 getInstance() {
return instance;
}
public static void main(String[] args) {
single01 temp1 = single01.getInstance();
single01 temp2 = single01.getInstance();
System.out.println(temp1==temp2);
}
}
当这个类第一次加载时,就创建一个实例对象instance,每次想要获取这个类的实例对象的时候就返回类加载时创建的实例对象instance。由main方法检验返回结果为true,表示创建的两个实例对象其实是同一个。这种写法是非常的简单实用,所以多数情况下可以采用这种方式。
2. 饱汉式(懒汉式)
与饿汉式的加载方式不同,类的实例化对象在需要用的时候才进行初始化,源码如下:
/**
* 饱汉式(懒汉式)
* 当需要创建此类的实例化对象时,才创建一个实例对象instance
* 每次想要获取这个类的实例对象的时候就返回instance
* @author frr
*
*/
public class single02 {
private static single02 instance = null;
private single02() {}
//static修饰的方法可以在不创建实例的情况下直接用类名进行调用
public static single02 getInstance() {
if(instance==null) {
//创建实例对象
instance = new single02();
}
return instance;
}
}
懒汉式表面上是节约了空间,因为刚加载类的时候不会创建此类的实例对象,但是却引发了多线程下的安全问题,我们利用main方法输出每个线程获取的实例化对象的hashCode来判断是否同一个对象(同一个类的不同实例对象的hashCode是不同的),测试代码如下:
/**
* 饱汉式(懒汉式)
* 当需要创建此类的实例化对象时,才创建一个实例对象instance
* @author frr
*
*/
public class single02 {
private static single02 instance = null;
private single02() {}
//static修饰的方法可以在不创建实例的情况下直接用类名进行调用
public static single02 getInstance() {
if(instance==null) {
//增加每个线程工作时间,放大实验结果
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
//创建实例对象
instance = new single02();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->
System.out.println(single02.getInstance().hashCode())
).start();
}
}
}
运行部分结果如下图:
我们可以看到,不同线程输出的实例对象的hashCode有时会不一致。原因是当一个线程在判断了instance为null但是还没来得调用构造方法创建实例对象时,另一个线程执行instance==null时,也是判断instance没有被创建,又调用了构造方法实例化了一个对象,就导致不同线程获取到的此类的实例化对象不一致。
3. 懒汉式-加synchronize关键字
那么我们如何保证线程安全呢,不用多说,首先想到synchronize关键字,调整后的源码如下:
package designPattern.single;
/**
* 饱汉式(懒汉式)
* 当需要创建此类的实例化对象时,才创建一个实例对象instance
* @author frr
*
*/
public class single03 {
private static single03 instance = null;
private single03() {}
//static修饰的方法可以在不创建实例的情况下直接用类名进行调用
public static synchronized single03 getInstance() {
if(instance==null) {
//增加每个线程工作时间
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
//创建实例对象
instance = new single03();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->
System.out.println(single03.getInstance().hashCode())
).start();
}
}
}
测试结果如下:
有结果可以看出,我们保证了每个线程访问拿到的都是同一个对象,解决了线程不安全的问题。但是如果看博主前面的博文你肯定会知道,synchronize关键字是非常重量级的,所以会导致效率下降。
4. 懒汉式-缩小synchronize范围
那么有没有那么一种写法,既能保证线程安全, 又能保证效率呢?首先想到的就是缩小synchronize的范围,也就是将synchronize写到判断条件里面,代码如下:
/**
* 饱汉式(懒汉式)
* 当需要创建此类的实例化对象时,才创建一个实例对象instance
* @author frr
*
*/
public class single04 {
private static single04 instance = null;
private single04() {}
//static修饰的方法可以在不创建实例的情况下直接用类名进行调用
public static single04 getInstance() {
if(instance==null) {
synchronized(single04.class) {
//增加每个线程工作时间
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
//创建实例对象
instance = new single04();
}
}
return instance;
}
//测试方法
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->
System.out.println(single04.getInstance().hashCode())
).start();
}
}
}
运行结果如下:
由结果我们可以看到这种写法也是存在线程不完全的问题,原因也是和懒汉式最开始的写法一样,都是在进行instance==null的判断后,另一个线程执行更快抢先创建了实例对象,导致了第一个线程得到的实例化对象不一致。
5. 双重检查DCL
有没有写法能够在保证线程安全的情况下,只对创建实例对象的代码进行加锁呢?答案就是双重检查,源码如下:
/**
* 饱汉式(懒汉式)
* 当需要创建此类的实例化对象时,才创建一个实例对象instance
* @author frr
*
*/
public class single04 {
private static single04 instance;
private single04() {}
//static修饰的方法可以在不创建实例的情况下直接用类名进行调用
public static single04 getInstance() {
if(instance==null) {
synchronized(single04.class) {
if(instance==null) {
//增加每个线程工作时间
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
//创建实例对象
instance = new single04();
}
}
}
return instance;
}
//测试方法
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->
System.out.println(single04.getInstance().hashCode())
).start();
}
}
}
运行结果如下:
从结果上来看,确实是能保证线程安全。但是事情真的这么简单吗?这种看似完美的写法其实存在问题,当CPU进行指令重排时,会导致有的线程获取到的是半初始化的对象,原理在《我的面试题(一):关于Object o = new Object()的追魂8连问!》中有详细解释。
6. DCL-加volatile关键字
针对上述情况,我们在做一些改进,就是在实例对象前加入volatile关键字进行修饰,源码如下:
/**
* 饱汉式(懒汉式)
* 当需要创建此类的实例化对象时,才创建一个实例对象instance
* @author frr
*
*/
public class single04 {
private volatile static single04 instance;
private single04() {}
//static修饰的方法可以在不创建实例的情况下直接用类名进行调用
public static single04 getInstance() {
if(instance==null) {
synchronized(single04.class) {
if(instance==null) {
//增加每个线程工作时间
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
//创建实例对象
instance = new single04();
}
}
}
return instance;
}
//测试方法
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->
System.out.println(single04.getInstance().hashCode())
).start();
}
}
}
原理就是volatile关键字有禁止指令重排的作用,如果你想更深入的了解volatile,那么请阅读《我的并发编程(三):Volatile的底层实现及原理》。
7. 静态内部类方式
静态内部类方式是利用JVM类加载的机制来保证单例。首先,每种类只会被JVM加载一次,第二次用的时候就直接用;第二点就是JVM加载外部类时不会加载内部类,所以就不会创建外部类的实例对象,以此实习懒加载。
/**
* 静态内部类方式
* JVM加载外部类时不会加载内部类,以此实习懒加载
* @author frr
*
*/
public class single06 {
private single06() {}
private static class single06Holder{
private final static single06 instance = new single06();
}
public static single06 getInstance() {
return single06Holder.instance;
}
//测试方法
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->
System.out.println(single06.getInstance().hashCode())
).start();
}
}
}
8. 枚举单例
除了上诉的饱汉式与懒汉式,还有一种最完美的写法,源码如下:
/**
* 枚举的方式,既能保证线程安全,也能防止反序列化
* @author frr
*
*/
public enum single07 {
instance;
//测试方法
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->
System.out.println(single07.instance.hashCode())
).start();
}
}
}
这种方式,既能保证线程安全,也能防止反序列化,原因如下:
(1) 枚举里面的instance始终只有一个,所以保证线程安全。
(2) 枚举没有构造方法,所以不能通过反序列化获取instance。
三、总结
总结起来就是一句话,单例模式中最常用的还是饱汉式,但是面试问得对多的是双重检查DCL,最完美的写法是枚举的方式。请期待下一篇《我的设计模式(二):Strategy 策略模式》。
更多精彩内容,敬请扫描下方二维码,关注我的微信公众号【Java觉浅】,获取第一时间更新哦!