设计模式实战——开发中常用到的单例模式
单例模式介绍
单例模式是一种常用的软件设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。以下是对单例模式的介绍:
一、定义与特点
- 定义:单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。
- 特点:
- 单一实例:通过私有构造函数或静态方法确保整个应用中只存在一个实例对象。
- 全局访问点:提供一个静态方法供外部调用,以获取该类的唯一实例。
- 延迟初始化:在需要时才创建实例,以节省资源并提高效率。
二、实现方式
单例模式的实现方式有多种,主要包括饿汉式、懒汉式、双重校验锁和静态内部类等。
- 饿汉式:在类加载时就完成实例化,避免了线程同步问题,但可能导致资源浪费。
- 懒汉式:在第一次使用时进行实例化,虽然节约了资源,但在多线程环境下需要加锁以保证线程安全。
- 双重校验锁:结合了饿汉式和懒汉式的优点,既实现了延迟加载,又保证了线程安全,但实现较为复杂。
- 静态内部类:利用了Java语言的特性,既实现了延迟加载,又保证了线程安全,且实现简单。
三、应用场景
单例模式广泛应用于需要频繁创建和销毁的对象的场景,如数据库连接池、线程池、缓存、日志对象等。这些场景中,使用单例模式可以避免频繁创建和销毁对象带来的性能开销,提高系统效率。
四、优缺点
- 优点:
- 节约资源:避免了频繁创建和销毁对象所带来的性能开销。
- 控制实例数目:确保在整个系统中某个类有且仅有一个实例。
- 全局访问点:提供了一个统一的访问点来获取该类的实例。
- 缺点:
- 灵活性差:由于单例模式限制了类的实例化,因此在需要多个实例的情况下不适用。
- 扩展困难:当需要继承单例类时,可能会因为单例模式的限制而变得困难。
- 测试不便:由于单例模式的存在,可能会给单元测试带来一定的困难。
总之,单例模式是一种实用的设计模式,适用于那些需要频繁创建和销毁的对象的场景。在实际开发中,应根据具体需求选择合适的实现方式,并注意避免其潜在的缺点。
JDK中的单例模式
一、介绍
JDK中的单例模式主要体现在Runtime类和GUI相关类中。以下是对JDK中的单例模式的具体介绍:
- Runtime类
- 定义与特点:在Java中,每个Java应用程序都有一个与之关联的Runtime实例,这个实例封装了Java运行时的环境。由于Java是单进程的,因此在一个JVM进程中,只能有一个Runtime实例,这正符合单例模式的特点。
- 实现方式:Runtime类使用了饿汉式单例模式,即在类加载时就创建好一个静态的对象供外部使用。这种方式简单且线程安全,因为实例在类加载时就已经创建完成,后续访问不涉及同步问题。
- GUI相关类
- 定义与特点:除了Runtime类外,JDK中的GUI相关类也采用了单例模式。这些类通常在第一次使用时才进行实例化,以避免影响JVM的启动速度。
- 实现方式:与Runtime类不同,GUI相关类采用的是懒汉式单例模式,即在真正需要的时候再创建实例。这种方式虽然节省了资源,但在多线程环境下需要额外的同步措施来保证线程安全。
- 其他应用
- 枚举实现:在JDK中,还可以通过枚举类型来实现单例模式。枚举类型本身具有单例的特性,且线程安全,无需额外的同步措施。
- 双重校验锁:双重校验锁(DCL)是一种常用的懒汉式单例模式变种,它结合了饿汉式和懒汉式的优点,既实现了延迟加载,又保证了线程安全。
综上所述,JDK中的单例模式主要应用于Runtime类和GUI相关类中,它们分别采用了饿汉式和懒汉式的实现方式。这些单例模式的应用不仅提高了系统的性能和资源利用率,还为开发者提供了便捷的全局访问点。
二、实例
Runtime
类封装了Java运行时的环境。每一个java程序实际上都是启动了一个JVM进程,那么每个JVM进程都是对应这一个Runtime实例,此实例是由JVM为其实例化的。每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。
由于Java是单进程的,所以,在一个JVM中,Runtime的实例应该只有一个。所以应该使用单例来实现。
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
以上代码为JDK中 Runtime
类的部分实现,可以看到,这其实是饿汉式单例模式。在该类第一次被classloader加载的时候,这个实例就被创建出来了。
Spring
中的单例模式
一、介绍
Spring中的单例模式主要体现在Bean的默认作用域上,即singleton。
在Spring框架中,单例模式是Bean默认的作用域,这意味着每个由Spring容器管理的Bean默认都是单例的。这种设计可以有效减少对象的创建和销毁次数,从而提高程序的性能和效率。当IOC容器维护Bean实例时,如果一个对象已经被创建了,那么以后每次请求该对象时,都会直接返回之前创建好的对象实例,避免了重复创建和销毁对象的开销。
Spring中的单例模式主要通过配置文件和注解两种方式来实现。在配置文件中,可以通过设置元素的scope属性为"singleton"来指定Bean的作用域为单例。而在注解方式中,可以使用@Component和@Scope("singleton")注解来定义一个单例Bean。
Spring中的单例模式虽然与经典的单例模式有所不同,但它同样遵循了“系统中只有一个实例”的原则,并且提供了全局访问点来获取这个实例。然而,由于Spring容器的特殊性,同一个类在不同容器中可能会有不同的实例,这与经典的单例模式有所区别。
总的来说,Spring中的单例模式是一种便捷且高效的方式来管理Bean的生命周期和作用域。它不仅可以提高系统的性能和效率,还可以简化Bean的配置和管理过程。
二、实例
我们知道在 Spring
中默认注入的Bean都是单例,那么Spring中的单例是怎么生成的呢?我们来看下Spring生成Bean的代码。
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
spring依赖注入时,使用了双重判断加锁的单例模式,首先从缓存MAP中获取bean实例,如果为null,对缓存map加锁,然后再从缓存中获取bean,如果继续为null,就创建一个bean。
Spring并没有使用私有构造方法来创建bean,而是通过singletonFactory.getObject()返回具体beanName对应的ObjectFactory来创建bean。实际上是调用了AbstractAutowireCapableBeanFactory的doCreateBean方法,返回了BeanWrapper包装并创建的bean实例。
MyBatis
中的单例模式
一、介绍
MyBatis中的单例模式主要体现在其VFS(Virtual File System)组件中。
VFS是MyBatis框架中的一个关键组件,用于查找和管理资源文件,如映射器XML文件。在MyBatis的实现中,VFS采用了单例模式来确保整个应用中只有一个VFS实例。这种设计可以有效地减少资源的消耗,并提高文件查找的效率。
MyBatis通过创建一个静态内部类来持有VFS的单例实例。这个静态内部类在被首次访问时才会加载,从而实现了懒加载的效果。具体来说,VFS类中有一个名为VFSHolder的静态内部类,它包含了一个静态的VFS实例。当调用VFS的getInstance()方法时,会返回VFSHolder中持有的这个唯一实例。
这种实现方式不仅保证了线程安全,还避免了饿汉式单例可能导致的资源浪费问题。因为只有在真正需要使用VFS时,才会创建其实例,从而节省了系统资源。
总的来说,MyBatis中的单例模式是一种高效且实用的设计模式,它通过确保VFS组件的唯一性,提高了资源利用率和系统性能。
二、实例
1. ErrorContext
ErrorContext是用在每个线程范围内的单例,用于记录该线程的执行环境错误信息。
public class ErrorContext {
private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n");
private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
private ErrorContext stored;
private String resource;
private String activity;
private String object;
private String message;
private String sql;
private Throwable cause;
private ErrorContext() {
}
public static ErrorContext instance() {
ErrorContext context = LOCAL.get();
if (context == null) {
context = new ErrorContext();
LOCAL.set(context);
}
return context;
}
}
构造函数是private修饰,具有一个static的局部instance变量和一个获取instance变量的方法,在获取实例的方法中,先判断是否为空如果是的话就先创建,然后返回构造好的对象。
只是这里有个有趣的地方是,LOCAL的静态实例变量使用了ThreadLocal修饰,也就是说它属于每个线程各自的数据,而在instance()方法中,先获取本线程的该实例,如果没有就创建该线程独有的ErrorContext。
也就是说 ErrorContext
是线程范围内的单例,而不是全局范围内(JVM内)的单例。
2. VFS
public abstract class VFS {
private static final Log log = LogFactory.getLog(VFS.class);
/** The built-in implementations. */
public static final Class<?>[] IMPLEMENTATIONS = { JBoss6VFS.class, DefaultVFS.class };
/** The list to which implementations are added by {@link #addImplClass(Class)}. */
public static final List<Class<? extends VFS>> USER_IMPLEMENTATIONS = new ArrayList<Class<? extends VFS>>();
/** Singleton instance. */
private static VFS instance;
/**
* Get the singleton {@link VFS} instance. If no {@link VFS} implementation can be found for the
* current environment, then this method returns null.
*/
@SuppressWarnings("unchecked")
public static VFS getInstance() {
if (instance != null) {
return instance;
}
}
VFS是MyBatis中提供的文件系统类,存在感比较低。但是我们看下这个类的源代码的话,的确是很标准的单例模式。
Log4j中的单例
Log4j中的单例是指Log4j框架中用于记录日志的对象,通常是一个Logger对象。在Log4j中,每个Logger对象都是单例的,这意味着在整个应用程序中,对于同一个类名或包名,只会创建一个Logger实例。这样可以确保日志记录的一致性和性能优化。
要获取一个Logger对象,可以使用以下代码:
import org.apache.log4j.Logger;
public class MyClass {
private static final Logger logger = Logger.getLogger(MyClass.class);
public void myMethod() {
logger.info("This is an info message");
logger.error("This is an error message");
}
}
在这个例子中,logger
是一个静态的Logger对象,它通过调用Logger.getLogger()
方法并传入当前类的Class对象来获取。这样,无论在哪个地方使用这个Logger对象,都会得到相同的实例,从而实现单例模式。
注意
Log4j框架内部使用了多种机制来确保多个Logger向同一个文件中打日志时的高效性和线程安全性。
高效的日志记录:Log4j使用异步日志记录机制,这意味着日志消息不会立即写入文件,而是先存储在内存中的一个缓冲区中。当缓冲区达到一定大小或者在一定时间间隔后,才会将缓冲区中的日志消息批量写入文件。这种方式可以减少磁盘I/O操作的次数,从而提高日志记录的效率。
线程安全:虽然Log4j的Logger对象是单例的,但实际的日志记录过程是由内部的Appender负责的。Appender负责将日志消息写入目标位置(如文件、控制台等)。Log4j提供了多种Appender实现,其中一些支持多线程访问,例如
FileAppender
和RollingFileAppender
。这些Appender内部使用了同步机制来确保线程安全,例如使用锁或其他并发工具来避免多个线程同时写入同一个文件。配置优化:为了进一步提高性能和线程安全性,可以通过合理配置Log4j来实现。例如,可以设置合适的缓冲区大小和刷新间隔,以及选择合适的Appender类型。此外,还可以通过调整日志级别来减少不必要的日志记录,从而减轻系统负担。
总之,Log4j通过异步日志记录、线程安全的Appender实现以及合理的配置优化,能够高效地处理多个Logger向同一个文件中打日志的情况。