首页 > 编程语言 >Java 实现单例模式

Java 实现单例模式

时间:2024-01-16 11:01:25浏览次数:32  
标签:初始化 Java private 枚举 模式 单例 序列化

目录

单例模式简介

单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。

双重检查锁

为了在多线程环境下,不影响程序的性能,不让线程每次调用 getInstanceC() 方法时都加锁,而只是在实例未被创建时再加锁,在加锁处理里面还需要判断一次实例是否已存在。

public class Singleton1 {
    private static volatile Singleton1 instance;

    private Singleton1() {}

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

优缺点

  • 优点

    • 延迟初始化:和懒汉模式一致,只有在初次调用静态方法 getInstance,才会初始化 Singleton1 实例。
  • 缺点

    • 性能优化:同步会造成性能下降,在同步前通过判读 instance 是否初始化,减少不必要的同步开销。

    • 线程安全:同步创建 Singleton1 对象,同时,注意到静态变量 instance 需要使用 volatile 修饰

延迟加载模式(Initialization-on-demand holder idiom)

加载一个类时,其内部类不会同时被加载。

一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。 由于在调用 Singleton2.getInstance() 的时候,才会对单例进行初始化。并且,通过反射,是不能从外部类获取内部类的属性的。

由于静态内部类的特性,只有在其被第一次引用的时候才会被加载,所以可以保证其线程安全性。

public class Singleton2 {
    private Singleton2() {

    }

    public static Singleton2 getInstance() {
        return ContextHolder.getInstance();
    }

    private static class ContextHolder {
        private static final Singleton2 INSTANCE = new Singleton2();
        public static Singleton2 getInstance() {
            return INSTANCE;
        }
    }
}

优缺点

  • 优点:

    • 实现代码简洁:与双重检查单例模式对比,静态内部类单例实现代码更简洁、清晰。

    • 延迟初始化:调用 getInstance 才初始化 Singleton2 对象。

    • 线程安全:JVM 在执行类的初始化阶段,会获得一个可以同步多个线程对同一个类的初始化的锁。

  • 缺点:

    • 需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久带的对象。

饿汉模式

饿汉模式是通过 JVM 在加载类的时候,就完成类对象的创建:

public class Singleton3 {
    private static final Singleton3 INSTANCE = new Singleton3();

    private Singleton3() {

    }

    public static Singleton3 getInstance() {
        return INSTANCE;
    }

}

优缺点

  • 优点:JVM 层面的线程安全

    The semantics of Java guarantee that the field will not be initialized until the field is referenced, and that any thread which accesses the field will see all of the writes resulting from initializing that field. -- 摘自《The "Double-Checked Locking is Broken" Declaration》

    Java 的语义保证了在引用该字段之前,该字段不会被初始化,并且访问该字段的任何线程,都将看到初始化该字段所产生的所有写入。

  • 缺点:造成空间浪费

    饥饿模式是典型的以空间换时间思想的实现: 不用判断就直接创建, 但创建之后如果不使用这个实例, 就造成了空间的浪费. 虽然只是一个类实例, 但如果是体积比较大的类, 这样的消耗也不容忽视.

枚举方式

创建枚举默认就是线程安全的,所以不需要担心 double checked locking,而且,还能防止反序列化,导致重新创建新的对象。保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量)。

public enum Singleton4 {
    INSTANCE(1, "car");

    private Integer id;
    private String name;

    private Singleton4(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    // ... 省略了getter/setter 方法
}

枚举单例模式的线程安全同样利用静态内部类中讲到类初始化锁。枚举单例模式能够在序列化和反射中保证实例的唯一性。

优缺点

  • 优点

    • 不需要考虑序列化的问题:

      枚举对象的序列化是由 JVM 保证的, 每一个枚举类型和枚举变量在 JVM 中都是唯一的, 在枚举类型的序列化和反序列化上 Java 做了特殊的规定: 在序列化时,Java 仅仅是将枚举对象的 name 属性输出到结果中, 反序列化时只是通过 java.lang.Enum#valueOf() 方法来根据名字查找枚举对象。编译器不允许对这种序列化机制进行定制、并且禁用了 writeObjectreadObjectreadObjectNoDatawriteReplacereadResolve 等方法, 从而保证了枚举实例的唯一性。

    • 不需要考虑反射的问题:

      在通过反射方法 java.lang.reflect.Constructor#newInstance() 创建枚举实例时, JDK 源码对调用者的类型进行了判断:

      // 判断调用者clazz的类型是不是Modifier.ENUM(枚举修饰符), 如果是就抛出参数异常:
      if ((clazz.getModifiers() & Modifier.ENUM) != 0)
           throw new IllegalArgumentException("Cannot reflectively create enum objects");
      
  • 缺点: 所有的属性都必须在创建时指定, 也就意味着不能延迟加载; 并且使用枚举时占用的内存比静态变量的2倍还多, 这在性能要求严苛的应用中是不可忽视的.


参考:

标签:初始化,Java,private,枚举,模式,单例,序列化
From: https://www.cnblogs.com/larry1024/p/17967056

相关文章

  • JavaSE(12) - 常用API(下)
    JavaSE(12)-常用API(下)JDK7以前的时间相关类Date类如何创建日期对象Datedate=newDate();//空参构造创建的对象,默认表示系统当前时间Datedate=newDate(指定毫秒值);//有参构造创建的对象,表示指定时间如何修改时间对象中的毫秒值setTime(毫秒值);如......
  • java安装
    1.要使用Java,必须先安装什么?去哪里下载?目前学习jdk17JDK(JavaDevelopmentKit)开发者工具包;Oracle官网(Java软件|Oracle中国) 2.LTS版本有哪些?很多企业还在使用哪个JDK版本?JDK8、11、17、21    很多企业还在使用JDK8/JDK11。3.如何验证JDK是否安装成功了?打......
  • JavaSE(13) - 常见算法 algorithm
    JavaSE(13)-常见算法algorithm查找算法Search基本查找BasicSearchpackagealgorithm.search;/*BasicSearch1.用基本查找,查找某个元素在数组中的索引(不考虑重复元素)2.用基本查找,查找某个元素在数组中的索引(考虑重复元素)*/publicclassBasicSearch{public......
  • web DevOps / css id / css class / javascript / Browser Object Model / bom / Docu
    sNSD_DEVOPS_02CSS概述概念与理解层叠样式表—也就是CSS—是在HTML之后应该学习的第二门技术。HTML用于定义内容的结构和语义,CSS用于设计风格和布局。比如,我们可以使用CSS来更改内容的字体、颜色、大小、间距,将内容分为多列,或者添加动画及其他的装饰效果。修改页......
  • 基于javaPoet的缓存key优化实践
    一.背景在一次系统opsreview中,发现了一些服务配置了@Cacheable注解。@cacheable来源于springcache框架中,作用是使用aop的方式将数据库中的热数据缓存在redis/本地缓存中,代码如下:@Cacheable(value={"per"},key="#person.getId()"+"_"+"#person.getName()")publicPerso......
  • 如何使用Java在Excel中添加动态数组公式?
    本文由葡萄城技术团队发布。转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。前言动态数组公式是Excel引入的一项重要功能,它将Excel分为两种风格:Excel365和传统Excel(2019或更早版本)。动态数组功能允许用户从单个单元格中的公式......
  • Java控制流
    Java流程控制Scanner对象基本语法:Scannerscn=newScanner(System.in); //Scanner类来获取用户的输入​ 通过Scanner类的next()与nextLine()方法获取输入的字符串,在读取前我们一般需要使用hasNext()与hasNextLine()判断是否还有输入的数据。1、next()一定要读取到有效......
  • Java方法
    Java方法一、方法定义与调用1、定义方法语法:[修饰符列表]返回值类型方法名(形式参数列表){ 方法体;}注意:[]符号叫做中括号,以上中括号[]里面的内容表示不是必须的,是可选的方法体由Java语句构成方法定义之后需要去调用,不调用是不会执行的Java都是值传递2、方法调......
  • Java就业学习 Day 1
    Java开发能力:系统学习Java开发的第一天,终于知道Java和JavaSE的区别是什么了。。过完了Java初阶的课程,从Java历史、数据类型到二位数组。Java中阶课程面向对象这一节还没看完,有几点影响还挺深的,大一刚学Java的时候确实没怎么明白。①方法的重载:之前不知道方法重载有什么用,现在......
  • java基础
    Java基础部分基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io的语法,虚拟机方面的语法。1、一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制?可以有多个类,但只能有一个public的类,并且public的类名必须......