IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 单例模式详解 -> 正文阅读

[Java知识库]单例模式详解

1、简介

单例模式模式是23种设计模式中最为简单的,而单例模式又分为两种模式

  • 饿汉式模式

  • 懒汉式模式

单例模式的特点:

  1. 单例模式中只能有一个实例

  2. 单例类必须自己创建自己的唯一实例

  3. 单例类必须给所有其他对象提供这一实例

因为单例模式只有一个实例对象,那么这个单例类的构造器必须为私有的。

2、饿汉式模式

饿汉式模式,这是一个比喻,程序一上来就直接创建这个实例化对象

public class Hello {
 ? ?//私有化构造器
 ? ?private Hello() {
?
 ?  }
 ? ?//直接实例化对象
 ? ?private static final Hello hello = new Hello();
 ? ?
 ? ?//对外提供这个对象
 ? ?public static Hello getInstance() {
 ? ? ? ?return hello;
 ?  }
}

这种方法一进行类加载,这个对象就被实例化了,这种方法有个很大的缺点,那就是浪费内存空间

例如

public class Hello {
    
    //私有化构造器
    private Hello() {

    }
    private static byte[] bytes1 = new byte[1024*1024];
    private static byte[] bytes2 = new byte[1024*1024];
    private static byte[] bytes3 = new byte[1024*1024];
    private static byte[] bytes4 = new byte[1024*1024];

    //对外提供这个对象
    public static ArrayList getInstance() {
        ArrayList arrayList = new ArrayList();
        arrayList.add(bytes1);
        arrayList.add(bytes2);
        arrayList.add(bytes3);
        arrayList.add(bytes4);
        return arrayList;
    }
}

可以看见,一进行初始化就被占用了4M的内存空间,极其浪费内存。

为了解决这个问题,就出现了懒汉式模式

3、懒汉式模式

懒汉式,即需要用到这个类的实例的时候,才进行这个对象的实例化。

public class Hello {
    //构造器私有化
    private Hello() {

    }
    private static Hello hello = null;
    //对外提供获取对象的方法
    public static Hello getInstance() {
        if(hello == null) {
            hello =  new Hello();
        }
        return hello;
    }
    
}

那么当用户调用我们的getInstance方法,这时才会进行实例化

当时这个是在单线程的情况下是单例模式,在多线程并发的情况下,它就不是单例模式了

public class Hello {
    //构造器私有化
    private Hello() {
        System.out.println(Thread.currentThread().getName());
    }
    private static Hello hello = null;
    //对外提供获取对象的方法
    public static Hello getInstance() {
        if (hello == null) {
            hello =  new Hello();
        }
        return hello;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                public void run() {
                    Hello.getInstance();
                }
            }).start();
        }
    }
}

这里我开了10个线程,那么出来是单例吗?

?

很明显,这里创建了3个对象,那么多线程并发的模式下确实破坏了这种单例模式,那么我们给这个getInstance方法加上一个锁

双重检测锁

 public static Hello getInstance() {
         if (hello==null) {
            synchronized (Hello.class) {
                if (hello == null) {
                    hello = new Hello();
                }
            }
        }
            return hello;
        }
    }

我们这里锁的是Hello.class,因为每一类的class文件只有一个,再次看程序运行的结果

?

诶,感觉我们解决了单例模式在多线程并发情况下出现的问题,但是我们又想到new 这个关键字创建对象的时候不是原子性的

正常的创建对象分为:

  1. new指令申请堆栈空间

  2. 调用构造函数来初始化对象

  3. 使对象的引用指向这个内存空间

那么不是原子性操作可能会导致cpu的指令重排,本来正常123步的操作,在cpu进行指令重排后就变成了132了,

那么此时突然有个线程B想要获取这个实例对象,执行到3的步骤的时候被B线程返回了这空对象,那么就会导致程序出问题。

解决这个问题的最好方法就是利用volatile关键字

  private volatile static Hello hello = null;

4、反射破坏单例模式

那么加上了这个volatile关键字来保证这个对象创建的原子性操作就可以完成单例了吗?

当然不行,因为有反射存在,它可以反过来获取到该类的构造方法,从而创建对象

   public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Hello hello1 = Hello.getInstance();

        Constructor<Hello> declaredConstructor = Hello.class.getDeclaredConstructor(null);

        //破坏私有化的构造器
        declaredConstructor.setAccessible(true);
        Hello hello2 = declaredConstructor.newInstance();

        System.out.println(hello1);
        System.out.println(hello2);
    }

?

由后面的HashCode可以看出,这是两个不同的对象,那么证明我们的反射确实破坏了我们创建出来的单例模式。

那么我们就从构造器中再添加一重验证

  private Hello() {
        if (hello != null) {
            throw new RuntimeException("不允许用反射破坏单例模式!");
        }
    }

?当时如果我两个对象都是由反射来创建的,那么一重的判断就没有效果!

 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<Hello> declaredConstructor = Hello.class.getDeclaredConstructor(null);
        //破坏私有化的构造器
        declaredConstructor.setAccessible(true);

        Hello hello1 = declaredConstructor.newInstance();
        Hello hello2 = declaredConstructor.newInstance();
        
        System.out.println(hello1);
        System.out.println(hello2);
    }

可以看到,加了判断可还是会创建出来两个对象。

这时我们可能会想到给这个构造器添加一个标志位,我一旦进入了这个构造器,我们就马上修改这个标志位。

 private static boolean isCreat = false;
    //构造器私有化
    private Hello() {
        if (!isCreat) {
            isCreat = true;
        }else {
            throw new RuntimeException("不可能使用反射获取对象");
        }
    }

?

那么这种方式在第一次用反射创建对象的时候,标志位就被改变了,那么在第二次反射创建对象将会抛出异常

但是这种方法还是能被反射完全破解!如果把这个标志位加密的话,懂的人还是能把它解密出来

public static void main(String[] args) throws Exception{
        Field declaredField = Hello.class.getDeclaredField("isCreat");

        Constructor<Hello> declaredConstructor = Hello.class.getDeclaredConstructor(null);
        //破坏私有化的构造器
        declaredConstructor.setAccessible(true);
        
        Hello hello1 = declaredConstructor.newInstance();
        
        //修改标志位
        declaredField.set(hello1,false);
        
        Hello hello2 = declaredConstructor.newInstance();
       
        System.out.println(hello1);
        System.out.println(hello2);
    }

?

那么单例模式又又又被破坏了!反射可真无敌啊

我们就会像反射是怎么通过获取到的构造器来构造对象的呢?我们点进这个newInstance方法中一探究竟

 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;
    }

突然发现里面的一个判断,它说如果这个class是枚举类型的,那么直接抛出一个异常说不能通过反射创建枚举类型的对象!

首先我们来说说枚举,枚举是JDK1.5就出现了,那么枚举类到底是什么样的呢?

例如一枚硬币往上抛,掉到地上只有两种结果,要么是正面,要么是反面

public enum Coin {
    
    CHARACTER,FLOWER;

}

那么CHARACTER和FLOWER就分别代表两种情况,字和花,没有第三种情况了

那么我们就用枚举来检测反射是否能破坏单例

public enum Instance {
    INSTANCE;

    public static Instance getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws Exception{
        Instance instance1 = Instance.getInstance();

        Constructor<Instance> declaredConstructor = Instance.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);

        Instance instance2 = declaredConstructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);

    }
}

?

这里报的却不是Cannot reflectively create enum objects,再翻译一下这个错误,它说主线程中没有这个空参的构造方法,奇怪,之前基础不是说如果没有手动添加构造器不是会有一个默认的空参构造器吗

我们来看看它编译之后的class文件

public enum Instance {
    INSTANCE;

    private Instance() {
    }

    public static Instance getInstance() {
        return INSTANCE;
    }

}

这里确实有空参构造方法啊,那为什么会报出没有这个方法的错误呢?what the...

我们现在来反编译一下这个class文件

?

我们这里使用javap反编译一下,却发现它还是有空参方法,而且知道这个枚举类是继承自Enum类的。

那么现在我们直接用jad工具去反编译class文件直接生成java文件。

?敲这行命令行,那么就会反编译生成java文件

private Instance(String s, int i)
    {
        super(s, i);
    }

我们惊讶的发现,它有一个有参的构造器,而且也调用了其父类的构造方法!

那么我们通过反射创建枚举对象就应该传递两个参数

 Constructor<Instance> declaredConstructor = Instance.class.getDeclaredConstructor(String.class,int.class);

再次执行该方法,就发现确实报出了Cannot reflectively create enum objects错误

?之前反编译出来的java文件发现我们写的枚举类型是继承自Enum的

?

那么我们点金这个父类Enum看看

有意思的是,这个父类只有一个有参构造方法

?

那么这就很明显为什么我们自己写的枚举类型没有空参构造方法了,有子先有父,我们自己写的枚举类型要调用父类的构造器,而父类的构造器又需要传入两个参数,这就是为什么我们反编译过后会出现两个参数的构造器!!!

而且这个父类构造器的方法前面还有注释:

/**
     * Sole constructor.  Programmers cannot invoke this constructor.
     * It is for use by code emitted by the compiler in response to
     * enum type declarations.
     *
     * @param name - The name of this enum constant, which is the identifier
     *               used to declare it.
     * @param ordinal - The ordinal of this enumeration constant (its position
     *         in the enum declaration, where the initial constant is assigned
     *         an ordinal of zero).
     */
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

前面翻译过来就是:唯一的构造器。程序员不能调用此构造函数。它是供编译器响应的代码使用的枚举类型声明。

那么单例模式就到这里啦,如果有帮助到你的,点个小赞吧,如有错误请各位大佬指出谢谢!!!

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-11-22 12:12:16  更:2021-11-22 12:14:38 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 2:42:13-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码