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知识库]波吉学设计模式——玩转单例模式

单例模式作为设计模式中最常见最重要的设计模式,今天波吉带你由浅入深的明白单例模式,相信你一定会有所收获的
在这里插入图片描述

单例模式

简介

? 所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供了一个取得其对象实例的方法。如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器的方法权限设置为private,这样就不能用new操作符在类的外部产生类的对象了,但是类内部仍然可以产生该类的对象,因为在类的外部开始还没发得到类的对象,只能调用该类的某个静态方法以返回类内部构建的对象,静态方法只能访问类中的静态成员变量,所以指向类内部产生的该类对象的变量也必须定义为静态的

核心作用

保证一个类只有一个对象,并且提供一个访问该实例的全局访问点

优点

  • 由于单例模式只生成一个实例,减少了系统的开销,当一个对象需要比较的多的资源时,如读取配置、产生其它依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决
  • 单例模式可以在系统设置全局访问点,优化环共享资源访问,例如可以设计一个单例类,负责所以数据表的映射处理

应用场景

? 单例模式只生成了一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置,产生其它依赖对象,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决,例如:java.lang.Runtime,网站计数器,应用程序的日志应用,数据库连接池,读取配置文件的类,Application,Windows中的任务管理器和回收站

  • 业务系统全局只需要一个对象实例,比如发号器,redis连接对象等
  • Spring IOC容器中的Bean默认就是单例
  • Spring Boot中的Controller,Service,Dao层中 通过 @AutoWrie的依赖注入默认就是单例的

主要分类

  • 饿汉:就是所谓的懒加载,延迟创建对象,需要使用的时候再延时创建对象,因为饿汉单例即使没有调用方法也会占据内存所以我们一般不采用
  • 懒汉:懒汉由于调用实例方法对象才会生成所以更符合我们的需求

饿汉单例

线程安全

class Bank {
    //1.私有化构造器,不允许外部可以调用
    private Bank() {

    }

    //2.内部创建类的对象
    //4.要求此对象必须声明为静态
    private static Bank instance = new Bank();

    //3.提供公共的方法,返回类的对象
    public static Bank getInstance() {
        return instance;
    }
}

public class SingletonTest1 {
    public static void main(String[] args) {
        Bank bank = Bank.getInstance();
    }
}

饿汉模式简单了解后我们着重分析懒汉单例

单例模式实现步骤:

  • 私有化构造函数
  • 提供获取单例的?法

懒汉单例

这是最简单实现懒汉单例的实例,后续内容会对懒汉单例做出升级!在本次实例中线程不安全

/**
 * 单例设计模式——懒汉
 *
 * @author ccy
 * @version 1.0
 * @date 2021/12/6 13:18
 */
class Order {
    //1.私有化构造器,不允许外部可以调用
    private Order() {

    }

    //2.声明当前类的对象
    //4.要求此对象必须声明为静态
    private static Order instance = null;

    //3.提供公共的方法,返回类的对象
    public static Order getInstance() {
        if (instance == null) {
            instance = new Order();
        }
        return instance;
    }
}

标题写到这种情况的单例模式是线程不安全原因就在于在高并发的场景下会创建多个对象违背了单例模式只创建一次的情况

在这里插入图片描述

为了应对高并发下能够只创建一次对象的情况所以我们引入Synchroized,但是采用synchronized 对方法加锁有很大的性能开销,因为当getInstance()内部逻辑比较复杂的时候,在高并发条件下没获取到加锁方法执行权的线程,都得等到这个方法内的复杂逻辑执行完后才能执行,等待浪费时间,效率比较低

这种实现懒汉单例线程是安全的但是不采用它的原因就在于synchronized带来的效率低

class Order {
    //1.私有化构造器,不允许外部可以调用
    private Order() {

    }

    //2.声明当前类的对象
    //4.要求此对象必须声明为静态
    private static Order instance = null;

    //3.提供公共的方法,返回类的对象
    public synchronized static Order getInstance() {
        if (instance == null) {
            instance = new Order();
        }
        return instance;
    }
}

为了满足以上需求,DCL双重检测锁机制的单例模式就出现了

双重检测锁模式

下面是上述代码的运行顺序:

  1. 检测实例是否已经初始化创建,如果是则立即返回
  2. 获得锁
  3. 再次检测实例是否已经初始化创建成功,如果还没有则创建实例
/**
 * 单例设计模式——懒汉
 *
 * @author ccy
 * @version 1.0
 * @date 2021/12/6 13:18
 */
class Order {
    //1.私有化构造器,不允许外部可以调用
    private Order() {

    }

    //2.声明当前类的对象
    //4.要求此对象必须声明为静态
    private static Order instance = null;

    //3.提供公共的方法,返回类的对象
    public static Order getInstance() {
        if (instance == null) {
            synchronized (Order.class) {
                if (instance == null) {
                    instance = new Order();
                }
            }
        }
        return instance;
    }
}

public class SingletonTest2 {
    public static void main(String[] args) {
        Order order1 = Order.getInstance();
        Order order2 = Order.getInstance();
        System.out.println(order1 == order2);
    }
}

DCL双重检测锁机制在逻辑上的确是趋近于完美了但是!!!由于指令重排的原因仍然有可能会创建多个对象,因为instance = new Order()这行代码的执行逻辑是

  1. 在堆中开辟对象所需空间,分配地址
  2. 根据类加载的初始化顺序进行初始化
  3. 将内存地址返回给栈中的引用变量

由于指令重排(你可以把它理解成编译器为了优化代码而对实际写出的代码在机器中进行重新排序导致原本的代码执行顺序发生变化)的缘故会出现这样的情况

  1. 在堆中开辟对象所需空间,分配地址
  2. 将内存地址返回给栈中的引用变量(此时变量已不为null,但是变量却并没有初始化完成)
  3. 根据类加载的初始化顺序进行初始化

在多线程下指令重排给带来的问题就会被放大

执行顺序Thread1Thread2
1第一次检测, instance 为null
2获取锁
3第二次检测, instance 为null
4在堆中分配内存空间
5instance 指向分配的内存空间
6第一次检测,instance不为null
7此时Thread 2对 instance的访问,访问到的是一个还未完成初始化的对象。所以在使用 instance 时可能会出错
8初始化 instance

所以为了避免指令重排我们只需要对初始化对象加volatile这一关键字即可,volatile是可以预防指令重排

下面代码是正确的双重检测锁机制

/**
 * 正确的双重检测锁机制
 *
 * @author ccy
 * @version 1.0
 * @date 2021/12/6 13:18
 */
class Order {
    //1.私有化构造器,不允许外部可以调用
    private Order() {

    }

    //2.声明当前类的对象
    //4.要求此对象必须声明为静态
    private volatile static Order instance = null; // 注意哦这里添加了关键字volatile为了防止指令重排

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

虽然这种单例模式线程安全虽然构造方法是私有的但是Java中反射可以破坏私有方法,我们仍然可以通过反射来获取对象

反射破坏双重检测锁机制

public class Lazy {
    private Lazy() {
        System.out.println(Thread.currentThread().getName());
    }
    private volatile static Lazy lazy;

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

    public static void main(String[] args) throws Exception {
        Lazy instance1 = Lazy.getInstance();
        Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        Lazy instance2 = declaredConstructor.newInstance();
        System.out.println(instance1 == instance2);
    }
}

在这里插入图片描述

枚举

没办法了只能搬出杀手锏了枚举元素天生就是线程安全的单例,调用效率也高,只是无法延时加载!

main方法中是我尝试使用反射来破坏枚举单例

public enum EnumSingle {

    INSTANCE;

    public static EnumSingle getInstance() {
        return INSTANCE;
    }
}
//尝试使用反射破坏枚举单例
class TestSingle {
    public static void main(String[] args) throws Exception {
        EnumSingle instance = EnumSingle.getInstance();
//        EnumSingle instance1 = EnumSingle.INSTANCE;
//        System.out.println(instance == instance1);
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();
        System.out.println(instance == instance2);
    }
}

虽然IDEA报错了但是错误并不是我们预计的错误

在这里插入图片描述

但从.class文件来看确实是存在无参构造的方法经过一系列手段后我们了解到实际上调用的是有参的构造方法

Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);调整代码以后报错成了我们预计的

在这里插入图片描述
单例模式我们今天就学到这里吧,如果博文对你有帮助
在这里插入图片描述

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

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