一、单例模式概述
1.1、概述
-
概述
单例模式,是设计模式中最常见的模式之一,它是一种创建对象模式,用于产生一个对象的具体实例,可以确保系统中
一个类只会产生一个实例
。 -
优缺点
【优点】
1、对于
频繁使用的对象
,可以省去 new 操作花费的时间
,尤其对那些重量级对象而言,削减了一笔非常客观的系统开销。2、由于
new 操作的次数减少
,因而对系统内存的使用频率也会降低
,从而减轻 GC 压力
,缩短 GC 停顿时间
。【缺点】
1、单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
2、在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
3、单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
-
应用场景
- Spring中创建的 Bean 实例默认都是单例。
- 数据库连接池的设计与实现。
- 多线程的线程池设计与实现。
-
核心实现点
构造方法私有化
1.2、分类
单例模式常见分为两大类:饿汉式、懒汉式
1.2.1、饿汉式
JVM 对**
类加载的时候
**,单例对象就会被创建,需要用的时候直接拿过来用就好了
-
优缺点
【优点】
获取对象的速度快
,线程安全
【缺点】
耗内存
(初始化时就讲对象实例化了,占用了内存) -
为什么饿汉式是线程安全的?
JVM 对类加载的时候,单例对象就会被创建,类加载的过程是线程安全的。
1.2.2、懒汉式
先不急着创建对象,在需要**
使用时候再去创建
**。
-
优缺点
【优点】
只有在使用时才被实例化,
节省了资源
【缺点】
线程不安全
二、常见写法
2.1、饿汉式
线程安全
反射无法破坏单例
public class HungryDemo {
/**
* 如果对象很大,一加载对象时就实例化,会浪费空间
*/
private final byte[] data1 = new byte[1024 * 1024];
private final byte[] data2 = new byte[1024 * 1024];
/**
* 私有化构造器
*/
private HungryDemo() {
System.out.println("create Singleton");
}
/**
* 类加载时就创建对象
*/
private static final HungryDemo HUNGRY = new HungryDemo();
public static HungryDemo getInstance(){
return HUNGRY;
}
}
复制
2.2、懒汉式
线程不安全
反射能破坏单例
2.2.1、实现
public class LazyDemo {
/**
* 私有化构造器
*/
private LazyDemo() {
System.out.println("create Singleton");
}
/**
* 不直接创建对象
*/
private static LazyDemo lazyDemo;
public static LazyDemo getInstance() {
if (lazyDemo == null) {
lazyDemo = new LazyDemo();
}
return lazyDemo;
}
}
复制
2.2.2、线程不安全测试
public class LazyDemo {
/**
* 私有化构造器
*/
private LazyDemo() {
System.out.println(Thread.currentThread().getName() + "create Singleton");
}
/**
* 不直接创建对象
*/
private static LazyDemo lazyDemo;
public static LazyDemo getInstance() {
if (lazyDemo == null) {
lazyDemo = new LazyDemo();
}
return lazyDemo;
}
/**
* 多线程并发测试
* @param args args
*/
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
LazyDemo.getInstance();
}).start();
}
}
}
复制
上面测试结果如下:
Thread-0create Singleton Thread-1create Singleton
复制
可看出,创建了2个对象,即单例模式被多线程破坏了
2.3、双重检查加锁(DCL)
线程安全
反射能破坏单例
3.3.1、实现
public class DCLDemo {
/**
* 私有化构造器
*/
private DCLDemo() {
System.out.println(Thread.currentThread().getName() + "create Singleton");
}
/**
* 不直接创建对象
* 此处必须加volatile
*/
private volatile static DCLDemo dclDemo;
public static DCLDemo getInstance() {
// 1重校验
if (dclDemo == null) {
// 对DCLDemo加锁
synchronized (DCLDemo.class) {
// 2重校验
if (dclDemo == null) {
// 创建对象
dclDemo = new DCLDemo();
}
}
}
return dclDemo;
}
/**
* 多线程并发测试
* @param args args
*/
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
DCLDemo.getInstance();
}).start();
}
}
}
复制
📢注意点:
必须加volatile
在创建对象那一步,实际上是三步操作:
(1)分配内存空间
(2)执行构造方法,初始化对象
(3)把对象指向这个空间
在此过程中,可能会发生指令重排
,从而变成1、3、2这样的一个步骤,
在多线程下,比如线程A执行了1、3步骤,此时线程B进来了,经过判定,dclDemo != null,那么将会跳过创建对象,直接return,而此时第2步还没执行(dclDemo还没完成构造),那么就会返回一个空的内存空间。
3.3.2、反射破坏
使用反射,可以获得两个对象
public class DCLDemo {
/**
* 私有化构造器
*/
private DCLDemo() {
System.out.println(Thread.currentThread().getName() + "create Singleton");
}
/**
* 不直接创建对象
* 此处必须加volatile
*/
private volatile static DCLDemo dclDemo;
public static DCLDemo getInstance() {
// 1重校验
if (dclDemo == null) {
// 对DCLDemo加锁
synchronized (DCLDemo.class) {
// 2重校验
if (dclDemo == null) {
// 创建对象
dclDemo = new DCLDemo();
}
}
}
return dclDemo;
}
/**
* 反射破解DCL
* @param args args
*/
public static void main(String[] args) throws Exception {
DCLDemo dclDemo1 = DCLDemo.getInstance();
Constructor<DCLDemo> declaredConstructor = DCLDemo.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
DCLDemo dclDemo2 = declaredConstructor.newInstance();
System.out.println(dclDemo1 == dclDemo2); // fasle
}
}
复制
3.3.3、进阶实现
在构造器里加锁和判断,当对象已经存在时,就不能通过构造器构造新的对象了
public class DCLDemo {
/**
* 私有化构造器
*/
private DCLDemo() {
synchronized (DCLDemo.class) {
if (dclDemo != null) {
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
System.out.println(Thread.currentThread().getName() + "create Singleton");
}
......
}
复制
报错如下:
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.demo1.test.singleton.DCLDemo.main(DCLDemo.java:52)
Caused by: java.lang.RuntimeException: 不要试图使用反射破坏异常
at com.example.demo1.test.singleton.DCLDemo.<init>(DCLDemo.java:18)
... 5 more
复制
3.3.4、反射进阶破坏
不创建对象,两个对象都是通过getDeclaredConstructor获取
public class DCLDemo {
......
/**
* 反射破解DCL
* @param args args
*/
public static void main(String[] args) throws Exception {
Constructor<DCLDemo> declaredConstructor = DCLDemo.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
DCLDemo dclDemo1 = declaredConstructor.newInstance();
DCLDemo dclDemo2 = declaredConstructor.newInstance();
System.out.println(dclDemo1 == dclDemo2); // fasle
}
}
复制
3.3.5、再次进阶实现
使用一个标志变量,不通过反编译的方式,是无法破解这个标志变量名和值的。
public class DCLDemo {
private static boolean flag = false;
/**
* 私有化构造器
*/
private DCLDemo() {
synchronized (DCLDemo.class) {
if (!flag) {
flag = true;
} else {
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
System.out.println(Thread.currentThread().getName() + "create Singleton");
}
.......
}
复制
报错如下:
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.demo1.test.singleton.DCLDemo.main(DCLDemo.java:58)
Caused by: java.lang.RuntimeException: 不要试图使用反射破坏异常
at com.example.demo1.test.singleton.DCLDemo.<init>(DCLDemo.java:23)
... 5 more
复制
3.3.6、反射再次进阶破坏
如果被反编译或者泄露了,那还是可以通过反射破坏
public class DCLDemo {
......
/**
* 反射破解DCL
* @param args args
*/
public static void main(String[] args) throws Exception {
// 获取字段并破坏私有权限
Field flag = DCLDemo.class.getDeclaredField("flag");
flag.setAccessible(true);
Constructor<DCLDemo> declaredConstructor = DCLDemo.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
DCLDemo dclDemo1 = declaredConstructor.newInstance();
// 获得第一个对象之后,再把flag值改回来
flag.set(dclDemo1, false);
DCLDemo dclDemo2 = declaredConstructor.newInstance();
System.out.println(dclDemo1 == dclDemo2); // fasle
}
}
复制
2.4、静态内部类
线程安全
反射能破坏单例
public class OuterClassDemo {
/**
* 构造器私有
*/
private OuterClassDemo() {
System.out.println(Thread.currentThread().getName() + "create Singleton");
}
/**
* 匿名内部类
*/
public static class InnerClass {
private static final OuterClassDemo outerClassDemo = new OuterClassDemo();
}
public static OuterClassDemo getInstance() {
return InnerClass.outerClassDemo;
}
/**
* 多线程并发测试
* @param args args
*/
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
OuterClassDemo.getInstance();
}).start();
}
}
}
复制
2.5、枚举
线程安全
反射不能破坏单例
/**
* 枚举类
*/
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
/**
* 测试反射破坏
*/
class Test{
public static void main(String[] args) throws Exception {
EnumSingleton instance1 = EnumSingleton.INSTANCE;
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
// 破坏私有权限
declaredConstructor.setAccessible(true);
EnumSingleton instance2 = declaredConstructor.newInstance();
System.out.println(instance1 == instance2);
}
}
复制
反射破坏会报错:Cannot reflectively create enum objects
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.example.demo1.test.singleton.Test.main(EnumSingleton.java:32)
复制
三、总结
-
单例模式需要处理的问题
- 将构造函数私有化
- 通过静态方法获取一个唯一实例
- 保证线程安全
- 防止反序列化造成的新实例等。
-
常见实现方式对比
在实际工作中,单例的使用还是比较常见的,在几种实现方式中,双重检测机制、静态内部类、枚举方式都是比较推荐。
单例模式实现方式 是否线程安全 是否懒加载 是否能防止反射破坏 饿汉式 ✅ ✅ 懒汉式 ✅ 双重锁检测 ✅ ✅ 静态内部类 ✅ ✅ 枚举 ✅ ✅ -
涉及到的知识点
单例模式
static修饰符、synchronized修饰符(加锁)、volatile修饰符(防止指令重排)、enum等
这里的每一个知识点都可以变成面试官下手的考点,而单例只是作为一个引子