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知识库]单例模式有这么多需要注意的

什么是单例模式?
单例模式是23种设计模式中最简单的一种,是创建型模式,对象由自己的类负责创建,并保证一个类只有一个实例!单例模式提供了一个外部获取实例的方法。单例模式有两种实现模式,一个是饿汉式,一个是懒汉式。
好处:全局只有一个实例,避免了频繁的创建类,减少了内存的开销与资源的消耗。
坏处:没有接口,不能继承,违反了单一职责原则。
实现思路:单例模式为了保证对象是自己创建的,所以需要把构造器私有,并且把声明的变量私有,对外提供一个获取实例的方法即可。

1、饿汉模式

顾名思义就是很饿,对象不管有没有用到都在一开始就new出来放着,这样如果实例没有使用的话就会浪费内存,但是好处是,由于对象在一开始就创建好了,所以它具有天然的线程安全优势,由于没有加锁,性能也是非常优秀的。

/**
 * 单例模式之饿汉模式
 */
public class HungryMan {

    /**
     * 单例模式将构造器私有,不允许通过new的方式创建对象
     */
    private HungryMan(){}

    /**
     * 在类加载的时候就将实例创建好,不管有没有用到
     */
    private static final HungryMan HUNGRY_MAN = new HungryMan();

    /**
     * 提供一个外部方法获取当前实例,而不是直接类名.
     * @return HUNGRY_MAN实例
     */
    public static HungryMan getInstance(){
        return HUNGRY_MAN;
    }
}

测试:

public class Test01 {
    public static void main(String[] args) {
        HungryMan hungryMan1 = HungryMan.getInstance();
        HungryMan hungryMan2 = HungryMan.getInstance();
        System.out.println(hungryMan1 == hungryMan2); // true
    }
}

饿汉模式没什么好说的,重点看看懒汉模式。

2、懒汉模式

懒汉模式,顾名思义,非常懒,对象在用到的时候才创建,好处是解决了饿汉模式可能会浪费内存的缺点,在单线程模式下完美替代饿汉模式,但是既然是动态创建的,那就避免不了多线程环境下的并发异常,先不废话,看看简单的懒汉模式如何实现。

1.0版本

/**
 * 单例模式之懒汉模式
 */
public class LazyMan {

    private LazyMan(){}

    private static LazyMan lazyMan;

    public static LazyMan getInstance(){
        // 如果实例为空,就创建实例,否则直接返回这个实例
        if (lazyMan == null) {
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

测试:

public class Test02 {
    public static void main(String[] args) {
        LazyMan lazyMan1 = LazyMan.getInstance();
        LazyMan lazyMan2 = LazyMan.getInstance();
        System.out.println(lazyMan1 == lazyMan2); // true
    }
}

看起来没有任何问题,但是如果我们在多线程环境下测试呢?
首先先在LazyMan的构造方法中添加代码,方便我们知道对象是谁创建的:

private LazyMan(){
    System.out.println(Thread.currentThread().getName()+"创建了LazyMan对象");
}

修改测试类:

public class Test01 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }
}

结果:
在这里插入图片描述
直接车祸现场,由于线程执行太快了,以至于第一个线程获取实例时,实例还没有创建完毕,后面的线程就冲上来了,发现对象还没有创建,又去创建。。。
我们可以将线程数量调打一些再看看效果,我这里修改为20个。
在这里插入图片描述

基本可以在13个线程开始执行之前完成实例的创建,所以后面都没有执行了,这个结果与你电脑性能有关,不同性能的电脑执行效果肯定是不一样的,但是都能得出这个结论:懒汉模式存在线程安全问题!

2.0 DCL双重检测锁模式

通过加锁,解决基本的线程安全问题。

/**
 1. 单例模式之懒汉模式
 */
public class LazyMan {

    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"创建了LazyMan对象");
    }

    private static LazyMan lazyMan;

    public static LazyMan getInstance(){
        // 第一层判断的作用是:如果实例已经创建了就直接返回即可,不需要锁了,这样可以提高效率
        if (lazyMan == null) {
            // 如果不存在,加锁创建实例
            synchronized (LazyMan.class){
                // 这里再判断一次的原因是防止多线程下误判,所以再判断一次
                if (lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}

继续使用上面的测试:
在这里插入图片描述
我们发现这已经解决了我们我们上个例子的问题,但是这样真的没问题了吗?
我们需要先了解,类加载的过程:

  1. 分配内存空间
  2. 创建实例
  3. 将实例指向内存空间

我们试想一下,如果同时有多个线程同时获取实例,判断为空之后给实例分配内存空间,这样就会产生多个内存空间,最后只有一个实例获得了锁创建了实例,将实例放入自己分配的内存空间,其他的线程由于拿不到锁创建不了实例,将一个空的放入了内存空间,也就是直接跳过了第二个指令。本来应该1->2->3执行的指令,变成了1->3->2

这就是DCL中存在的指令重排现象,解决的办法即使在变量前面加一个volatile关键字。

volatile关键字在java中的年纪很老了,在synchronize出现的后更是存在感极地。

它的主要作用就是保证可见性,禁止指令重排。详细可以参看关于volatile

所以最后我们的懒汉模式的代码为:

/**
 * 单例模式之懒汉模式
 */
public class LazyMan {

    private LazyMan(){}

    private volatile static LazyMan lazyMan;

    public static LazyMan getInstance(){
        // 第一层判断的作用是:如果实例已经创建了就直接返回即可,不需要锁了,这样可以提高效率
        if (lazyMan == null) {
            // 如果不存在,加锁创建实例
            synchronized (LazyMan.class){
                // 这里再判断一次的原因是防止多线程下误判,所以再判断一次
                if (lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}

如果是面试,到这里就够了~ 如果想多了解一下,请往下看

天下没有不透风的墙,世上没有不出bug的代码,上面真的能确保只得到一个实例吗?

3.0 杜绝反射破环

首先我们在之前的基础上进行测试,通过反射创建对象。

public class Test01 {
    public static void main(String[] args) throws Exception {
        LazyMan lazyMan1 = LazyMan.getInstance();

        // 通过反射获取构造器
        Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null);
        // 无视private
        lazyManConstructor.setAccessible(true);
        // 通过构造器创建实例
        LazyMan lazyMan2 = lazyManConstructor.newInstance(null);

        System.out.println(lazyMan1 == lazyMan2);  // false
    }
}

我们发现结果又为false了,也就是我们不能保证全局只有一个单例,这违反了单例模式的初衷。

既然是通过构造方法破坏了单例模式,那自然也是从构造方法改起,在原来代码中加入一个标志位,保证我们的构造方法只能调用一次不就好了吗?

// 标志位,默认为false,如果构造方法调用了,就改为true,保证构造方法只能调用一次!
private static boolean flag = false;

private LazyMan(){
    if (flag){
        throw new RuntimeException("不要试图通过构造方法创建实例");
    }
    flag = true;
}

测试代码不用改,再次测试!
在这里插入图片描述
基本上解决了~

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-09-04 17:22:29  更:2021-09-04 17:23:18 
 
开发: 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/23 13:02:31-

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