我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Java单例模式与破坏单例模式概念原理深入讲解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Java单例模式与破坏单例模式概念原理深入讲解

什么是单例模式

经典设计模式又分23种,也就是GoF 23 总体分为三大类:

  • 创建型模式
  • 结构性模式
  • 行为型模式

Java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例、饿汉式单例、登记式单例。

单例模式有以下特点:

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

  单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态。

饿汉式(预加载)

饿汉式单例: 在类加载时,就会创建好将会使用的对象,可能会造成内存的浪费

示例:

public class Hungry {
    // 创建唯一实例
    private final static Hungry HUNGRY = new Hungry();
    private Hungry(){}
	// 全局访问点 ---> 拿到HUNGRY实例
    public static Hungry getIntance(){
        return HUNGRY;
    }
}

而预加载就是先一步加载,我们没有使用该单例对象但是已经将其加载到内存中,那么就会造成内存的浪费

懒汉式(懒加载)

懒汉式改善了饿汉式浪费内存的问题,等到需要用到实例的时候再去加载到内存中

懒汉式写法( 线程不安全 ):

public class LazyMan {
    private LazyMan(){}
    public static LazyMan lazyMan;
    public static LazyMan getInstance(){
        if (lazyMan==null){
           lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

但是在不进行任何同步干预的情况下,懒汉式不是线程安全的单例模式,经典的解决方案就是利用双重检验锁保证程序的原子性和有序性,如下示例:

public class LazyMan {
    private LazyMan(){}
    // 懒汉当中的双重检验锁 --> 可以保证线程安全
    public volatile static LazyMan lazyMan;
    // volatile 保证了new实例时不会发生指令重排
    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                // 此处上锁  以保证原子操作
                if (lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}

反射破坏单例模式

反射是一种动态获取类资源的一种途径,我们让然可以通过反射来获取单例模式中的更多实例:

public class LazyMan {
    // 空参构造器
    private LazyMan(){}
    // 懒汉当中的双重检验锁 --> 可以保证线程安全
    public volatile static LazyMan lazyMan;
    // volatile 保证了new实例时不会发生指令重排
    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                // 此处上锁  以保证原子操作
                if (lazyMan == null){
                    lazyMan = new LazyMan();// 不是原子操作
                }
            }
        }
        return lazyMan;
    }
    public static void main(String[] args) throws Exception{
        // 获取无参构造器
        Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);// 无视私有
        // 获取实例
        LazyMan instance2 = constructor.newInstance();
        LazyMan instance3 = constructor.newInstance();
        LazyMan instance4 = constructor.newInstance();
        // 懒汉式单例 获取唯一实例
        LazyMan instance = LazyMan.getInstance();
        System.out.println("getIntance获取实例(1)hashCode:"+instance.hashCode());
        System.out.println("反射构造器newIntance获取实例(2)hashCode:"+instance2.hashCode());
        System.out.println("反射构造器newIntance获取实例(3)hashCode:"+instance3.hashCode());
        System.out.println("反射构造器newIntance获取实例(4)hashCode:"+instance4.hashCode());
    }
}

上述程序输出结果如下:

修复方式1:

// 对空参构造器进行上锁 并对唯一实例lazyman判断是否已经初始化
 private LazyMan(){
    if (lazyMan != null){
		throw new RuntimeException("不要试图破坏单例模式");
    }
 }

但是这种修复方式仍然会被破坏,我们首先是利用了反射来获取LazyMan的空参构造器,并利用其构造器进行初始化获取实例,但是如果我们一直不调用getIntance方法来初始化lazyman实例而一直用反射获取,那么这种方式就形同虚设

因此,得出下一个修复方式。我们依然对空参构造器进行上锁,然后利用标志位保证我们的空参构造器只能使用一次,也就是最多只能为一个实例进行初始化。

修复方式2:

// 解决2.  对空参构造器进行上锁  利用标志位保证空参构造器只能初始化一次实例  但是标志位字段仍可以通过其他途径被拿到  并且修改
    private static boolean flag = false;
	private LazyMan(){
        synchronized(LazyMan.class){
            if (flag == false){
                flag = true;
            }else {
                throw new RuntimeException("不要试图破坏单例模式");
            }
        }
    }

上述代码所示,利用flag作为标志位来保证空参构造器只能对最多一个实例执行初始化操作。但是,同时我们所设置的标志位flag同样存在被通过各种渠道拿到的风险,比如反编译。拿到flag标志后就可以对其修改,示例:

public static void main(String[] args) throws Exception{
        // 获取无参构造器
        Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);// 无视私有
        // 懒汉式单例 获取唯一实例
        LazyMan instance = LazyMan.getInstance();  // (1)
        // 获取标志位字段并进行修改
        Field flag1 = LazyMan.class.getDeclaredField("flag");
        // (1) 处已经调用了空参构造器  flag变为true  此处修改为false 可以继续创建实例
        flag1.set(instance,false);
        LazyMan instance2 = constructor.newInstance();
        // 与上述同理
        flag1.set(instance2,false);
        LazyMan instance3 = constructor.newInstance();
        System.out.println("getIntance获取实例(1)hashCode:"+instance.hashCode());
        System.out.println("反射构造器newIntance获取实例(2)hashCode:"+instance2.hashCode());
        System.out.println("反射构造器newIntance获取实例(3)hashCode:"+instance3.hashCode());
    }

那么既然如此,是不是单例程序无论如何设计最终都会被反射破坏呢?

事实并非如此,我们打开反射得到的构造器.newInstance方法源码查看:

// 我们只看如下两行
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");

如上述代码所示,Java给出的解释为:

如果实际参数和形式参数的数量不同;如果原始参数的展开转换失败;或者如果在可能展开之后,参数值不能通过方法调用转换转换为相应的形式参数类型;如果此构造函数属于枚举类型。符合上述任一情况将会抛出IllegalArgumentException("Cannot reflectively create enum objects")非法参数异常

也就是说,枚举类型是可以避免单例模式被破坏的

public enum enumSingle {
    INSTANCE;
    public enumSingle getInstance() {
        return INSTANCE;
    }
}
class TestEnumSingle{
    public static void main(String[] args) throws Exception {
        // 下面我们尝试用反射来破坏枚举类
        // 枚举类的构造器实际上带有两个参数 String和int
        Constructor<enumSingle> declaredConstructor = enumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        // 直接获取实例
        enumSingle instance = enumSingle.INSTANCE;
        // 反射获取实例
        enumSingle enumSingle1 = declaredConstructor.newInstance();
        System.out.println("类名直接访问获取实例hashCode:"+instance.hashCode());
        System.out.println("反射实例hashCode:"+enumSingle1.hashCode());
    }
}
// 最终抛出  java.lang.IllegalArgumentException: Cannot reflectively create enum objects

除了反射会打破单例之外,序列化Serializable也同样会破坏单例模式,具体体现是物品们同一对象在序列化前和反序列化之后不是同一对象

到此这篇关于Java单例模式与破坏单例模式概念原理深入讲解的文章就介绍到这了,更多相关Java单例模式内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Java单例模式与破坏单例模式概念原理深入讲解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Java单例模式与破坏单例模式概念原理深入讲解

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建
2023-02-21

Java单例模式与破坏单例模式的概念是什么

本文小编为大家详细介绍“Java单例模式与破坏单例模式的概念是什么”,内容详细,步骤清晰,细节处理妥当,希望这篇“Java单例模式与破坏单例模式的概念是什么”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。什么是单例
2023-07-05

Java单例模式的攻击与防御怎么理解

这篇文章主要介绍“Java单例模式的攻击与防御怎么理解”,在日常操作中,相信很多人在Java单例模式的攻击与防御怎么理解问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Java单例模式的攻击与防御怎么理解”的疑
2023-06-02

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录