Java 单例模式

文章目录

前言

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

单例模式

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

单例模式的几种实现方式

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

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

补充:

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

    private Singleton() {}

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

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

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

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {//在多线程情况下,理论上有可能出现多个线程同时进入了该判断体。
            instance = new Singleton();
        }
        return instance;
    }
}
懒汉式-同步方法(不推荐)
  • 思想:在每次调用getInstance方法时都进行同步,从而确保即使在多线程环境下,也不会创建出多个实例。
  • 优点:延迟加载、线程安全。
  • 缺点:我们知道,绝大大部分情况下资源冲突并不会频繁发生,而每次调用getInstance方法时都需要进行同步操作,这会导致性能下降。尤其是在高并发情况下,同步操作可能会成为瓶颈。
java 复制代码
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),只在必要时进行同步。
  • 优点:延迟加载,并且线程安全。
  • 缺点:实现稍微复杂一些。
java 复制代码
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的可见性和有序性
懒汉式-静态内部类(推荐)
  • 思想:控制类加载的时机,利用类加载机制保证初始化实例时只有一个线程。
  • 优点:既实现了懒加载,又能保证线程安全,而且代码简洁。
java 复制代码
public class Singleton {
    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}
存在的问题

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

java 复制代码
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接口(以上代码未实现)。

java 复制代码
 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方法返回单例的现有实例,可以确保序列化和反序列化过程中始终只有一个实例。如:

java 复制代码
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保证线程安全的。
java 复制代码
public enum Elvis {
    INSTANCE;

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

总结

  • 饿汉式:在类加载时创建实例,简单易懂,无需加锁。
  • 懒汉式:延迟创建实例,需考虑线程安全问题。
    • 经典写法:非线程安全。
    • 同步方法:线程安全但性能较差。
    • 双重检查锁(DCL):线程安全且性能较好。
    • 静态内部类:线程安全且简洁。
  • 枚举:简洁且天然具备线程安全性和序列化安全性,防止反射破坏。
    好文推荐:Java 枚举实现单例模式,线程安全又优雅
相关推荐
捕鲸叉2 分钟前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer6 分钟前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq9 分钟前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml432 分钟前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~33 分钟前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong16168835 分钟前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7891 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java2 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山2 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
睡觉谁叫~~~2 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust