单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
单例模式常用的两种:饿汉式单例模式;懒汉式单例模式。
1.饿汉式单例模式:
顾名思义饿汉式单例模式是说在类加载的时候就已经将类给初始化在内存中,从而后续的调用的都是从内存中获取这一个类的初始化实例对象,所有引用的都指向同一个地址。
饿汉式单例模式:
/**
* @author:
* @date: 2022/2/21 15:45
* @description: 饿汉式单例模式:类加载的时候就会自动构造产生一个单例对象
*/
public class HungrySingleton {
/**
* 定义类属性
*/
private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
/**
* 私有化构造方法
*/
private HungrySingleton() {
}
/**
* 对外暴露拿到实例的方法
* @return
*/
public static HungrySingleton getInstance(){
return HUNGRY_SINGLETON;
}
}
单线程测试类:
public class Test {
public static void main(String[] args) throws Exception {
HungrySingleton singleton1 = HungrySingleton.getInstance();
HungrySingleton singleton2 = HungrySingleton.getInstance();
System.out.println(singleton1);
System.out.println(singleton2);
}
}
打印结果:
design.singleton.HungrySingleton@2b193f2d
design.singleton.HungrySingleton@2b193f2d
Process finished with exit code 0
多线程测试:
*/
public class Test {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 1000; i++) {
new Thread(()->{
Test.println();
}).start();
countDownLatch.await();
}
countDownLatch.countDown();
}
private static void println(){
System.out.println(HungrySingleton.getInstance());
}
}
多线程测试结果:

两种测试结果(多线程的可以多测试几次),返回的结果都是一样的,说明饿汉式单例模式是线程安全的。
2.懒汉式单例模式:
在类加载时,就给该类创建一个可用的内存空间,但是具体的实例对象需要在使用时自己用new的方式初始化一次,之后所有其他引用的实例地址均指向第一次new的地址。
/**
* @author:
* @date: 2022/2/21 18:09
* @description: 懒汉式单例模式
*/
public class LazyUnSafeSingleton {
/**
* 类成员属性
*/
private static LazyUnSafeSingleton instance;
/**
* 私有化构造方法
*/
private LazyUnSafeSingleton() {
}
/**
* 对外暴露拿到实例的方法
* @return
*/
public static LazyUnSafeSingleton getInstance() {
if (instance == null) {
instance = new LazyUnSafeSingleton();
}
return instance;
}
}
一般测试:
public class Test {
public static void main(String[] args) throws Exception {
LazyUnSafeSingleton singleton1 = LazyUnSafeSingleton.getInstance();
LazyUnSafeSingleton singleton2 = LazyUnSafeSingleton.getInstance();
System.out.println(singleton1);
System.out.println(singleton2);
}
}
一般测试结果:
design.singleton.LazyUnSafeSingleton@2b193f2d
design.singleton.LazyUnSafeSingleton@2b193f2d
Process finished with exit code 0
可以看出singleton1与singleton2的地址是一样的,两次实例的对象引用地址一致。但是第一次引用时,才开始new一个实例,之前在类加载过程中并未直接实例化。
多线程测试:
public class Test {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 1000; i++) {
new Thread(()->{
Test.println();
}).start();
countDownLatch.await();
}
countDownLatch.countDown();
}
private static void println(){
System.out.println(LazyUnSafeSingleton.getInstance());
}
}
多线程测试结果:

一般测试没问题,多线程测试出现了对象不一样的情况!,说明多线程下的一般懒汉单例模式属于线程不安全的。
一般的懒汉式单例模式优化后
演化成双重检查锁单例模式(DCL)
/**
* @author:
* @date: 2022/2/21 19:25
* @description:
*/
public class LazySafeSingleton {
/**
* 类成员属性
*/
private static volatile LazySafeSingleton instance;
/**
* 私有化构造方法
*/
private LazySafeSingleton() {
}
/**
* 对外暴露拿到实例的方法
* @return
*/
public static LazySafeSingleton getInstance() {
if (instance == null) {
synchronized (LazySafeSingleton.class) {
if (instance == null) {
instance = new LazySafeSingleton();
}
}
}
return instance;
}
}
多线程测试:
public class Test {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 1000; i++) {
new Thread(()->{
Test.println();
}).start();
countDownLatch.await();
}
countDownLatch.countDown();
}
private static void println(){
System.out.println(LazySafeSingleton.getInstance());
}
}
多线程测试结果:

可以看到结果如我们的预期,大家可以多次执行看看会不会出现不一样的对象。
这两种单例真的就不能被创建出多例吗?
我们尝试用反射去创建对象,看看效果
代码:
public class Test {
public static void main(String[] args) throws Exception {
// 通过单例模式拿到对象
HungrySingleton singleton1 = HungrySingleton.getInstance();
// 通过反射创建对象
Class singletonClass = HungrySingleton.class;
Constructor[] cts = singletonClass.getDeclaredConstructors();
// 访问权限打开setAccessible(true),就可以访问私有构造函数
cts[0].setAccessible(true);
HungrySingleton singleton2 = (HungrySingleton)cts[0].newInstance();
System.out.println(singleton1);
System.out.println(singleton2);
}
}
执行结果:
design.singleton.HungrySingleton@2b193f2d
design.singleton.HungrySingleton@355da254
Process finished with exit code 0
两种方式拿到的对象地址不一样,说明不是一个对象。
结论:
一般的饿汉式和懒汉式单例模式都是可以被反射给破坏的。
如何解决?
可以使用 枚举类单例模式 解决这个问题
枚举类单例模式:
/**
* @author:
* @date: 2022/2/21 15:45
* @description: 枚举式单例模式
*/
public enum EnumSingleton {
/**
* 枚举自身
*/
INSTANCE;
/**
* 做业务逻辑
* @return
*/
public void doSomeThing(){
System.out.println("实现你这个类需要做的一些事");
}
}
测试类:
/**
* @author: dingshitai
* @date: 2022/2/21 15:50
* @description:
*/
public class Test {
public static void main(String[] args) throws Exception {
// 只能通过这种方式去调用类里面的方法
EnumSingleton.INSTANCE.doSomeThing();
}
可以看出我们使用了枚举类单例模式,只能用这种方式去调用类内部的方法。
那可以被反射破坏吗?
不能。
上代码:
public class Test {
public static void main(String[] args) throws Exception {
// 通过反射创建对象
Class EnumSingletonClass = EnumSingleton.class;
Constructor[] cts = EnumSingletonClass.getDeclaredConstructors();
// 访问权限打开setAccessible(true),就可以访问私有构造函数
cts[0].setAccessible(true);
EnumSingleton singleton1 = (EnumSingleton) cts[0].newInstance();
System.out.println(singleton1);
}
}
测试结果:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at design.singleton.Test.main(Test.java:18)
Process finished with exit code 1
枚举类是不能被反射创建出实例对象的!!
为啥呢?
原因就在这行
EnumSingleton singleton1 = (EnumSingleton) cts[0].newInstance();
进入 newInstance() 方法中
可以看到反射的源码:

类对象类型是ENUM类型的时候,会直接报出错误,从而阻止反射生成实例化对象,这就是最根本的原因。
所以如果想要用到单例模式,既考虑到安全性又考虑到并发性,建议使用枚举类单例模式。
最后聊一聊spring源码中用到单例模式的地方
在DefaultSingletonBeanRegistry这个注册类中去根据beanName生成对象的时候

spring用到了单例模式。
网友评论