首页 > 其他分享 >单例模式详解

单例模式详解

时间:2023-09-18 21:55:21浏览次数:58  
标签:scpD 模式 flag 详解 static 单例 import ScpD public

饿汉单例模式

package com.std.www.singletonmode;

import java.util.UUID;

public class ScpD {
    private final static ScpD scpD=new ScpD();

    public static ScpD getScpD() {
        return scpD;
    }
}

类一经创建就会给对象分配内存,这种方式会造成不必要的内存浪费

懒汉单例模式

package com.std.www.singletonmode;

import java.util.UUID;

public class ScpD {
    private static ScpD scpD;
    
    private ScpD(){
        
    }

    public static ScpD getScpD() {
        if(scpD==null) scpD=new ScpD();
        return scpD;
    }
}

这里构造方法被定义为私有,只有在调用的时候才会给对象分配内存,但是该方式在多线程下有问题

package com.std.www.singletonmode;

import java.util.UUID;

public class ScpD {
    private static ScpD scpD;

    private final String DId;
    private ScpD(){
        this.DId = "D级人员:编号" + UUID.randomUUID().toString().substring(0, 4);
    }
    public static ScpD getScpD() {
        if(scpD==null) scpD=new ScpD();
        return scpD;
    }
    public void show(){
        System.out.println(this.DId);
    }

}

package com.std.www;

import com.std.www.singletonmode.ScpD;

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

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        for(int i=0;i<10;i++){
            new Thread(()->{ScpD scpD=ScpD.getScpD();scpD.show();}).start();
        }
    }
}

可以发现创建了10个对象,并不是单例模式,对此我们需要给该类上一把锁

 

package com.std.www.singletonmode;

import java.util.UUID;

public class ScpD {
    private volatile static ScpD scpD;//volatile关键字防止指令重排

    private final String DId;
    private ScpD(){
        this.DId = "D级人员:编号" + UUID.randomUUID().toString().substring(0, 4);
    }
    public static ScpD getScpD() {
        if(scpD==null)//双重检测锁模式DCL懒汉式单例,synchronized用于同步多线程,表示该代码块在该类中一次只能由一个线程执行,volatile防止指令重排
            synchronized (ScpD.class) {
                //正常指令顺序:1.分配内存空间2.初始化对象3.对象引用该内存
                //多线程可能导致出现空指针异常即改变了原有的执行顺序,比如先引用该内存在对象初始化之前
                if (scpD == null) {
                    scpD = new ScpD();
                }
            }
        return scpD;
    }
    public void show(){
        System.out.println(this.DId);
    }

}

package com.std.www;

import com.std.www.singletonmode.ScpD;

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

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        for(int i=0;i<10;i++){
            new Thread(()->{ScpD scpD=ScpD.getScpD();scpD.show();}).start();
        }
    }
}

上面虽然解决的多线程问题, 但是依然不安全,看下面的例子

package com.std.www;

import com.std.www.singletonmode.ScpD;

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

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        ScpD scpD=ScpD.getScpD();
        //反射获取构造器
        Constructor<? extends ScpD> scpDecCon = scpD.getClass().getDeclaredConstructor();
        //设置私有构造器可访问
        scpDecCon.setAccessible(true);
        //通过构造器新建对象
        ScpD scpD1=scpDecCon.newInstance();
        scpD.show();
        scpD1.show();
    }
}

 这里发现通过反射依然可以破坏掉单例模式的安全性,下面的是解决办法,修改构造器再次判断

synchronized (ScpD.class) {//三重检测
    if (scpD != null) {
        throw new RuntimeException("检测到有反射企图破坏单例模式");
    }
    this.DId = "D级人员:编号" + UUID.randomUUID().toString().substring(0, 4);
}

不过这里依然有一个小问题,那就是如果一开始就没有使用单例创建对象的话

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
    //反射获取构造器
    Constructor<? extends ScpD> scpDecCon = ScpD.class.getDeclaredConstructor();
    //设置私有构造器可访问
    scpDecCon.setAccessible(true);
    //通过构造器新建对象
    ScpD scpD1=scpDecCon.newInstance();
    ScpD scpD2=scpDecCon.newInstance();
    scpD1.show();
    scpD2.show();
}

对此我们可以设立一个判断依据

private ScpD() {
    private static boolean flag=false;
    synchronized (ScpD.class) {//三重检测
        if (flag) {
            throw new RuntimeException("检测到有反射企图破坏单例模式");
        }
        else flag=true;
        this.DId = "D级人员:编号" + UUID.randomUUID().toString().substring(0, 4);
    }
}

 不过这里依然有破解办法

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
   //反射获取私有属性字段
    Field flag=ScpD.class.getDeclaredField("flag");
    //设置字段可访问
    flag.setAccessible(true);
    //反射获取构造器
    Constructor<? extends ScpD> scpDecCon = ScpD.class.getDeclaredConstructor();
    //设置私有构造器可访问
    scpDecCon.setAccessible(true);
    //通过构造器新建对象
    //flag:false
    ScpD scpD1=scpDecCon.newInstance();
    //flag:true
    //设置字段属性
    flag.set(flag,false);
    //flag:false
    ScpD scpD2=scpDecCon.newInstance();
    //flag:true
    scpD1.show();
    scpD2.show();
}

这里我们可以搞一个小小的优化

private static enum Flag{
    ISNULL,
    NOTNULL
}
private static Flag flag=Flag.ISNULL;
private ScpD() {
    synchronized (ScpD.class) {//三重检测
        if (flag==Flag.ISNULL) {
            throw new RuntimeException("检测到有反射企图破坏单例模式");
        }
        else flag=Flag.NOTNULL;
        this.DId = "D级人员:编号" + UUID.randomUUID().toString().substring(0, 4);
    }
}

这样要想改变字段的值必须先获取得到该私有的枚举类,然后通过分析得到枚举类中的值,虽然也能通过反射改变但是要比前者复杂不少

通过源码我们可以发现

反射是无法构造一个枚举类型的对象,因此如果可以选择枚举类型作为单例模式是最为安全的

由此可得单例模式的安全性受诸多因数影响

标签:scpD,模式,flag,详解,static,单例,import,ScpD,public
From: https://www.cnblogs.com/liyiyang/p/17713176.html

相关文章

  • Python3 ACM模式的输入输出处理
    python3ACM模式的输入输出例子教学_amc模式python读取输入_汀、人工智能的博客-CSDN博客Python的输入是字符串,所以要自己转类型strip去掉左右两端的空白符,返回strslipt把字符串按空白符拆开,返回[str]map把list里面的值映射到指定类型,返回[type]EOF用抓异常print后面加逗号......
  • MySQL三大日志(binlog、redo log和undo log)详解
    硬核干货!一文掌握binlog、redolog、undolog(qq.com)MySQL日志:undolog、redolog、binlog(qq.com)MySQL三大日志(binlog、redolog和undolog)详解|JavaGuide(Java面试+学习指南)MySQL日志主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其......
  • 02-示波器使用详解
    目录一.刻度与位置调节二.Cursor按钮使用三.触发电平旋钮四.菜单设置五.探头上的10x,1x以及补偿旋钮一.刻度与位置调节1.如图上面两个位置旋钮分别调节波形的垂直和水平位置,波形的高度和宽度不会发生变化.2.如图下面两个标度旋钮分别调节垂直和水平一个方格的刻度,旋......
  • MySQL事务隔离级别详解
    MySQL事务隔离级别详解|JavaGuide(Java面试+学习指南)事务隔离级别总结SQL标准定义了四个隔离级别:READ-UNCOMMITTED(读取未提交):最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。READ-COMMITTED(读取已提交):允许读取并发事务已经提交的数......
  • oracle19c(CDB模式)_获取所有对象&&表数据脚本(迁移后数据比对)
    oracle19c进行数据库迁移后,数据比对靠人工的话比较麻烦,通过如下脚本可以直接取数,获取对象及数据结果文件后,通过notpad++即可进行对比脚本内容如下--------------------------------------------------------------------------------------------------------------------------......
  • 视频汇聚/视频云存储/视频监控管理平台EasyCVR分发rtsp流起播慢优化步骤详解
    安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快,可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等,以及支持厂家私有协议与SDK接入,包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安防视频监控的能力,也具备接入AI智能分析的......
  • HeadFirst设计模式学习之OO设计模式入门
    【一】引入---鸭子无论在哪门编程语言中,都离不开我们最熟悉的鸭子模型,因此作者在引入部分也是利用鸭子作为案例引入我们进行入门的学习【1】鸭子游戏现在我们需要做一款模拟鸭子游泳的游戏在游戏中,有不同的鸭子,不同的鸭子都会游泳和呱呱叫而这款游戏的实现思路就是一......
  • 视频汇聚/视频云存储/视频监控管理平台EasyCVR分发rtsp流起播慢优化步骤详解
    安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快,可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等,以及支持厂家私有协议与SDK接入,包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安防视频监控的能力,也具备接入AI智能分析的......
  • KingbaseES V8R3集群运维案例之---主库数据库服务down后failover切换详解
    案例说明:对KingbaseESV8R3集群,主库数据库服务down后,failover切换进行分析,详解其执行切换的过程,本案例可用于对KingbaseESV8R3集群failover故障的分析参考。适用版本:KingbaseESV8R3集群架构:node_id|hostname|port|status|lb_weight|role|select_cnt......
  • KingbaseES V8R3集群运维案例之---流复制异步同步及全同步模式配置
    案例说明:通过案例描述KingbaseESV8R3集群异步、同步及全同步强一致性配置,本案例为一主二备的架构。适用版本:KingbaseESV8R3集群架构:集群复制配置参数说明:1)sync_flag[kingbase@node101bin]$cat../etc/HAmodule.conf|grep-isync_#1->synchronouscluster,0->async......