深入学习单例设计模式

news2024/11/17 23:41:46

目录

一.单例模式的定义

 二.单例模式的实现方式

1.懒汉模式:

2.饿汉模式

3.静态内部类方式

4.反射模式

5.枚举方式

6.序列化方式

三.单例模式的应用


一.单例模式的定义

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

使用的场景:重量级的对象、不需要多个实例,以及我们想复用的对象,如线程池,数据库连接池等 等。Spring中的IOC容器是单例对象,JDK的Runtime也是单例对象。

单例模式的类图:

 二.单例模式的实现方式

1.懒汉模式:

    懒汉模式:延迟加载的方案,只有我们在使用的时候才实例化

    先来看看版本1:   

package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;

public class LazySingleton {

    private static LazySingleton lazySingleton ;

    private LazySingleton() {
    }

    public static LazySingleton getLazySingleton(){

        if(lazySingleton == null){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lazySingleton =new LazySingleton();
        }
        return lazySingleton ;
    }


    public static void main(String[] args) {

        new Thread(()->{
            System.out.println(LazySingleton.getLazySingleton());
        }).start();
        new Thread(()->{
            System.out.println(LazySingleton.getLazySingleton());
        }).start();
    }
}

  运行结果:

 多个线程的情况,每个线程使用的不是一个对象,这根本就不是一个单例。

先来看看版本2:   将synchronized写在if条件的里面

加锁

package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;

public class LazySingleton2 {

    private static LazySingleton2 lazySingleton ;

    private LazySingleton2() {
    }

    public static LazySingleton2 getLazySingleton(){
        if(lazySingleton == null){
            synchronized (LazySingleton2.class){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lazySingleton =new LazySingleton2();
            }
        }
        return lazySingleton ;
    }


    public static void main(String[] args) {

        new Thread(()->{
            System.out.println(LazySingleton2.getLazySingleton());
        }).start();
        new Thread(()->{
            System.out.println(LazySingleton2.getLazySingleton());
        }).start();
    }
}

运行结果:

依然没有实现单例,为什么呢?

 

 看图,多线程情况下,t1,t2,t3,t4都执行到这里了,假如t1获得了锁,那么进入代码块中新建了一个对象,执行之后释放锁,接着t2竞争到了锁,t2又重新创建了一个对象,这样又不符合单例模式了。

再来看看版本3:将synchronized写在if的外面

package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;

public class LazySingleton2 {

    private static LazySingleton2 lazySingleton ;

    private LazySingleton2() {
    }

    public static LazySingleton2 getLazySingleton(){
        synchronized (LazySingleton2.class){
            if(lazySingleton == null){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lazySingleton =new LazySingleton2();
            }
        }
        return lazySingleton ;
    }


    public static void main(String[] args) {

        new Thread(()->{
            System.out.println(LazySingleton2.getLazySingleton());
        }).start();
        new Thread(()->{
            System.out.println(LazySingleton2.getLazySingleton());
        }).start();
    }
}

执行结果:

 这次是只有一个对象了,那么这个版本有没有什么问题呢?

多线程的情况下,每一个线程到这里都得等待锁,这样性能是很低的,

就算单例对象已经产生了,线程依然拿不到这个单例对象,还要等待锁。

所以得在synchronized外面再加一个if判断

 继续来看看版本4:将synchronized写在两个if的中间

package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;

public class LazySingleton3 {

    private static LazySingleton3 lazySingleton ;

    private LazySingleton3() {
    }

    public static LazySingleton3 getLazySingleton(){
        if(lazySingleton == null) {
            synchronized (LazySingleton3.class) {
                if (lazySingleton == null) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    lazySingleton = new LazySingleton3();
                }
            }
        }
        return lazySingleton ;
    }


    public static void main(String[] args) {

        new Thread(()->{
            System.out.println(LazySingleton3.getLazySingleton());
        }).start();
        new Thread(()->{
            System.out.println(LazySingleton3.getLazySingleton());
        }).start();
    }
}

运行结果依然是正确的,确保了单例,这就是单例的DCL模式(Double check lock)

针对这个版本4,还有什么问题呢?

那就是对象创建的过程中的重排序问题

这里还是和jvm基础知识相关,看看学好jvm多么的重要

new一个对象的过程:(这也可能是一道面试题)
1.检查类是否加载
   如果没有加载,那么就去加载类
2.为对象在堆中分配一块内存空间
3. 初始化 
 3.1对象实例变量初始化为0
 3.2设置对象头(比如锁状态、对象在Survivor区挺过gc的次数都在这里设置)
 3.3调用init<>方法:对象实例变量被赋予程序员想给的值,调用构造函数
4.将内存地址赋给引用这个对象的变量

其中因为即时编译器JIT或者CPU都可能会在物理层面上将第3步和第4步重新排序,
也就是说第4步先执行,第3步后执行  3,4步是没关系的
即:对编译器或者CPU都可能会在物理层面上对字节码进行指令重排

那么lazySingleton = new LazySingleton3();这里就有问题了,多线程的情况下,T1正在获取了锁,正在new一个对象,但是其实这个对象还没有new完,但是引用已经指向该对象的地址,这时候T2来了,判断lazySingleton是不是为空,结果是false,这就导致可能T2线程拿到的是一个半成品的对象,执行就会出错。那怎么办呢?

private volatile static LazySingleton3 lazySingleton ;

将lazySingleton 用volatile 修饰即可

原因:volatile可以阻止lazySingleton 引用对应的对象创建时候的重排序。

那么这个懒汉模式的单例的最终版本最终如下代码所示:

package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;

public class LazySingletonVolatile {

    private volatile static LazySingletonVolatile lazySingleton ;

    private LazySingletonVolatile() {
    }

    public static LazySingletonVolatile getLazySingleton(){
        if(lazySingleton == null) {
            synchronized (LazySingletonVolatile.class) {
                if (lazySingleton == null) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    lazySingleton = new LazySingletonVolatile();
                }
            }
        }
        return lazySingleton ;
    }


    public static void main(String[] args) {

        new Thread(()->{
            System.out.println(LazySingletonVolatile.getLazySingleton());
        }).start();
        new Thread(()->{
            System.out.println(LazySingletonVolatile.getLazySingleton());
        }).start();
    }
}

2.饿汉模式

package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;

public class HungrySingleton {

    private static HungrySingleton singleton = new HungrySingleton();

    private HungrySingleton() {
    }

    private static HungrySingleton getHungrySingleton(){
        return singleton ;
    }


    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(HungrySingleton.getHungrySingleton());
        }).start();
        new Thread(() -> {
            System.out.println(HungrySingleton.getHungrySingleton());
        }).start();
    }
}

饿汉模式是很简洁安全(个人认为)的方式

简洁:代码量少

安全:怎么说呢?难道没有多线程并发问题吗?

这就又涉及到JVM的内存知识了

类加载器的种类:引导类加载器 扩展类加载器 应用程序类加载器 自定义类加载器(稍稍复习一下)
类加载的过程:
1.编译器编译完成的.class二进制数据文件被类加载器加入到JVM的数据运行区的方法区,
  之后在堆区生成class对象
2.连接
   2.1.验证:比如是不是开头是cafe babe,
             也就是查看被加载的二进制数据文件是不是符合JVM的规范
   2.2.准备:类的static静态属性赋默认值  int类型赋0 布尔类型赋false 引用类型赋null
   2.3.解析:静态解析,常连池中的符号引用转换成一个直接引用
3.初始化:类的static静态属性赋程序员想给赋的值,当然别忘了静态代码块中的内容也在这时候执行

要说的两点是:
第一:类加载是先于new对象执行的
第二:类加载过程是线程安全的,原子性的,是JVM帮助我们实现线程安全的一种机制。

理解了上述说的,那一定知道了为啥饿汉模式是线程安全的了,类加载之后这个对象就有了,而且JVM帮助我们线程安全的实现了创建对象的过程。

饿汉模式是推荐使用的创建单例模式的方式。

3.静态内部类方式

package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;

/**
 * 静态内部类的方式创建单例模式
 */
public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton() {
    }

    public static StaticInnerClassSingleton getSingleton(){
        return InnerClassSingletonHolder.singleton ;
    }

    public static class InnerClassSingletonHolder{
        private static StaticInnerClassSingleton singleton = new StaticInnerClassSingleton();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                System.out.println(StaticInnerClassSingleton.getSingleton());
            }).start();
        }
    }
}

思考:静态内部类InnerClassSingletonHolder何时被加载呢?

是我们在 返回InnerClassSingletonHolder.singleton (使用)的时候进行内部类的加载,这个过程也是通过JVM类加载的过程保证线程安全。

只有在真正使用对应的类的时候,才会触发初始化,也可以说是类加载,例如:当前类是启动类即main函数所在的类,直接进行new操作,访问静态属性,访问静态方法,用反射访问类,初始化一个类的子类等。

4.反射模式

  这里其实不能说是一种创建单例模式的方式,而是一种反射攻击

 以静态内部类创建单例为例子,进行 反射测试

重点看main方法:

package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * 静态内部类的方式创建单例模式
 */
public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton() {
    }

    public static StaticInnerClassSingleton getSingleton(){
        return InnerClassSingletonHolder.singleton ;
    }

    public static class InnerClassSingletonHolder{
        private static StaticInnerClassSingleton singleton = new StaticInnerClassSingleton();
    }


    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<StaticInnerClassSingleton> constructor
                = StaticInnerClassSingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        StaticInnerClassSingleton staticInnerClassSingleton = constructor.newInstance();

        StaticInnerClassSingleton singleton = StaticInnerClassSingleton.getSingleton();
        System.out.println(staticInnerClassSingleton == singleton);
    }


}

结果:false   (间接说明了反射生成的是新对象)。

这样又破坏了单例模式, 怎么防呢?

在饿汉模式或者静态内部类的模式下,在构造方法中加判断:

package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * 静态内部类的方式创建单例模式
 */
public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton() {
        if(InnerClassSingletonHolder.singleton!=null){
            throw new RuntimeException("单例模式不允许多个实例...");
        }
    }

    public static StaticInnerClassSingleton getSingleton(){
        return InnerClassSingletonHolder.singleton ;
    }

    public static class InnerClassSingletonHolder{
        private static StaticInnerClassSingleton singleton = new StaticInnerClassSingleton();
    }


    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        Constructor<StaticInnerClassSingleton> constructor
                = StaticInnerClassSingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        StaticInnerClassSingleton staticInnerClassSingleton = constructor.newInstance();

        System.out.println(staticInnerClassSingleton);
    }


}

结果:

5.枚举方式

点进constructor.newInstance();方法:

 其中的这块代码:说明如果反射创建对象的类型是枚举类型的话,jdk会帮助我们,不让反射的方式进行对象的创建,也就是防止了反射攻击。

 验证一下:

package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public enum EnumSingleton {

    INSTANCE ;

    public void say(){
        System.out.println(this.hashCode());
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
        constructor.setAccessible(true);
        EnumSingleton instance = constructor.newInstance("INSTANCE", 0);
        instance.say();

    }


}

运行结果:

Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)

所以说枚举类型帮助我们阻止了反射攻击。

看字节码的话,我们就会发现其中的奥秘:枚举类型底层其实也是一种饿汉模式

使用javap 命令:

javap -v  -p F:\enjoystudy\concurrent\out\production\concurrent\com\tuling\learnjuc\sync\xiaoshanshan\singleton_study\EnumSingleton.class

查看字节码:

 可以看到com.tuling.learnjuc.sync.xiaoshanshan.singleton_study.EnumSingleton这个类是继承了java.lang.Enum这个抽象类

 而且构造方法是私有的

 具体的new操作在一个static代码块中执行的。

6.序列化方式

package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;

import java.io.*;

public class SingletonSerializable {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
         // 写出  序列化
         // 此处以饿汉模式为例   而且饿汉类已经实现了Serializable接口
        HungrySingleton instance = HungrySingleton.getHungrySingleton();
        ObjectOutputStream objectOutputStream
                = new ObjectOutputStream(new FileOutputStream("objectInputStream"));
        objectOutputStream.writeObject(instance);
        objectOutputStream.close();

        // 写入  反序列化
        ObjectInputStream objectInputStream
                = new ObjectInputStream(new FileInputStream("objectInputStream"));
        HungrySingleton o = (HungrySingleton)objectInputStream.readObject();
        System.out.println(instance == o);
        objectInputStream.close();

    }
}

运行结果:false

这说明 反序列化已经破坏了单例模式

那怎么办呢?

 在Serializable接口中有这么一段话:就是说从流中读取的对象对应的类应该有一个方法:任意的访问修饰符来修饰的方法

ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

在懒汉的单例类加上这个方法:

 ps:这里还要做的一件事就是:懒汉单例类中 要加上序列版本号,为什么呢?

        比如:第一步,把懒汉单例类对象序列化了

                  第二步,把懒汉单例类中加入了新的方法或者新的属性

                  第三步,把懒汉单例类对象进行反序列化

结果就报错了:

版本号不兼容

 所以要加上版本号

 再测试就是true了。

以上是针对饿汉模式,其实懒汉模式和静态内部类都会被反序列化这样的情况破坏掉单例模式,解决方法如上。

但是针对枚举类型的单例模型,就不会被反序列化破坏掉单例模式。

看看为什么?主要看看这个方法

objectInputStream.readObject();

 F5继续:

 

 Class和Enum处理的方式不一样的,在TC_OBJECT中会检查是不是有readResolve这个方法签名,这个方法就是我们上面要加的方法。

 三.单例模式的应用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/695013.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【table中部分tr的折叠与展开】

示例功能&#xff1a; 1. 点击“作品”按钮&#xff0c;会显示author的作品信息 2. 再次点击“作品”按钮&#xff0c;会收起author的作品信息 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name&quo…

IDEA远程Debug调试工具(Remote)的使用

我们在开发的过程中&#xff0c;经常会遇到这样的情况&#xff1a;代码在本地测试得好好的&#xff0c;但部署上线后测试结果就不一样了&#xff0c;这时就需要去服务器上查看日志进行分析从而定位问题&#xff0c;但这样还是会比较麻烦&#xff0c;如果能够Debug调试&#xff…

CSS实现进度条和订单进度条---竖向

之前做了一个横向订单进度条&#xff0c;手机访问显示很难兼容样式&#xff0c;下面做一个竖向的&#xff0c;再结合情况微调一下&#xff0c;方便去兼容手机。 1.直接放页面 代码如下&#xff08;示例&#xff09;&#xff1a; <!DOCTYPE html> <html xmlns:th"…

【Java高级语法】(二十一)数组操作类:解析Arrays类中的全部操作方法,解锁Java数组操作技巧~

Java高级语法详解之数组操作类 1️⃣ 概念2️⃣ 优势和缺点3️⃣ 使用3.1 Arrays类常用方法3.2 使用技巧 4️⃣ 应用场景&#x1f33e; 总结 前言&#xff1a;在学习本文之前&#xff0c;应该先学习并清楚了解Java基础部分的数组相关的概念和知识。 若还不具备学习条件&#xf…

途乐证券|人工智能概念再度下挫 海天瑞声、寒武纪等跌超10%

人工智能概念26日盘中大幅回调&#xff0c;截至发稿&#xff0c;当虹科技跌近18%&#xff0c;美亚柏科、昆仑万维跌约13%&#xff0c;博睿数据、光云科技、海天瑞声、寒武纪等跌超10%&#xff0c;焦点科技跌停&#xff0c;云从科技、朗玛信息、三六零等跌超9%。 香港途乐证券有…

力扣125:判断字符串是否是回文字符串

题目描述&#xff1a; 如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后&#xff0c;短语正着读和反着读都一样。则可以认为该短语是一个回文串。 字母和数字都属于字母数字字符。 给定一个字符串s&#xff0c;如果它是回文串&#xff0c;返回true&#xff1…

文件批量重命名利器!统一重命名和隐藏编号,让不同类型的文件整齐有序

进入数字时代&#xff0c;我们的电脑中存储了大量的文件&#xff0c;但是随着时间的推移&#xff0c;这些文件名可能变得混乱而难以管理。那么正需要文件批量改名高手来为你排忧解难&#xff0c;不仅可以将不同类型的文件统一重命名&#xff0c;还能隐藏顺序编号&#xff0c;让…

k8s calico 网络异常处理

故障 worker3故障重启后&#xff0c;该节点的 pod 访问不了其它节点服务 2023-06-26T07:44:41.041Z ERROR setup unable to start manager {"error": "Get \"https://10.244.64.1:443/api?timeout32s\": dial tcp 10.244.64.1:443: i/o…

还在用gtest?更好用的测试框架介绍

gtest需要安装有时候带来很多不方便。网络原因&#xff0c;下载安装gtest或者git上拉gtest都可能因为网络原因失败。除了gtest之外&#xff0c;还有很多轻量级易用的单元测试库&#xff0c;比如doctest。 现代C unit-test库 除了gtest之外&#xff0c;还有很多轻量级易用的单元…

Vscode如何快速打开用户的 settings.json 文件

1、打开vscode编辑器&#xff0c;本文演示的vscode是中文版 2、点击右下角齿轮’设置’ 3、在弹出的设置菜单中选择: 设置 ’ 快捷键 cmd, &#xff08;macos&#xff09; ctrl,&#xff08;windows&#xff09; 4、 点击 设置 ’ 后, 弹出设置窗口 5、在设置窗口右上角点击…

一些性能优化思路与策略

一些性能优化思路与策略 1. 前言2. 性能观察指标3.性能监控工具/途径 1. 前言 今天公司同事做技术分享&#xff0c;题目就是&#xff1a;一些性能优化思路与策略&#xff0c;我学习了一下然后做了如下总结。 2. 性能观察指标 响应时间:平均响应时间&#xff0c;TP95、TP99等等…

将字符串“01:03”转换成秒数

场景&#xff1a;我们经常遇到这种场景&#xff0c;用户使用前端的时间组件自定义时间长度&#xff0c;比如antd的timePicker&#xff0c;传递选中的时间给后端&#xff0c;但是在后端中我们一般会使用秒数或者分钟数去保存&#xff0c;而不会保存一个字符串。 这个时候我们需要…

探索OpenCV的AI实现视频超分

OpenCV除了使用光流算法与普通插值实现图像视频超分&#xff0c;还提供AI深度学习实现视频超分。算法模型包括&#xff1a;edsr、espcn、fsrcnn、lapsrn&#xff0c;实现超分的倍数有2、3、4、8。通过AI实现的视频超分比传统算法的效果更好&#xff0c;图像更清晰。 1、超分算…

大数据面试题之Mysql:每日三题(五)

大数据面试题之Mysql:每日三题 1.MySQL索引存储结构(六种)2.on和where的区别3.mysql是怎么查重的&#xff1f;(重点掌握3种&#xff0c;distinct&#xff0c;group by&#xff0c;row_number) 很开心写完上一篇&#xff0c;就立刻找到了我入职的第二家公司&#xff0c;所以我还…

Gitlab保护分支与合并请求

目录 引言 1、成员角色指定 1、保护分支设置 2、合并请求 引言 熟悉了Git工作流之后&#xff0c;有几个重要的分支&#xff0c;如Master(改名为Main)、Develop、Release分支等&#xff0c;是禁止开发成员随意合并和提交的&#xff0c;在此分支上的提交和推送权限仅限项目负责…

电商假货品牌如何应对

随着电商的逐步发展&#xff0c;不同渠道首的产品上架数量呈明显增长&#xff0c;这些链接里会有经销商店铺&#xff0c;或者是非授权店铺&#xff0c;同时伴随的还会有低价、窜货、假货产品。假货大多是低价链接&#xff0c;面对线上逐渐增多的假货链接&#xff0c;品牌又该如…

C++ / QT 旅游产品管理系统

一、项目介绍 旅游产品管理系统 你是一家旅行社的 IT 主管&#xff0c;现在需要你设计并实现一个旅游产品管理系统。 1 &#xff09; 基本功能要求 * 实现基础界面&#xff1a; 参照现有的旅游产品管理系统&#xff1a;查看产品、选择产品、使用说明等内容 * 支持旅游产品…

【每日一题】Leetcode - 283. 移动零

题目 Leetcode - 283. 移动零 解题思路 从右向左遍历&#xff0c;遇到0&#xff0c;就将后面所有元素前移&#xff0c;同时更新长度&#xff0c;使其减1&#xff0c;因为移动n次&#xff0c;倒数n位就被0占据&#xff0c;后续操作可忽略 class Solution {public void moveZ…

G1吊舱全面升级,新增软件驱动库,支持多平台使用

G1吊舱&#xff0c;是我们去年推出的一款自研的云台产品&#xff0c;体积小巧&#xff0c;功能强大&#xff0c;上市后得到了很多朋友的支持。在上市后的一年期间&#xff0c;我们多方收集用户反馈&#xff0c;并基于用户的宝贵建议对G1吊舱进行了软硬件上的升级。 ​ 一、软件…

【ubuntu】【vmware tools】解决重启后看不到共享目录的问题

1、现象 ubuntu 22 vmware 16&#xff0c;安装后会发现 “Reinstall VMware Tools…” 灰色不可用。如图&#xff1a; 2、原因分析 ubuntu 22 ISO 内不再提供 VMware Tools 的安装包&#xff0c;未检测到所以灰色不可用 在 Ubuntu 22 上挂载 Windows HGFS 共享目录&#xff…