当今软件开发行业变化迅速,开发人员需要不断学习新的技术和方法,以保持竞争力。设计模式是一种被广泛使用的解决方案,它可以帮助我们解决各种软件设计问题。设计模式是一种被广泛接受的最佳实践,它们提供了一种通用的解决方案,可以在各种不同的场景中使用。本篇博客将深入讲解单例模式的概念、分类和使用场景。
单例模式顾名思义,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。在应用程序中,有些对象只需要一个实例,例如线程池、缓存、日志记录器等。使用单例模式可以确保只有一个实例被创建,并提供一个全局访问点来访问该实例,从而避免了创建多个实例带来的资源浪费和不必要的复杂性。
Singleton实际上有很多种写法,但是严格来讲只有两种是完美无缺的。但实际工作中我们用的很有可能并不是完美的版本。单例模式的第一步需要我们将构造方法设为private,这就代表其他类new不出来,只能通过调用Mgr01.getInstance方法来获得已经创建好的mgr01对象。
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01() {
}
public static Mgr01 getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1 == m2);
}
}
这是一个使用饿汉式实现单例模式的示例代码。类加载到内存之后只实例化一个单例,JVM保证线程安全,因为JVM保证每一个class只会load到内存一次,static变量实在class被加载到内存之后马上就进行初始化的,所以也保证只初始化一次,所以无论拿多少次,即使是多线程访问也不会出问题。
实际上要加载一个类我们也可以这么来写:Class.forName("类的名字"),只把class放到内存里而不进行实例化,如果我们用这种方式把Mgr01加到内存之后,这个static的INSTANCE是实例化的,因为他是一个静态变量,load到内存就会初始化。
但是这种写法有一个缺点,就是还没使用就初始化,可能会浪费空间。
第二种写法是采用了静态语句块,本质上和第一种方法没区别:
public class Mgr02{
private static final Mgr02 INSTANCE;
static {
INSTANCE=new Mgr02();
}
private Mgr02() {
}
public static Mgr02 getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
Mgr02 m1 = Mgr02.getInstance();
Mgr02 m2 = Mgr02.getInstance();
System.out.println(m1 == m2);
}
}
这两种方法的缺点就是,实例在类加载时就被创建了,如果应用程序不需要使用该实例,那么就会浪费一定的内存空间。于是有了我们的懒加载方式,俗称懒汉式:
public class Mgr03 {
private static volatile Mgr03 INSTANCE;
private Mgr03(){}
public static Mgr03 getInstance() {
if(INSTANCE==null){
synchronized (Mgr03.class){
if(INSTANCE==null){
INSTANCE=new Mgr03();
}
}
}
return INSTANCE;
}
}
懒汉式就是指,什么时候我用到什么时候我再把它初始化。这种方式在上一篇object的文章中有详细讲过它的线程安全问题以及双重验证的机制,这里就不再赘述了。
我们再来看一种静态内部类的方式(这是一种完美的写法):
public class Mgr07 {
private Mgr07(){}
private static class Mgr07Holder{
private static final Mgr07 INSTANCE=new Mgr07();
}
public static Mgr07 getInstance(){
return Mgr07Holder.INSTANCE;
}
}
我们定义了一个静态内部类,在静态内部类里面初始化了Mgr07。饿汉式加载时不管用到与否,在类加载的时候就对实例进行初始化了,而Mgr07里面,如果我们只加载Mgr07的话,这里面的Mgr07Holder是不会被初始化的。一个类的静态内部类,在外面的类被加载的时候,它里面的静态的类是不会被加载的,只有当我们调用getInstance方法的时候才会被加载。因此这种方式不但实现了懒加载,还实现了只有一个实例。这种方式的线程安全是由JVM来帮我们保证线程安全的,保证每个class只被load到内存一次。
我们再来看下一种写法,JAVA的创始人之一Joshua Bloch曾经写过一本书叫做《Effective Java》,在本这书当中他提到了一种通过枚举类来实现单例模式的方法:
public enum Mgr08 {
INSTANCE;
public void m(){//其他业务方法
System.out.println("m");
}
}
在这个枚举类中只有一个取值,就是INSTANCE,当我们需要这个实例的时候直接调用Mgr08.INSTANCE就可以。这是完美中的完美:枚举单例,不仅可以解决线程同步,还可以防止反序列化。
ps:
反序列化是将序列化后的数据恢复为对象的过程。在Java中,对象可以被序列化为字节流,然后通过网络传输或者保存到文件中。当需要使用该对象时,可以将字节流反序列化为对象,从而恢复对象的状态。
反序列化可以用于实现远程方法调用(Remote Method Invocation,简称 RMI),在客户端和服务器之间传输对象。客户端可以将对象序列化为字节流,然后通过网络传输到服务器,服务器将字节流反序列化为对象,从而调用对象的方法。在这个过程中,对象的状态被保存下来,可以在客户端和服务器之间共享。
反序列化也可以用于实现对象的持久化,将对象保存到文件中,下次需要使用时再从文件中读取并反序列化为对象。这种方式可以避免对象的重新创建和初始化,提高应用程序的性能和效率。
需要注意的是,反序列化可能会带来安全问题,因为反序列化时会调用对象的构造函数和其他方法,如果不加限制地反序列化未知来源的数据,可能会导致恶意代码的执行。因此,在反序列化时需要谨慎处理,可以使用安全的序列化机制、限制反序列化的类和方法等方式来提高安全性。
Java的反射可以做到通过一个class文件,把整个class加载到内存,再new一个实例出来。前面的各种写法都可以被找到class文件通过反序列化(反射)的方式new一个实例出来,如果要考虑防止反射,最完美的写法就只有这一种了(Mgr08)。枚举单例不会被反序列化的原因是因为枚举类没有构造方法的(java语言的规定,反编译之后枚举是一个abstract class),就算拿到class文件也无法构造它的对象,它的反序列化返回的只是INSTANCE这样一个值,如果再根据这个值查询对象的话返回的时我们单例创建的同一个对象,所以严格来讲这种方法确实是最完美的方法,只是这种写法非常别扭。
实际上,在我们日常开发过程中,单例模式都是由Spring的bean工厂来保证的,不需要我们操心:
在Spring中,BeanFactory 是一个用于管理和创建 Bean 的工厂类。默认情况下,Spring 的 BeanFactory 会创建单例 Bean,也就是说,每个 Bean 只会被创建一次,并且在应用程序的整个生命周期中只存在一个实例。这种方式可以避免创建多个实例带来的资源浪费和不必要的复杂性。
Spring 的 BeanFactory 通过使用注册表(Registry)来管理 Bean 的创建和生命周期。注册表维护了一个 Bean 的缓存,当需要获取 Bean 时,会先从缓存中查找,如果缓存中存在该 Bean 的实例,就直接返回该实例,否则就创建一个新的实例,并将其加入到缓存中。在整个应用程序的生命周期中,只有一个缓存实例,所有的单例 Bean 都存储在这个缓存中。
为了保证单例模式,Spring 使用了对象池(Object Pool)技术。对象池是一种用于缓存和重用对象的技术,它可以避免创建和销毁对象带来的开销,提高应用程序的性能和效率。在 Spring 中,BeanFactory 将创建的 Bean 存储在对象池中,当需要获取 Bean 时,会先从对象池中查找,如果对象池中存在该 Bean 的实例,就直接返回该实例,否则就创建一个新的实例,并将其加入到对象池中。
需要注意的是,Spring 的单例模式是基于 BeanFactory 的,也就是说,只有在同一个 BeanFactory 中才能保证单例模式。如果使用多个 BeanFactory,那么每个 BeanFactory 都会创建一个新的实例,无法保证单例模式。因此,在实际应用中,需要根据具体的情况选择适合的 BeanFactory 实现方式。

本文介绍了单例模式的概念,确保一个类只有一个实例并提供全局访问点。讨论了不同类型的单例实现,包括饿汉式、懒汉式、静态内部类和枚举实现,强调了线程安全和内存管理。此外,文章还提到Spring框架如何通过BeanFactory保证单例模式的应用。
1711

被折叠的 条评论
为什么被折叠?



