Java 单例模式

前言

最近看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):线程安全且性能较好。
    • 静态内部类:线程安全且简洁。
  • 枚举:简洁且天然具备线程安全性和序列化安全性,防止反射破坏。
    好文推荐:Java 枚举实现单例模式,线程安全又优雅
  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值