首页 > 编程语言 >模式-单例模式-java

模式-单例模式-java

时间:2024-09-11 15:05:39浏览次数:10  
标签:Singleton java 模式 实例 线程 private 单例 static public

目录

前言

最近看Effective Java看到了一点关于单例模式的内容,结合自己所知,在此做个总结归纳。

单例模式

单例模式(Singleton Pattern)是一种常用的软件设计模式,用于限制一个类的实例化次数,确保在整个程序运行期间,该类只有一个实例存在,并提供一个全局访问点来访问这个实例。

单例模式的几种实现方式

普遍来说,单例模式的实现主要有两种方式:

  • ​ 饿汉式:类加载时该单实例对象被创建。
  • 懒汉式:首次使用该对象时,该单实例对象才会被创建。

补充:

  • 枚举

饿汉式

  • 思路:在类加载时就创建好一个静态实例,因此类加载器保证了单例的唯一性。
饿汉式-静态变量写法
  • 优点:简单易懂,不需要加锁。
  • 缺点:无论是否需要,都会在类加载时创建实例,可能造成资源浪费。
public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}
饿汉式-静态代码块写法
  • 实现逻辑与静态变量写法写法基本一致,优缺点同上。
public class Singleton {
    private static final Singleton INSTANCE ;

	static{
	INSTANCE  = new Singleton()
	}
    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

懒汉式

  • 总体思想:在第一次使用时才创建实例。
懒汉式-经典写法
  • 思想:第一次使用时才创建实例。
  • 优点:可以延迟加载。
  • 缺点:在多线程环境下可能会产生多个实例,需要加锁来保证线程安全。
public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {//在多线程情况下,理论上有可能出现多个线程同时进入了该判断体。
            instance = new Singleton();
        }
        return instance;
    }
}
懒汉式-同步方法(不推荐)
  • 思想:在每次调用getInstance方法时都进行同步,从而确保即使在多线程环境下,也不会创建出多个实例。
  • 优点:延迟加载、线程安全。
  • 缺点:我们知道,绝大大部分情况下资源冲突并不会频繁发生,而每次调用getInstance方法时都需要进行同步操作,这会导致性能下降。尤其是在高并发情况下,同步操作可能会成为瓶颈。
public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static synchronized  Singleton getInstance() {
        if (instance == null) {//在多线程情况下,理论上有可能出现多个线程同时进入了该判断体。
            instance = new Singleton();
        }
        return instance;
    }
}
懒汉式-双重检查锁(推荐)
  • 思想:使用双重检查锁定(Double-Checked Locking),只在必要时进行同步。
  • 优点:延迟加载,并且线程安全。
  • 缺点:实现稍微复杂一些。
public class Singleton {
    private volatile static Singleton instance;//注意volatile 

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {//检查是否未初始化
            synchronized (Singleton.class) {//注意,在一个线程拿到锁后,可能有多个线程阻塞在该部分,在当前线程完成初始化操作后,他们也是有机会拿到锁的,因而需要在锁内部再加一个判断
                if (instance == null) {//为了保证其他线程对instance的可见性,instance 应该声明为volatile 
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

注意:

  • 在双重检查锁定中,instance变量需要被声明为volatile,以确保多线程环境下对instance的可见性和有序性
懒汉式-静态内部类(推荐)
  • 思想:控制类加载的时机,利用类加载机制保证初始化实例时只有一个线程。
  • 优点:既实现了懒加载,又能保证线程安全,而且代码简洁。
public class Singleton {
    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

存在的问题

简单来说,私有构造方法并不是绝对安全的,仍可通过一定方式拿到它,请看如下代码:

public static void main(String[] args) throws Exception {

        Singleton s = Singleton.getInstance();//这个Singleton可以是以上某一个
        Constructor<Singleton > constructor = Singleton .class.getDeclaredConstructor();
        constructor.setAccessible(true);//打开可访问性
        Singleton sf = constructor.newInstance();//获取其无参构造方法
        System.out.println(s == sf);//比较引用
    }

在此情况下,我们通过反射拿到了该类的一个实例,与原实例比较引用,会发现,其指向并不相同。
当然,反射问题属于比较极端的问题。但是,其在序列化和反序列化下也并不安全,假设我们的实例类实现了 Serializable接口(以上代码未实现)。

 Singleton s = Singleton.getInstance();//这个Singleton可以是以上某一个
 byte[] serialize = SerializationUtils.serialize(s);
 Object deserialize = SerializationUtils.deserialize(serialize);
 System.out.println(s == deserialize); //true or false --> false

与原实例比较引用,会发现,其指向并不相同。关于这一部分,实际上有个机制:

当一个实现了Serializable接口的单例对象被序列化后,再通过反序列化操作恢复时,默认情况下会生成一个新的对象实例。这意味着即使原始对象是单例的,反序列化后的对象也将是一个新的实例。

为了保证序列化安全,需要在单例类中定义readResolve方法。这个方法将在反序列化过程中被调用,用来返回一个替代对象。通过readResolve方法返回单例的现有实例,可以确保序列化和反序列化过程中始终只有一个实例。如:

public class Singletonimplements Serializable {

    private static class LazyHolder {
        private static final SingletonINSTANCE = new MyTest();
    }

    private Singleton() {
    }

    public static final SingletongetInstance() {
        return LazyHolder.INSTANCE;
    }

    private Object readResolve() {//see 
        return LazyHolder.INSTANCE;
    }
 }

枚举(天然适合)

事实上,枚举实现单例模式的方式是基于语言级别的支持,它不仅简洁,而且天然具备线程安全性和序列化安全性。
比如:

  • 反射安全方面:在Constructor源码中,当调用newInstance创建对象时,会检查该类是否为ENUM,如果是则抛出异常,也就是说即使拿到了该枚举类的构造方法,也无法通过反射来建立它的实例。
  • 序列化安全方面:当枚举对象被序列化时,只会将枚举常量的名字(name)输出到结果中,而不是整个对象的状态。在反序列化时,Java 会通过调用 java.lang.Enum.valueOf(Class, String) 方法来根据名字查找枚举常量,而不是创建一个新的枚举对象。。
  • 线程安全:由反编译可知,枚举常量的初始化是在静态代码块中完成的,在类加载时完成初始化,而类加载是由JVM保证线程安全的。
public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() {
        System.out.println("Elvis has left the building.");
    }
}

总结

  • 饿汉式:在类加载时创建实例,简单易懂,无需加锁。
  • 懒汉式:延迟创建实例,需考虑线程安全问题。
    • 经典写法:非线程安全。
    • 同步方法:线程安全但性能较差。
    • 双重检查锁(DCL):线程安全且性能较好。
    • 静态内部类:线程安全且简洁。
  • 枚举:简洁且天然具备线程安全性和序列化安全性,防止反射破坏。

标签:Singleton,java,模式,实例,线程,private,单例,static,public
From: https://www.cnblogs.com/bbban/p/18408247

相关文章

  • 《JavaEE进阶》----14.<SpringMVC配置文件实践之【验证码项目】>
    本篇博客介绍的是Google的开源项目Kaptcha来实现的验证码。这种是最简单的验证码。也是很常见的一种验证码。可以去看项目结果展示。就可以明白这个项目了。前言:随着安全性的要求越来越高、很多项目都使用了验证码。如今验证码的形式也是有许许多多、更复杂的图形验证码......
  • Java语言程序设计基础篇_编程练习题*18.10 (字符串中某个指定字符出现的次数)
    目录题目:*18.10(字符串中某个指定字符出现的次数)习题思路代码示例 输出结果题目:*18.10(字符串中某个指定字符出现的次数)  编写一个递归方法,使用下面的方法头给出一个指定字符在字符串中出现的次数。publicstaticintcount(Stringstr,chara) 例如,coun......
  • Java小白一文讲清Java中集合相关的知识点(八)
    HashMap底层机制及源码剖析HashMap底层维护了Node类型的数组table,默认为null当创建对象时,将加载因子初始化为0.75;当添加key-value时,通过key的哈希值得到在table的索引,然后判断该索引处是否有元素,如果没有元素直接添加,如果该索引处有元素,继续判断该元素的key是否和......
  • Java中的线程优先级与调度:如何有效管理线程的执行顺序
    Java中的线程优先级与调度:如何有效管理线程的执行顺序大家好,我是微赚淘客返利系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!在Java中,线程的优先级和调度策略对于高效管理线程执行顺序至关重要。通过合理地设置线程优先级和调度策略,可以有效地优化应用的性能和响应时间。本......
  • 如何在Java服务中使用Circuit Breaker模式:Hystrix与Resilience4j的比较
    如何在Java服务中使用CircuitBreaker模式:Hystrix与Resilience4j的比较大家好,我是微赚淘客返利系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!在分布式系统中,服务调用的稳定性和可靠性至关重要。CircuitBreaker(熔断器)模式可以有效地防止服务故障的蔓延,保护系统的稳定性。本......
  • Java服务端中的数据验证:使用Bean Validation与Spring Validator的最佳实践
    Java服务端中的数据验证:使用BeanValidation与SpringValidator的最佳实践大家好,我是微赚淘客返利系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!在Java服务端开发中,数据验证是确保应用数据准确性和可靠性的关键步骤。本文将探讨BeanValidation和SpringValidator这两种数......
  • Java的class与String互相转换,自定义密码策略
    目的客户要求密码校验方式,用自己的认证方式。提供一种方案,在不出补丁的情况下,解决这个问题。原理1、本地写一个类,用客户想要的方案,实现密码校验的接口,编译成class类。2、然后把这个class类,先转换成二进制,再转换成16进制的字符串。3、将字符串写到客户的数据库里。4、重启服务,在......
  • Java中的缓存穿透与雪崩问题:解决方案与设计模式
    Java中的缓存穿透与雪崩问题:解决方案与设计模式大家好,我是微赚淘客返利系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!在分布式系统中,缓存是提高性能的重要手段。然而,缓存系统在实际应用中常常会遇到缓存穿透和缓存雪崩这两种问题。本文将探讨这两种问题的成因以及在Java中......
  • Java中的负载测试:从单元测试到集成测试的完整覆盖策略
    Java中的负载测试:从单元测试到集成测试的完整覆盖策略大家好,我是微赚淘客返利系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!今天我们来聊一聊Java中的负载测试。负载测试是保证系统性能和稳定性的重要手段,而完整的测试策略不仅包括单元测试,还要覆盖到集成测试。本文将从单......
  • 如何在Java中实现应用的动态扩展:基于热插拔与插件机制的实现
    如何在Java中实现应用的动态扩展:基于热插拔与插件机制的实现大家好,我是微赚淘客返利系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!在现代应用开发中,为了应对不断变化的需求和快速迭代的要求,应用的动态扩展能力变得尤为重要。实现动态扩展的关键技术包括热插拔和插件机制。......