饿汉式 DCL懒汉式 ,探究!
饿汉式
java
package com.mystpet.dianli;
//假设你的项目非常庞大,JVM 启动时加载了这个 Hungry 类。因为它是“饿汉式”,所以随着类的加载,那个带有 4MB 累赘数据的 HUNGRY 实例就被立刻创建出来了,死死地占用了内存。
//但是! 如果你的程序在后续的运行过程中,从头到尾都没有调用过 Hungry.getInstance()(可能因为某些业务逻辑没走到这一步),那么这 4MB 的内存空间就被白白浪费了,直到程序结束才会被回收。
public class Hungry {
private byte[] data1=new byte[1024*1024];
private byte[] data2=new byte[1024*1024];
private byte[] data3=new byte[1024*1024];
private byte[] data4=new byte[1024*1024];
private Hungry(){
}
private final static Hungry HUNGRY=new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}懒汉式单例
java
public class LazyManV1 {
private static LazyManV1 lazyMan;
// 1. 私有化构造器,不让别人直接 new,保证单例只能由自己的方法创造
private LazyManV1() {
System.out.println(Thread.currentThread().getName() + " 创建了实例");
}
// 2. 提供获取实例的方法
public static LazyManV1 getInstance() {
if (lazyMan == null) {
lazyMan = new LazyManV1();
}
return lazyMan;
}
}缺点: 如果有两个线程同时运行到 if (lazyMan == null),它们都会觉得目前没有实例,然后各自 new 了一个。单例直接被破功。
java
public class LazyManV2 {
//必须加 volatile!防止指令重排导致的半初始化问题
private volatile static LazyManV2 lazyMan;
private LazyManV2() {
System.out.println(Thread.currentThread().getName() + " 创建了实例");
}
// 双重检测锁模式的 懒汉式单例 DCL懒汉式
public static LazyManV2 getInstance() { //静态方法,没有实例对象可以锁,只能锁全局唯一的类对象
if (lazyMan == null) { // 第一重检测
synchronized (LazyManV2.class) { // 加锁
if (lazyMan == null) { // 第二重检测
lazyMan = new LazyManV2(); // 此时才是安全的
}
}
}
return lazyMan;
}
}缺点: 虽然完美挡住了多线程,但黑客可以使LazyManV2.class.getDeclaredConstructor() 强行破解私有构造器,无限 new 新对象。
java
public class LazyManV3 {
private volatile static LazyManV3 instance;
// 【关键升级2】加入布尔值暗号,默认是 false
private static boolean flag = false;
private LazyManV3() {
synchronized (LazyManV3.class) {
if (flag == false) {
flag = true; // 第一次创建,把暗号改为 true
} else {
// 第二次还想通过反射进来?直接引爆!
throw new RuntimeException("不用试图使用反射破坏单例!");
}
}
}
public static LazyManV3 getInstance() {
if (instance == null) {
synchronized (LazyManV3.class) {
if (instance == null) {
instance = new LazyManV3();
/** * 1.分配内存空间
* 2.执行构造方法,初始化内存对象
* 3.把这个对象指向这个空间
*/
}
}
}
return instance;
}
// 反射!
public static void main(String[] args) throws Exception {
//LazyMan instance = LazyMan.getInstance();
Field flag = LazyMan.class.getDeclaredField("flag");
flag.setAccessible(true);
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); //定位到那个被私有化隐藏起来的构造器
declaredConstructor.setAccessible(true); //强行夺权
LazyMan instance = declaredConstructor.newInstance(); //创建对象
flag.set(instance, false); //拿到了类里面的私有变量 `flag`,同样用 `setAccessible(true)` 撬开它的锁,然后强行把内存里这个变量的值给改写了
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
}静态内部类
java
package com.mystpet.dianli;
// 静态内部类
public class Holder {
private Holder() {
}
public static Holder getInstance() {
return InnerClass.HOLDER;
}
public static class InnerClass { //静态内部类,调用才会加载
private static final Holder HOLDER = new Holder();
}
}单例保证: JVM 在底层规定死了:一个类的初始化过程,天生就是单线程互斥的。 当 10000 个线程同时触发去加载 InnerClass 时,JVM 会自动在底层挂上一把隐形的超级大锁,只放一个线程进去把 HOLDER 创建好,其他线程必须在外面干等。等创建好了,大家直接拿现成的就行。
缺点: 依然防不住反射
枚举
查看反射的newInstance()方法,发现不能对枚举类使用

测试
java
package com.mystpet.dianli;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
// enum 是一个什么? 本身也是一个Class类
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
EnumSingle instance1=EnumSingle.INSTANCE;
// 在这里依然用 getDeclaredConstructor(null),系统会直接报错 NoSuchMethodException(找不到无参构造)
Constructor<EnumSingle> declaredConstructor=EnumSingle.class.getDeclaredConstructor(String.class,int.class);
EnumSingle instance2=declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}java
//反射底层代码
// 如果检查到你这个类是一个 Enum
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
// 直接抛出异常
throw new IllegalArgumentException("Cannot reflectively create enum objects");