文章目录
前言
最近看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 枚举实现单例模式,线程安全又优雅!