彻底玩转单例模式
单例三要素
- 私有的构造方法;
- 指向自己实例的私有静态引用;
- 以自己实例为返回值的静态的公有方法。
1. 饿汉式单例模式
立即加载 : 在类加载初始化的时候就主动创建实例;
//饿汉式单例,在类初始化时,已经自行实例化,天生就是线程安全的类只加载异常只有一个实例(比较耗费内存资源)
public class Hungry {
private Hungry() {}//构造方法私有化
private final static Hungry Hungry = new Hungry();//类初始化时,自己实例化
public static Hungry getInstance() {
return Hungry;
}
}
2.懒汉式单例
单线程场景
延迟加载 : 等到真正使用的时候才去创建实例,不用时不去主动创建
//懒汉式单例,(单线程没问题,多线程有问题)
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName());
};
private static LazyMan LAZY_MAN;
public static LazyMan getInstance(){
if (LAZY_MAN==null) {
LAZY_MAN = new LazyMan();
}
return LAZY_MAN;
}
}
多线程场景
1.静态内部类方式:也即饿汉式和懒汉式的组合,调用getInstance()方法时才创建,达到了类似懒汉式的效果,同时又是线程安全的
public class TestStatic {
private TestStatic(){}
private static class InnerClass{
private final static TestStatic testStatic = new TestStatic();
}
public static TestStatic getInstance(){
return InnerClass.testStatic;
}
}
- 双重检测锁模式+volatile(推荐使用)
//懒汉式单例,(保证多线程安全:)
class LazyMan2 {
private volatile static LazyMan2 lazyMan;//加volatile禁止指令重排
private LazyMan2() {//私有的构造器
}
//双重检查锁定:DCL懒汉式
public static LazyMan2 getInstance() {
if (lazyMan == null) {//需要时才创建实例
synchronized (LazyMan2.class) {//加类锁保证一个类只有一个实例
if (lazyMan == null) {
lazyMan = new LazyMan2();//new对象不是一个原子性操作
/*
* new对象的三步
* 1、分配对象的内存空间
* 2、执行构造器,初始化对象
* 3、把这个对象指向这个内存空间
* 正常情况:执行123,
* 多线程情况下指令重排:一个线程执行132,当执行3后,另一个线程进来,
* 由于内存空间已经被占用了,会导致当前线程认为对象不为null,
* 执行return,但时对象实际还没有创建,会产生问题,需要在lazyMan上加volatile修饰
*
* */
}
}
}
return lazyMan;
}
}
- 双重检测锁模式+volatile+防止反射破坏(测试),通过反射标志位修改标志位,还是能破坏单例
//懒汉式单例,(保证多线程安全:)
class LazyMan2 {
private static boolean flag = false;//标记位
private volatile static LazyMan2 lazyMan;//加volatile禁止指令重排
//防止反射破坏,
private LazyMan2() {//私有的构造器
synchronized (LazyMan2.class) {
if (flag == false) {
flag = true;
} else {
throw new RuntimeException("不要试图使用反射破坏");
}
}
}
//双重检查锁定:DCL懒汉式
public static LazyMan2 getInstance() {
if (lazyMan == null) {//需要时才创建实例
synchronized (LazyMan2.class) {//加类锁保证一个类只有一个实例
if (lazyMan == null) {
lazyMan = new LazyMan2();//new对象不是一个原子性操作
/*
* new对象的三步
* 1、分配对象的内存空间
* 2、执行构造器,初始化对象
* 3、把这个对象指向这个内存空间
* 正常情况:执行123,
* 多线程情况下指令重排:一个线程执行132,当执行3后,另一个线程进来,
* 由于内存空间已经被占用了,会导致当前线程认为对象不为null,
* 执行return,但时对象实际还没有创建,会产生问题,需要在lazyMan上加volatile修饰
*
* */
}
}
}
return lazyMan;
}
//测试:在反射面前任何代码都是不安全的
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException {
Class<LazyMan2> lazyMan2Class = LazyMan2.class;
Constructor<LazyMan2> declaredConstructor = lazyMan2Class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);//破坏构造器私有化
LazyMan2 lazyMan2 = lazyMan2Class.newInstance();//创建LazyMan2的对象
LazyMan2 lazyMan21 = LazyMan2.getInstance();//通过单例模式创建的对象
System.out.println(lazyMan2);//hangry.LazyMan2@2f2c9b19
System.out.println(lazyMan21);//hangry.LazyMan2@31befd9f
System.out.println(lazyMan2==lazyMan21);//false
}
}
解决通过反射破坏单例模式
枚举类的单例模式
枚举单例线程安全,实现简单,调用效率高,不能延时加载。枚举本身就是单例模式,由JVM从根本上提供保障并且可以天然的防止反射和反序列化漏洞!需要继承的场景它就不适用了。枚举方式是Effective Java作者提倡的方式。
public enum ClassD {
//定义一个枚举的元素,它就代表了Singleton的一个实例。
INSTANCE;
//对外部提供调用方法:将创建的对象返回,只能通过类来调用
public void otherMethod(){
//功能处理
}
//测试
public static void main(String[] args) {
ClassD a = ClassD.INSTANCE;
ClassD b = ClassD.INSTANCE;
System.out.println(a==b);
}
}
枚举反编译后源码的构造器
使用jad反编译工具:cmd输入:jad -sjava TestEnum.class,得到如下
//有参构造
private TestEnum(String s, int i){
super(s, i);
}
使用反射破坏枚举的构造器
//枚举单例
public enum TestEnum {
INSTANCE;
public static TestEnum getInstance(){
return INSTANCE;
}
//反射破坏
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<TestEnum> testEnumClass = TestEnum.class;
Constructor<TestEnum> declaredConstructor = testEnumClass.getDeclaredConstructor(String.class,int.class);//使用隐藏的构造
declaredConstructor.setAccessible(true);
TestEnum testEnum = declaredConstructor.newInstance();
System.out.println(TestEnum.getInstance() == testEnum);
}
}
得到结果:抛出异常:java.lang.IllegalArgumentException: Cannot reflectively create enum objects
所有反射不会破坏枚举的单例