六、单例模式(Singleton)
1、介绍
单例模式的产生,它主要是为了解决在软件系统中,对于一些类,我们只需要创建一个全局唯一的实例。例如,系统的配置信息、数据库连接池等。如果有多个实例可能会导致状态不一致,或者额外的资源消耗。这个需求在很多软件系统中都会出现,因此单例模式成为了一种常见的解决方案。
2、生活实例
首先,让我们想象这样一个场景:你的城市只有一个邮局,每个人都要通过这个邮局去邮寄或取信件。这个邮局就是这个城市的唯一实例,你无法创建另一个邮局,这就是单例模式在现实生活中的应用。
3、java代码实例
现在,让我们用 Java 来创建一个单例类。假设我们要创建一个 "邮局" 的单例类:
public class PostOffice {
// 创建一个 PostOffice 类的对象
private static PostOffice instance = new PostOffice();
// 让构造函数为 private,这样该类就不会被实例化
private PostOffice() {}
// 获取唯一可用的对象
public static PostOffice getInstance() {
return instance;
}
public void showMessage() {
System.out.println("Hello, this is the only post office in town!");
}
}
在这段代码中,我们首先创建了一个 static 类型的 PostOffice 对象,并且将构造函数设置为 private,以防止其他人直接创建新的 PostOffice 对象。我们提供了一个 public 的方法,叫做 getInstance,它返回了我们初始化时创建的那个唯一的 PostOffice 对象。
现在,你可以这样使用这个 PostOffice 单例:
public class SingletonDemo {
public static void main(String[] args) {
// 不合法的构造函数,会报错:The constructor PostOffice() is not visible
// PostOffice po = new PostOffice();
// 获取唯一可用的对象
PostOffice po = PostOffice.getInstance();
// 显示消息
po.showMessage();
}
}
在这个例子中,我们试图通过 PostOffice 的构造函数来创建新的对象,但是会报错,因为构造函数是 private 的,不能直接访问。但是我们可以通过 getInstance 方法来获取到唯一的 PostOffice 对象,然后调用 showMessage 方法。
希望这个例子可以帮助你理解单例模式!这个模式很有趣,因为它把我们在现实生活中常见的一个概念(唯一性)应用到了编程中。
4、其它例子
让我们首先通过Java的Runtime类来看看单例模式是如何在实际中应用的。Runtime类管理着Java程序的运行时环境。每个Java应用都有一个Runtime类实例,使应用能够与其运行时环境相互作用。但是你无法直接创建一个新的Runtime实例,因为Runtime类的构造方法是私有的。相反,你必须通过Runtime类的静态方法getRuntime()来获取Runtime实例。这就是单例模式的一个实现。
public class SingletonExample {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime(); // 获取Runtime实例
// ...
}
}
Spring框架的Bean也是单例模式的一个典型实现。在Spring框架中,一个bean默认是单例的,也就是说,Spring容器中的所有bean默认都是单例模式创建的。这样做的好处是,可以重复使用Bean,而不是每次需要时都创建一个新的Bean。这样可以大大提高效率和性能。
以下是一个Spring的例子。假设我们有一个名为"myService"的bean,你可以多次从Spring容器中获取它,但每次获取的都是同一个实例。
public class SingletonExample {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
MyService serviceA = (MyService) context.getBean("myService");
MyService serviceB = (MyService) context.getBean("myService");
System.out.println(serviceA == serviceB); // 打印结果是 "true"
}
}
以上的代码示例表明,serviceA和serviceB是同一个实例,它们是Spring容器中的单例bean。
这就是单例模式在Java Runtime类和Spring框架中的应用,它们都通过特定的方式实现了全局唯一实例的创建和管理,从而提高了程序的性能和效率。
5、几个你可能经常见到的也使用了单例模式的例子
Java中有几个常见的单例模式的例子,包括:
-
java.lang.Runtime
: 这个类用于管理Java应用程序的运行时环境。你不能直接创建Runtime类的新实例,而是必须通过调用其静态方法getRuntime()来获取Runtime实例。 -
java.lang.System
: System类包含一些有用的类字段和方法。它不能被实例化,所有的字段和方法都是静态的。 -
java.awt.Desktop
: Desktop类允许Java应用程序启动已在本机桌面上注册的关联应用程序,以处理URI或文件。Desktop实例是通过静态方法getDesktop()获取的。 -
java.lang.management.ManagementFactory
: 这个类是工厂方法用于获取管理接口的对象,如:操作系统、线程、内存等。
以上都是Java中常见的单例模式的应用例子。请注意,每个类都有其独特的方法来控制实例的创建和访问,这正是单例模式的核心。
6、常见的单例模式的写法
在Java中,常见的单例模式的写法主要有以下几种:
-
懒汉式(线程不安全):懒汉式是指在真正需要使用实例的时候再去创建。这种方式的好处是如果最后这个实例没有被使用,那么就不会创建,从而避免了资源的浪费。但是,缺点是需要处理多线程同步问题,否则可能会出现多个线程同时创建实例的情况。"懒"字,可以理解为这个实例"懒"得不愿意一开始就创建,它想等到真正需要使用的时候再去创建。
public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
这种方式在多线程环境下是不安全的,如果多个线程能够同时进入
if (instance == null)
,并且此时instance
为null
,那么会有多个线程执行instance = new Singleton()
,这样会导致实例化多次instance
。 -
懒汉式(线程安全):
public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
这种方式能够在多线程中很好的工作,但是每次调用
getInstance
方法时都需要进行同步,造成不必要的同步开销。 -
饿汉式:饿汉式是指在类加载时就创建实例。这种方式的好处是可以确保线程安全,因为实例是在类加载时就创建好的,所以不会存在多个线程同时创建实例的情况。但是,缺点是如果这个实例最后没有被使用,那么就会造成资源的浪费。因为"饿"字,可以想象成这个实例"饿"得不能等到真正需要使用的时候再创建,所以一开始就创建好了。
public class Singleton { private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }
这种方式基于类加载机制,避免了多线程同步问题,但是如果
Singleton
类没有被装载,那么instance
不会被实例化,这时候类被装载其实是不需要创建实例的,会造成不必要的资源浪费。 -
双重检查锁定(Double Checked Locking):
双重检查锁定(Double Checked Locking)的名称确实看起来有点晦涩,不过它的实现方式其实还是很直观的。
我们可以通过一个生活实例来帮助理解。比如你的室友需要使用洗手间,但是他不确定洗手间是否有人。他可以直接尝试打开门,但是如果洗手间里有人,那将是一个非常尴尬的情况。所以他可能先敲一下门,这就是第一次“检查”,如果没有人回应,他就可以认为洗手间是空的。但是在他打开门的时候,他可能还会再次听听里面是否有水声等提示有人的声音,这就是第二次“检查”。这个过程就像我们在创建单例对象时,先检查对象是否已经创建,如果没有创建,就获取锁并再次检查,然后才开始创建对象。
public class Singleton { private volatile static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
这段代码做了两次检查,第一次是在同步块外,如果instance不为null,则直接返回,这样就避免了每次都需要进入同步块,可以提高效率。如果instance为null,才进行同步,然后在同步块内再进行一次检查。如果这时instance仍为null,则创建新的实例。
这样的双重检查方式可以确保即使有多个线程同时调用getInstance()方法,也能保证只创建一个Singleton实例。同时由于使用了关键字volatile,保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。
双重检查锁定可以在多线程环境下保持高性能。
-
静态内部类方式:
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
这种方式同样利用了类加载机制来保证只创建一个instance实例。它与饿汉式的区别在于:饿汉式只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,SingletonHolder类没有被主动使用,只有通过显式调用getInstance方法时,才会显式装载SingletonHolder类,从而实例化instance。
-
枚举方式:
public enum Singleton { INSTANCE; public void whateverMethod() { } }
这是最安全的方法,它能防止多次实例化,防止反序列化重新创建新的对象,绝对防止多次实例化。
-
静态内部类方式的改进版(Initialization on Demand Holder,IODH):
假设你在一家公司工作,这家公司的大楼里有一个自动贩卖机。这个自动贩卖机只在第一个需要用它的员工使用卡片激活后才会开启。一旦被激活,所有的员工都可以使用这个自动贩卖机。此外,这个大楼中不会有第二台自动贩卖机出现,所以无论哪个员工想要用自动贩卖机,他们都只能使用这一台。
这个例子就是对应到IODH模式的实现:
- 自动贩卖机对应到代码中的Singleton实例
- 激活自动贩卖机对应到在第一次需要Singleton实例时创建它
- 所有的员工都使用同一台自动贩卖机对应到所有对
Singleton.getInstance()
的调用都返回同一个实例
以下是在Java中使用IODH模式的例子:
public class Singleton { private static class Holder { private static final Singleton INSTANCE = new Singleton(); } private Singleton() { } public static Singleton getInstance() { return Holder.INSTANCE; } }
在这个例子中,Singleton实例是在Holder类中创建的,只有当第一次调用
Singleton.getInstance()
时,Holder类才会被加载,Singleton实例才会被创建。之后的所有Singleton.getInstance()
调用都将返回同一个实例。所以,如果你觉得IODH难以理解或记忆,你可以想象它就像是一个只有在第一个需要用它的人激活后才会开启的自动贩卖机,并且无论有多少人需要使用自动贩卖机,他们都只能使用这一台。
7、一些常见的示例:
-
饿汉式(线程安全,调用效率高,但不能延时加载):
Java中的java.lang.Runtime就是使用了饿汉式单例模式。Runtime类没有公开的构造方法,但提供了一个静态方法Runtime.getRuntime()来获取Runtime类的唯一实例。 -
懒汉式(线程安全,调用效率不高,但可以延时加载):
Spring框架中,在单例作用域的Bean默认为懒加载,第一次调用getBean()时才会初始化Bean,这就是懒汉式的应用。 -
双重检测锁式(由于JVM底层内部模型原因,偶尔会出问题,不建议使用):
在Android开发中,如果要实现一个单例且这个单例在多线程环境下使用且性能要求较高,双重检测锁式是一种常见的实现方式。 -
静态内部类式(线程安全,调用效率高,可以延时加载):
Android源码中,对于系统服务(SystemService)的管理,往往采用静态内部类式来实现单例。 -
枚举式(线程安全,调用效率高,不能延时加载,可以天然的防止反射和反序列化调用):
Java的枚举类型实际上就是一个类,因此可以在枚举中添加自己的方法。当我们需要单例时,可以使用枚举方式,这是最简单的方式。在Effective Java中,Joshua Bloch提倡使用枚举方式来实现单例。 -
双重检测锁式的优化版(完全解决DCL失效问题):
实际应用中,这种方式还是相对较少的。因为在大多数情况下,对于单例模式的要求并不会严苛到需要在DCL基础上进行优化。 -
静态内部类方式的改进版(Initialization on Demand Holder,IODH):
IODH方式同样也是很常见的一种方式,这种方式在很多开源框架中都有应用,如Spring框架、Apache commons工具类库等。
这些模式在不同的场合有不同的应用,可以根据具体的需求选择使用。