01、单例模式
GoF23
0、七大原则
第一个原则最重要。
其他的原则都有相应的优点和缺点,我们23种设计模式就是根据这些原则设置出来的
1、单例模式
程序员必会的,不会都丢人
1、饿汉式
比较饿,程序加载的时候就会创建!
优点:线程安全
缺点:如果类中的对象很多,会很消耗内存
package _01单例模式;
/**
* @author zhangbingbing
* @version 1.0
* @date 2020/6/14 15:29
*/
public class Hungry {
//饿汉式,无论是饿汉还是懒汉,万恶的反射都能破解
private Hungry() {
}
//饿汉的缺点:这行代码解释很清晰的显示出来
//类加载的时候就创建对象 则会加载类中的类变量到内存中 数组 集合等,很占内存
private final static Hungry HUNGRY = new Hungry(); //线程安全
//对外接口获取实例对象
public static Hungry getInstance() {
return HUNGRY;
}
}
class MyYest{
public static void main(String[] args) throws Exception {
Hungry[] hungries = new Hungry[100];
for (int i = 0; i < 100; i++) {
int finalI = i;
new Thread(() -> {
hungries[finalI] = Hungry.getInstance();
}).start();
}
for (int i = 0; i < 100; i++) {
System.out.println(i + ":" + hungries[i].hashCode());
}
}
}
2、懒汉式
我们需要的时候才创建对象
缺点:原始的线程不安全
所以我们要优化成 DCL :双重检测锁模式
有因为new对象的时候不是原子性操作所以我们要加上volatile关键字
这样才能把线程安全解决!
package _01单例模式;
/**
* @author zhangbingbing
* @version 1.0
* @date 2020/6/9 22:57
*/
public class Lazy {
//单例模式都必须把构造方法设置成私有的
private Lazy() {
//只要创建都会进入构造方法
if (lazy == null) {
System.out.println(Thread.currentThread().getName() + "-->");
}
}
//懒汉自己不加载,等我们调用getInstance方法的时候才加载
//缺点显而易见:当多个线程同时调用的时候会破坏单例
private volatile static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
//还是有问题
//因为在这里创建对象cpu内部其实是执行了三步操作
//也就是不是原子性的
//1、分配内存空间
//2、初始化对象
//3、把对象引用指向内存空间
//cpu三个顺序可以任意执行 132 出问题
}
}
}
return lazy;
}
}
class MyTest {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 100; i++) {
int num = i;
new Thread(() -> {
Lazy lazy = Lazy.getInstance();
}).start();
}
}
}
3、反射破解
以上两种单例都会被反射破解,直接暴力破解构造函数
一下是最优的,不过还是不安全
package _01单例模式;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
/**
* @author zhangbingbing
* @version 1.0
* @date 2020/6/14 16:17
*/
public class LazyAnno {
//使用反射破解单例,加上反破解的方法
//来个哨兵,这个哨兵只有我们知道,但是还是不安全,可以被破解,可以用反射设置成true
private static boolean bingbing = true;
private LazyAnno() {
if (bingbing) {
//没事
bingbing = false;
} else {
throw new RuntimeException("不要试图用反射破坏单例!!");
}
}
private volatile static LazyAnno lazyAnno;
public static LazyAnno getInstance() {
if (lazyAnno == null) {
synchronized (LazyAnno.class) {
if (lazyAnno == null) {
lazyAnno = new LazyAnno();
}
}
}
return lazyAnno;
}
}
class MyAnno{
public static void main(String[] args) throws Exception{
Constructor<LazyAnno> declaredConstructor = LazyAnno.class.getDeclaredConstructor(null);
//暴力破解
declaredConstructor.setAccessible(true);
LazyAnno lazyAnno2 = declaredConstructor.newInstance();
Field bingbing = LazyAnno.class.getDeclaredField("bingbing");
bingbing.setAccessible(true);
bingbing.set(lazyAnno2, true);
LazyAnno lazyAnno1 = declaredConstructor.newInstance();
System.out.println(lazyAnno1==lazyAnno2); //false
}
}
当通过反编译知道那个哨兵的时候就可以破解!
所以我们迎来了天然的单例
枚举类型
package _01单例模式;
import java.lang.reflect.Constructor;
/**
* @author zhangbingbing
* @version 1.0
* @date 2020/6/14 16:39
*/
public enum EnumSingle {
//使用枚举来实现单例
SINGLE;
public static EnumSingle getInstance() {
return SINGLE;
}
}
class TestEnum{
public static void main(String[] args) throws Exception {
EnumSingle instance = EnumSingle.getInstance();
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle enumSingle = declaredConstructor.newInstance();
System.out.println(instance == enumSingle);
}
}
这个报错:说没有这个无参构造
一顿分析发现idea骗了我们:
本身的构造器参数是String 和 int类型的
ok~得出最终答案
因为反射的底层不能破坏枚举,所以枚举是安全的单例
总结一下:
单例模式我们首先需要把构造器私有化,市面上主要有两种单例
- 饿汉:我们直接在类的内部创建对象,类一加载就创建对象
缺点可能会很耗内存,但是线程安全
- 懒汉
线程不安全所以我们一般使用DCL懒汉模式,双层检测锁模式,还要保证原则性
但是这两种都会被反射破解,还有一种天然安全的单例,就是枚举类型,因为反射源码里写了必能通过反射创建枚举类型的实例~