单例设计模式是一种创建型设计模式,其主要目的是确保一个类只有一个实例,并提供全局访问点来访问该实例。这意味着无论在何处创建对象,都将获得相同的实例,确保系统中的唯一性。
结构
单例模式通常包含下面要素:
- 私有构造函数:单例类通常会将构造函数设为私有,以防止外部直接创建对象。
- 私有静态实例变量:单例类内部会维护一个私有的静态实例变量,用于保存单例对象。
- 公共静态方法:提供一个公共静态方法来获取单例对象。这个方法通常被称为"getInstance"。
实现
单例模式的实现方式有多种,以下是其中两种常见的实现方式:懒汉式和饿汉式。这两种方式分别处理了单例对象的延迟加载和线程安全性。
懒汉式单例模式(Lazy Initialization)
懒汉式单例模式在首次访问时才创建实例,这样可以避免在程序启动时占用额外的内存。但需要注意,在多线程环境下需要考虑线程安全问题,可以使用同步来解决。
java
public class LazySingleton {
// 本类中创建本类对象
private static LazySingleton instance;
private LazySingleton() {
// 私有构造函数
}
public static synchronized LazySingleton getInstance() {
if (instance == null) { // 如果此时有多个线程进行到这一步,则会创建多个实例
instance = new LazySingleton();
}
return instance;
}
}
饿汉式单例模式(Eager Initialization)
饿汉式单例模式在类加载时就创建实例,因此不存在线程安全问题。但可能在程序启动时占用额外的内存。
java
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {
// 私有构造函数
}
public static EagerSingleton getInstance() {
return instance;
}
}
上述两种方式都使用了私有构造函数,以防止外部直接创建实例。它们都提供了一个公共静态方法 getInstance()
来获取单例对象。
需要根据具体需求和线程安全性要求选择合适的实现方式。懒汉式单例在首次访问时创建实例,适用于延迟加载的情况,但需要考虑线程安全。饿汉式单例在类加载时创建实例,线程安全,但可能占用额外的内存。
饿汉式还可以使用静态代码块的方式实现:
java
public class EagerSingleton {
// 私有构造方法
private EagerSingleton(){}
// 声明变量
private static EagerSingleton instance;
// 静态代码块中赋值
static {
instance = new EagerSingleton();
}
public static EagerSingleton getInstance(){
return instance;
}
}
此外,还可以考虑双重检查锁(Double-Checked Locking)、枚举等其他方式来实现单例模式,这些方式可以提供更好的性能和线程安全性。选择哪种方式取决于具体的需求和应用场景。
线程安全的懒汉式单例模式
下面有三种方式,可以使得普通懒汉式变得线程安全:
- 使用双重检查锁(Double-Checked Locking)(最常见的方式)
java
public class LazySingleton {
// 声明一个私有的静态变量来保存单例实例
private volatile static LazySingleton instance;
// 私有构造函数,防止外部直接创建实例
private LazySingleton() {
// 在构造函数中进行初始化工作
}
// 公共静态方法来获取单例实例
public static LazySingleton getInstance() {
// 第一次检查,如果实例为空,则进入同步块
if (instance == null) {
synchronized (LazySingleton.class) {
// 第二次检查,防止多个线程同时通过第一次检查,创建多个实例
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
在上述代码中,关键点是使用了双重检查锁(Double-Checked Locking)来确保在多线程环境下只有一个实例被创建。通过第一次检查判断实例是否为空,如果为空,则进入同步块,在同步块中再次检查实例是否为空,然后创建实例。这种方式在第一次创建实例后,后续获取实例时不会进入同步块,提高了性能。
值得注意的是,在多线程环境下,一个线程可能在另一个线程创建实例后,仍然看到一个旧的、不完整的实例。这是因为线程可能会将部分初始化完成的对象引用,而不是完整初始化后的对象。volatile
关键字确保了写入操作对其他线程是可见的,因此在获取实例时,始终能够看到已经完成初始化的实例。
现代编译器和处理器为了提高性能,可能会对指令进行重排序,这可能导致在创建实例时的指令重排,使得其他线程在实际对象初始化之前就能够看到对象的引用。
为了避免由于指令重排序导致的上述问题,需要将 instance
变量声明为 volatile
,以确保在创建实例时的顺序正确。这是一种相对安全的延迟加载单例模式的实现方式。
- 静态内部类:由于JVM在加载外部类的过程中,并不会加载静态内部类,只有当内部类的属性/方法被调用时才会被加载。因此使用静态内部类来延迟加载单例对象,这种方式不需要使用显式的同步锁,并且能够确保线程安全(推荐)
java
public class LazySingleton {
private static class SingletonHolder {
private static final LazySingleton INSTANCE = new LazySingleton();
}
private LazySingleton() {
// 私有构造函数
}
public static LazySingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
- 枚举单列:使用枚举类型来实现单例,枚举类型的实例在类加载时被初始化,且线程安全。
java
public enum LazySingleton {
INSTANCE;
// 在枚举中可以添加其他成员和方法
}
枚举单例模式是一种非常安全且简洁的方式来实现单例,但它也有一些限制和潜在的缺点:
- 不支持延迟加载:枚举单例模式在类加载时就创建了实例,因此无法实现延迟加载(Lazy Initialization)。如果你的单例需要在第一次访问时才创建,那么枚举单例就不适用。
- 不支持继承 :枚举类不能被继承,因为枚举类型已经是
final
的。如果需要基于现有单例实现创建其他类型的单例,就无法使用枚举。 - 有限的灵活性:枚举单例模式提供了一个相对简单的方式来创建单例,但如果需要在单例类中包含多个方法、属性或者需要实现某些特定的接口,那么枚举可能无法满足需求。枚举类型是一种特殊的类,不能扩展或实现其他接口。
- 不支持懒加载和外部配置:由于枚举单例在类加载时立即创建实例,因此无法通过外部配置文件等方式来配置单例对象的属性。这可能会限制一些高度可配置的场景。
尽管枚举单例模式具有这些限制,但对于许多应用场景来说,它是一种非常可靠和方便的单例模式实现方式。
综上所述,在选择单例实现方式时,应根据具体需求和项目的特点来进行权衡,确定最合适的实现方式。
综合考虑,选择饿汉式还是懒汉式单例模式应根据具体的需求和项目背景来决定。如果确保线程安全是首要考虑的因素,并且单例对象较小,可以选择饿汉式。如果希望延迟加载或者需要更灵活的初始化逻辑,可以选择懒汉式,但需要注意处理线程安全问题。
存在的问题及解决方案
有一些方式可以破坏单例模式的原则,使得多个实例可以被创建或访问。以下是一些可能破坏单例模式的方式:
-
反射:使用反射机制可以访问类的私有构造函数,从而创建多个实例。例如,在Java中,可以通过反射创建对象并绕过单例的构造函数。
javaClass<Singleton> singletonClass = Singleton.class; Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor(); constructor.setAccessible(true); Singleton instance1 = constructor.newInstance();
-
序列化和反序列化:当一个单例对象被序列化然后反序列化时,会创建一个新的对象实例。
java// 当反序列化一个已序列化的单例对象时,可能会创建新的实例 FileInputStream fileIn = new FileInputStream("singleton.ser"); ObjectInputStream in = new ObjectInputStream(fileIn); Singleton instance2 = (Singleton) in.readObject(); in.close();
-
克隆:通过对象的克隆方法可以创建一个新的实例,破坏了单例的唯一性。
java// 使用克隆方法创建新实例 Singleton instance3 = (Singleton) instance.clone();
那么,如何解决上述可能会遇到的情况呢?
结合一个简单的例子来说明如何破坏和防止破坏单例模式。
首先,我们将创建一个简单的单例类,然后展示如何可能破坏它,以及如何修复这些问题。
单例类的示例:
java
public class LazySingleton {
private static class SingletonHolder {
private static final LazySingleton INSTANCE = new LazySingleton();
}
private LazySingleton() {
// 私有构造函数
}
public static LazySingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
1. 反射问题:
使用反射可以破坏单例模式,因为它可以访问私有构造函数。为了防止这种情况,可以在构造函数中添加逻辑:
java
public class LazySingleton {
private static boolean flag = false;
private static class SingletonHolder {
private static final LazySingleton INSTANCE = new LazySingleton();
}
private LazySingleton() {
synchronized(LazySingleton.class){
// 私有构造函数,判断flag是否为true,true则表示非第一次创建
if(flag){
throw new RuntimeException("当前已经有一个实例");
}
// 将flag值设置为true
flag = true;
}
}
public static LazySingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
2. 序列化问题:
在序列化和反序列化时,会创建新的实例。为了防止这个问题,可以添加readResolve
方法:
java
public class LazySingleton implements Serializable{
private static class SingletonHolder {
private static final LazySingleton INSTANCE = new LazySingleton();
}
private LazySingleton() {
// 私有构造函数
}
public static LazySingleton getInstance() {
return SingletonHolder.INSTANCE;
}
// 当进行反序列化时,会自动调用该方法,将该方法的返回值返回
protected Object readResolve() {
return SingletonHolder.INSTANCE;
}
}
3. 类加载器问题:
确保只有一个类加载器加载单例类,通常是由应用程序类加载器(Application Classloader)加载:
java
public class Singleton {
private static Singleton instance;
private Singleton() {
if (instance != null) {
throw new IllegalStateException("已经存在一个实例");
}
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这些示例展示了如何防止一些可能破坏单例模式的情况,并确保只有一个实例被创建和访问。注意,实际应用中可能需要根据具体情况采用不同的防御措施。
源码剖析
上面已经将单例模式的基本概念和用法学习完毕,那么单例模式在哪些地方被使用了,以及是怎么使用的呢?下面通过查询JDK中的源码来学习一下。
Runtime
java.lang.Runtime
类是一个经典的单例类,表示运行时环境。通过调用 Runtime.getRuntime()
方法可以获取唯一的 Runtime
实例。
通过IDEA中的搜索(快捷键:双击Shift)可以快速找到它
截取其中的一部分代码
java
package java.lang;
import java.io.*;
import java.util.StringTokenizer;
import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
// 其他方法...
}
通过观察它的源码,可以发现它其实是一个经典的饿汉式单例模式。
下面来使用其中的一个方法,来尝试单例模式下的方法调用。
java
@Test
public void testRuntime() {
// 获取Runtime对象
Runtime runtime = Runtime.getRuntime();
Process process;
try {
// 调用runtime的方法exec,参数为一个命令行命令,方法的作用就是执行该命令
process = runtime.exec("ping www.baidu.com");
// 调用process对象中获取输入流的方法
InputStream inputStream = process.getInputStream();
// 读取数据
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
// 将字节数组转换成字符串输出到控制台
System.out.println(new String(buffer,0,len,"GBK"));
}
} catch (IOException e) {
e.printStackTrace();
}
}
其实就是一个很简单的例子,相当于用命令行来ping一下百度网站,如果电脑是联机状态,控制台则会输出以下内容:
Desktop
通过翻译类上面的注释可以了解该类的作用
下面就来查看一下其中的源码
java
package java.awt;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URI;
import java.net.URL;
import java.net.MalformedURLException;
import java.awt.AWTPermission;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.peer.DesktopPeer;
import sun.awt.SunToolkit;
import sun.awt.HeadlessToolkit;
import java.io.FilePermission;
import sun.security.util.SecurityConstants;
public class Desktop {
private static Desktop desktop;
private static DesktopPeer desktopPeer;
// 私有构造函数,防止外部实例化
private Desktop() {
peer = Toolkit.getDefaultToolkit().createDesktopPeer(this);
}
// 获取唯一的 Desktop 实例
public static synchronized Desktop getDesktop(){
if (GraphicsEnvironment.isHeadless()) throw new HeadlessException();
if (!Desktop.isDesktopSupported()) {
throw new UnsupportedOperationException("Desktop API is not " +
"supported on the current platform");
}
sun.awt.AppContext context = sun.awt.AppContext.getAppContext();
Desktop desktop = (Desktop)context.get(Desktop.class);
if (desktop == null) {
desktop = new Desktop();
context.put(Desktop.class, desktop);
}
return desktop;
}
// 测试当前平台是否支持此类
public static boolean isDesktopSupported(){
Toolkit defaultToolkit = Toolkit.getDefaultToolkit();
if (defaultToolkit instanceof SunToolkit) {
return ((SunToolkit)defaultToolkit).isDesktopSupported();
}
return false;
}
// 其他方法...
}
很显然,它属于懒汉式单例模式。
到此,单例模式已经学习完毕~