设计模式实战——开发中常用到的单例模式

单例模式介绍

单例模式是一种常用的软件设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。以下是对单例模式的介绍:

一、定义与特点

  1. 定义:单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。
  2. 特点
    • 单一实例:通过私有构造函数或静态方法确保整个应用中只存在一个实例对象。
    • 全局访问点:提供一个静态方法供外部调用,以获取该类的唯一实例。
    • 延迟初始化:在需要时才创建实例,以节省资源并提高效率。

二、实现方式

单例模式的实现方式有多种,主要包括饿汉式、懒汉式、双重校验锁和静态内部类等。

  1. 饿汉式:在类加载时就完成实例化,避免了线程同步问题,但可能导致资源浪费。
  2. 懒汉式:在第一次使用时进行实例化,虽然节约了资源,但在多线程环境下需要加锁以保证线程安全。
  3. 双重校验锁:结合了饿汉式和懒汉式的优点,既实现了延迟加载,又保证了线程安全,但实现较为复杂。
  4. 静态内部类:利用了Java语言的特性,既实现了延迟加载,又保证了线程安全,且实现简单。

三、应用场景

单例模式广泛应用于需要频繁创建和销毁的对象的场景,如数据库连接池、线程池、缓存、日志对象等。这些场景中,使用单例模式可以避免频繁创建和销毁对象带来的性能开销,提高系统效率。

四、优缺点

  1. 优点
    • 节约资源:避免了频繁创建和销毁对象所带来的性能开销。
    • 控制实例数目:确保在整个系统中某个类有且仅有一个实例。
    • 全局访问点:提供了一个统一的访问点来获取该类的实例。
  2. 缺点
    • 灵活性差:由于单例模式限制了类的实例化,因此在需要多个实例的情况下不适用。
    • 扩展困难:当需要继承单例类时,可能会因为单例模式的限制而变得困难。
    • 测试不便:由于单例模式的存在,可能会给单元测试带来一定的困难。

总之,单例模式是一种实用的设计模式,适用于那些需要频繁创建和销毁的对象的场景。在实际开发中,应根据具体需求选择合适的实现方式,并注意避免其潜在的缺点。

JDK中的单例模式 

一、介绍

JDK中的单例模式主要体现在Runtime类和GUI相关类中。以下是对JDK中的单例模式的具体介绍:

  1. Runtime类
    • 定义与特点:在Java中,每个Java应用程序都有一个与之关联的Runtime实例,这个实例封装了Java运行时的环境。由于Java是单进程的,因此在一个JVM进程中,只能有一个Runtime实例,这正符合单例模式的特点。
    • 实现方式:Runtime类使用了饿汉式单例模式,即在类加载时就创建好一个静态的对象供外部使用。这种方式简单且线程安全,因为实例在类加载时就已经创建完成,后续访问不涉及同步问题。
  2. GUI相关类
    • 定义与特点:除了Runtime类外,JDK中的GUI相关类也采用了单例模式。这些类通常在第一次使用时才进行实例化,以避免影响JVM的启动速度。
    • 实现方式:与Runtime类不同,GUI相关类采用的是懒汉式单例模式,即在真正需要的时候再创建实例。这种方式虽然节省了资源,但在多线程环境下需要额外的同步措施来保证线程安全。
  3. 其他应用
    • 枚举实现:在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向同一个文件中打日志时的高效性和线程安全性。

  1. 高效的日志记录:Log4j使用异步日志记录机制,这意味着日志消息不会立即写入文件,而是先存储在内存中的一个缓冲区中。当缓冲区达到一定大小或者在一定时间间隔后,才会将缓冲区中的日志消息批量写入文件。这种方式可以减少磁盘I/O操作的次数,从而提高日志记录的效率。

  2. 线程安全:虽然Log4j的Logger对象是单例的,但实际的日志记录过程是由内部的Appender负责的。Appender负责将日志消息写入目标位置(如文件、控制台等)。Log4j提供了多种Appender实现,其中一些支持多线程访问,例如FileAppenderRollingFileAppender。这些Appender内部使用了同步机制来确保线程安全,例如使用锁或其他并发工具来避免多个线程同时写入同一个文件。

  3. 配置优化:为了进一步提高性能和线程安全性,可以通过合理配置Log4j来实现。例如,可以设置合适的缓冲区大小和刷新间隔,以及选择合适的Appender类型。此外,还可以通过调整日志级别来减少不必要的日志记录,从而减轻系统负担。

总之,Log4j通过异步日志记录、线程安全的Appender实现以及合理的配置优化,能够高效地处理多个Logger向同一个文件中打日志的情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值