之所以使用private声明一个私有的构造器,其主要目的就是为了不让用户创建新的实例对象
——亚里士多德
进行实例化的时候的几个步骤
(指令重排可能会打乱这三个顺序)
😧什么是单例模式?
保证类在内存中只能有一个对象,且构造私有的类设计模式,他主要解决了一个全局使用的类频繁地创建与销毁。
🐕 单例模式的好处:
😧缺点:
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 static Hungry hungry= new Hungry();
private static Hungry getHungry(){
return hungry;
}
}
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName() + " " + "初始化了一个");
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if(lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
}
这种情况看似没有问题,是因为在单线程情况下没u有问题,一旦在多线程场景下,问题立马就出来了,可能会实例化好多个LazyMan
如开10个线程 ,会初始化5个,按理应该是1个:
👇
我们可以想到的方法是,为其加锁 第一个想到的是sychronized
//双重锁检测机制
public static LazyMan getInstance(){
if(lazyMan == null){
synchronized (LazyMan.class){
if (lazyMan == null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
但是这样看似解决了多线程的问题,但是仍然还有其他问题,那就是更底层的指令重排问题。
因为lazyMan = new LazyMan()本身就不是一个原子性操作,当你执行这条指令的时候,会经历以下步骤
如果大家都是按照 1 2 3顺序来做,肯定是没问题的,但是synchronized只是锁对象,并不保证底层禁止指令重排,这就会有严重的事情,比如:
两个线程A,B分别以 123 与 132的顺序执行的指令。 当B执行完3号指令的时候,由于某种原因卡住了,此时A观察到lazyMan已经不为null了,那么就会返回lazyMan. 但其实此时的lazyMan还没有被初始化
👇
说起禁止指令重排,我们立马想到了volatile,是的没错,只需要为我们的懒汉加上volatile关键字即可了 ==== 》(齐全之后的懒汉模式被称为DCL懒汉模式)
private volatile static LazyMan lazyMan;
DCL懒汉模式代码:
package com.kai.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName() + " " + "初始化了一个");
}
private volatile static LazyMan lazyMan;
//双重锁检测机制
public static LazyMan getInstance(){
if(lazyMan == null){
synchronized (LazyMan.class){
if (lazyMan == null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
答案是肯定的,因为java中存在反射机制,而反射机制会破坏掉很多看似很安全的机制。
接着刚刚的lazyMan代码,我们继续写一个main方法,用反射机制来进行破坏。
//反射
public static void main(String[] args) throws Exception {
LazyMan instaince = LazyMan.getInstance();
//拿到他的无参构造器
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan instance2 = constructor.newInstance();
//输出两个实例,看是否是同一个实例
System.out.println(instaince);
System.out.println(instance2);
}
事实证明,单例模式确实是被破坏掉了。
👇
你可能会说,那我在构造器里面再加锁不就行了
private LazyMan() throws Exception {
synchronized (LazyMan.class){
if(lazyMan == null){
System.out.println(Thread.currentThread().getName() + " " + "初始化了一个");
}else{
throw new Exception("禁止使用反射来破坏单例");
}
}
}
可能你觉得胜利了,但是道高一尺,魔高一丈
👇
此时,我们再来修改以下main方法。让两个实例都是通过反射来制造
public static void main(String[] args) throws Exception {
//LazyMan instaince = LazyMan.getInstance();
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
此时又出现了刚刚的情况。
👇
你可能又会说,那我构造一个私有的变量来检测构造方法有没有被执行不就可以了?
让我们看一下这样的具体做法
private static boolean judge = false;
private LazyMan() throws Exception {
synchronized (LazyMan.class){
if(lazyMan == null){
if(judge == false) judge = true;
else throw new Exception("禁止使用反射机制破坏");
System.out.println(Thread.currentThread().getName() + " " + "初始化了一个");
}else{
throw new Exception("禁止使用反射来破坏单例");
}
}
}
此时你可能又觉得没问题了,可惜道高一尺魔高一丈。
👇
让我们再次修改main方法中的代码吗
public static void main(String[] args) throws Exception {
//LazyMan instaince = LazyMan.getInstance();
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
//通过反射获取属性
val judge = LazyMan.class.getDeclaredField("judge");
judge.setAccessible(true);
constructor.setAccessible(true);
LazyMan instance2 = constructor.newInstance();
//把judge设置为false,就可以继续创建实例了
judge.set(constructor,false);
LazyMan instance = constructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
看到这里,你是不是觉得很累,反射好像是永远都有他的对策,,,,, 没错,这也正是懒汉模式单例的致命缺点。
枚举是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化,多线程安全。
它可以有效的防止反射。
public enum EnumSingleton {
INSTANCE;
public void outPut(){
System.out.println("=================");
}
}
他所对应的class文件如下
package li;
public enum EnumSingleton {
INSTANCE;
private EnumSingleton() {
}
public void outPut() {
System.out.println("=================");
}
}
尝试使用反射机制破坏
class test5{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingleton enumSingleton = declaredConstructor.newInstance();
enumSingleton.outPut();
}
}
注意: 我们这里仍然是通过null来调用无参构造。 我们运行一下看效果
Exception in thread "main" java.lang.NoSuchMethodException: li.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at li.test5.main(EnumSingleton.java:15)
他竟然提示我们该类中没有无参构造。。。一定是有什么猫腻在其中,毕竟我们刚刚亲手写了无参构造
👇
尝试用jad进行反编译,先要打开jad,然后在cmd输入:jad -sjava EnumSingleton.class
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumSingleton.java
package li;
import java.io.PrintStream;
public final class EnumSingleton extends Enum
{
public static EnumSingleton[] values()
{
return (EnumSingleton[])$VALUES.clone();
}
public static EnumSingleton valueOf(String name)
{
return (EnumSingleton)Enum.valueOf(li/EnumSingleton, name);
}
//看这里,发现是有两个参数的有参构造方法
private EnumSingleton(String s, int i)
{
super(s, i);
}
public void outPut()
{
System.out.println("\u7EA2\u706B\u706B\u604D\u604D\u60DA\u60DA");
}
public static final EnumSingleton INSTANCE;
private static final EnumSingleton $VALUES[];
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
}
可以看出来, 他确实是虚晃了我们一枪,其实他是有着带两个参数的构造器的,那我们就换一种获取构造器的方法。(用反射来尝试使用有参构造)
class test5{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//使用有参构造
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
EnumSingleton enumSingleton = declaredConstructor.newInstance();
enumSingleton.outPut();
}
}
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at li.test5.main(EnumSingleton.java:17)
结果告诉我们不能用反射来创建枚举对象。 这才是我们真正想要的异常
一般情况下,不建议使用第 2 懒汉方式,建议使用第 1 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 4 种静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用第 5 种枚举方式。如果有其他特殊的需求,可以考虑使用第 3 种双检锁方式。