思想:
- 构造器私有
- 实例化的变量引用私有化
- 获取实例的方法共有
实现:
饿汉式
/**
* @author SHshuo
* @data 2021/8/23--9:51
* 构造器私有,声明为static静态,一上来就加载全部的资源
*/
public class hungryPattern {
// 构造器私有
private hungryPattern(){
}
// 类加载的时候初始化对象
public final static hungryPattern hungry = new hungryPattern();
// 提供对外接口
public static hungryPattern getInstance(){
return hungry;
}
}
DCL(双重检测模式double checked locking)懒汉式:
为什么双重检查 ?
- 第一个 if:如果不用第一个if判断,在多线程的情况下,所有的线程都会进行抢锁,也就是串行执行;
- 第二个 if:为了防止多个线程同时满足为 null 的条件,依次进入重复创建对象。
为什么使用 volatile ?
- 防止在创建对象的时候,将一个未初始化的对象引用暴露出来,从而导致不可预料的结果;
- 参考:https://cloud.tencent.com/developer/article/1905167
代码实现:
/**
* @author SHshuo
* @data 2021/8/23--9:56
* 构造器私有,需要用到的资源加载
* lazyPattern lazy = new lazyPattern();创建对象分为三步:
* 1.分配内存空间
* 2.执行构造函数、创建对象
* 3.将对象指向内存空间
*/
public class lazyPattern {
// 构造器私有
private lazyPattern(){
System.out.println(Thread.currentThread().getName());
}
// 先初始化对象
private volatile static lazyPattern lazy;
// 对外接口、用到了再创建、并加锁
public static lazyPattern getInstance(){
// 双重检测锁模式。DCL
if (lazy == null){
// 加锁、保证高并发
synchronized (lazyPattern.class){
// 为什么还要加if,因为第一个if不保证原子性
if(lazy == null){
// 防止创建对象的时候指令重排,用volatile修饰
lazy = new lazyPattern();
}
}
}
return lazy;
}
// 测试
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
getInstance();
}).start();
}
}
}
单例模式不安全,可以被反射破坏。通过反射机制调用私有构造器:
// 通过反射机制调用私有构造器 Constructor<reflexLazyPattern> declaredConstructor = reflexLazyPattern.class.getDeclaredConstructor(); declaredConstructor.setAccessible(true); // 创建对象(调用构造函数) reflexLazyPattern reflexLazyPattern = declaredConstructor.newInstance(); System.out.println(reflexLazyPattern);
反射源码分析:jvm内部不允许反射操作枚举。枚举类是天然地可以抵御序列化对单例的破坏和反射攻击的。
源码:
@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;
}
枚举实现单例模式,防止反射破坏,最优解。
/**
* @author SHshuo
* @data 2021/8/23--14:23
* enum是天生单例且线程安全的,这种线程安全是jvm层面的,因为jvm保证类加载是线程安全的。
* 其次,枚举序列化的时候用的是类名和class来找到唯一枚举对象,所以序列化也能保证单例。
* 最后反射enum的时候也有保证,不论是你反射用了无参的,还是enum(String.class,int.class),
* 底层都会被阻拦,因为如果是enum类型就会抛出异常,这是jdk层面的保护。
* <p>
* <p>
* 单元素的枚举类型已经成为实现Singleton的最佳方法
* -- 出自 《effective java》
*/
public enum EnumLazyPattern {
// 创建一个枚举对象、该对象天生单例
INSTANCE;
// 重写构造函数、构造函数默认是private
EnumLazyPattern() {
System.out.println(Thread.currentThread().getName());
}
// 提供对外接口
public EnumLazyPattern getInstance() {
return INSTANCE;
}
// 测试
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
EnumLazyPattern.INSTANCE.getInstance();
}).start();
}
}
}
补充:枚举实现单例模式:
1. enum是天生单例且线程安全的,这种线程安全是jvm层面的,因为jvm保证类加载是线程安全的。
2. 枚举序列化的时候用的是类名和class来找到唯一枚举对象,所以序列化也能保证单例。
3. 反射enum的时候也有保证,不论是你反射用了无参的,还是enum(String.class,int.class),底层都会被阻拦,因为如果是enum类型就会抛出异常,这是jdk层面的保护。
4. 单元素的枚举类型已经成为实现Singleton的最佳方法
-- 出自 《effective java》
5. 参考资料:
- https://www.jianshu.com/p/5647d1f598c7
- https://www.jianshu.com/p/d35f244f3770
- https://www.jianshu.com/p/9a8ea903b940
个人总结:
- 枚举实现单例模式是最优解,操作简单,自然解决了反射和序列化带来的问题。
- 说白了,就是将实例化变量私有private volatile static Class className ; 定义成INSTANCE。构造函数私有,对外接口更加简洁,直接返回INSTANCE即可。

京公网安备 11010502036488号