面试必杀技-Java单例模式

Java 单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。其核心思想是:

  • 私有化构造函数: 防止直接创建实例。
  • 静态成员变量: 存储唯一的实例。
  • 静态方法: 提供获取唯一实例的方法。

Java 编程中,对象创建或类的实例化是使用new运算符和类中声明的公共构造函数完成的,如下所示。

java 复制代码
Clazz clazz = new Clazz();

Clazz()是使用new运算符调用的默认公共构造函数,用于为Clazz类创建或实例化一个对象并将其分配给变量clazz,其类型为Clazz

创建单例时,我们必须确保只创建一个对象或只发生一个类的实例化。为了确保这一点,以下常见事项成为先决条件。

  1. 所有构造函数都需要声明为private构造函数。

    new它可以防止在类外使用" "运算符创建对象。

  2. 需要一个私有常量/变量对象持有者来保存单例对象;即,需要声明一个私有静态或私有静态最终类变量。

    它保存单例对象。它充当单例对象的单一引用源

    • 按照惯例,该变量命名为INSTANCEinstance
  3. 需要一个静态方法来允许其他对象访问单例对象。

    • 此静态方法也称为静态工厂方法,因为它控制类对象的创建。
    • 按照惯例,该方法命名为getInstance()

有了这些理解,让我们更深入地了解单例。以下是为类创建单例对象的 6 种方法。

1. 静态饥饿单例类

当我们掌握了所有实例属性,并且希望只有一个对象和一个类来为彼此相关的一组属性提供结构和行为时,我们可以使用静态热切单例类。这非常适合应用程序配置和应用程序属性。

java 复制代码
public class EagerSingleton {

    private static final EagerSingleton INSTANCE = new EagerSingleton();

    private EagerSingleton() {}

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        EagerSingleton eagerSingleton = EagerSingleton.getInstance();
    }
}

JVM中加载类本身时会创建单例对象并将其分配给INSTANCE常量。getInstance()提供对该常量的访问。

虽然编译时依赖属性是好的,但有时运行时依赖也是必要的。在这种情况下,我们可以使用静态块来实例化单例。

java 复制代码
public class EagerSingleton {

    private static EagerSingleton instance;

    private EagerSingleton(){}

    // static block executed during Class loading
    static {
        try {
            instance = new EagerSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception occurred in creating EagerSingleton instance");
        }
    }

    public static EagerSingleton getInstance() {
        return instance;
    }
}

单例对象是在 JVM 中加载类本身时创建的,因为所有静态块都在加载时执行。对变量的访问instance由静态方法提供getInstance()

2. 动态懒汉单例类

单例更适合于应用程序配置和应用程序属性。考虑异构容器创建、对象池创建、层创建、外观创建、享元对象创建、每个请求的上下文准备和会话等:它们都需要动态构造单例对象,以便更好地分离关注点。在这种情况下,需要动态惰性单例。

java 复制代码
public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton(){}

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

仅当调用该方法时才会创建单例对象getInstance()。与静态饿汉单例类不同,此类不是线程安全的。

java 复制代码
public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton(){}

    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }

}

getInstance()方法需要同步,以确保该getInstance()方法在单例对象实例化中是线程安全的。

3. 动态懒汉改进型单例类

java 复制代码
public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton(){}

    public static LazySingleton getInstance() {
      if (instance == null) {
        synchronized (LazySingleton.class) {
            if (instance == null) {
                instance = new LazySingleton();
            }
        }
      }
      return instance;
    }

}

我们不需要锁定整个getInstance()方法,而是可以仅锁定具有双重检查或双重检查锁定的块,以提高性能和线程争用。

java 复制代码
public class EagerAndLazySingleton {

    private EagerAndLazySingleton(){}

    private static class SingletonHelper {
        private static final EagerAndLazySingleton INSTANCE = new EagerAndLazySingleton();
    }

    public static EagerAndLazySingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

单例对象只在getInstance()方法调用时才创建,是Java内存安全的单例类,线程安全,惰性加载,使用最广泛,推荐使用。

尽管性能和安全性有所改进,但Java 中的内存引用、 反射序列化却对为一个类创建一个对象这一唯一目标提出了挑战。

  • 内存引用: 在多线程环境中,引用的变量可能会发生线程读写的重新排序,如果该变量未声明为volatile,则随时可能发生脏对象读取。
  • 反射: 通过反射,可以将私有构造函数变为公共构造函数,并创建一个新的实例。
  • 序列化: 序列化的实例对象可用于创建同一个类的另一个实例。

所有这些都会影响静态和动态单例。为了克服这些挑战,我们需要将实例持有者声明为 volatile 和 override equals()hashCode()并将readResolve()其声明为 Java 中所有类的默认父类Object.class

4. 带枚举的单例

如果枚举用于静态热切单例,则可以避免内存安全、反射和序列化的问题。

java 复制代码
public enum EnumSingleton {
    INSTANCE;
}

5. 具有功能和库的单例

虽然必须了解单例中的挑战和注意事项,但是当可以利用成熟的库时,为什么还要担心反射、序列化、线程安全和内存安全呢?Guava是一个非常流行且成熟的库,处理了许多编写有效 Java 程序的最佳实践。

我有幸使用Guava 库来解释基于供应商的单例对象实例化,以避免大量繁重的代码行。***将函数作为参数传递是的关键特性 虽然供应商函数提供了一种实例化对象生产者的方法,但在我们的例子中,生产者必须只生产一个对象,并且在一次实例化之后应该不断返回相同的对象。我们可以记忆/缓存创建的对象。使用 lambda 定义的函数通常被延迟调用来实例化对象,而记忆技术有助于延迟调用动态单例对象创建。 这些是伪装的静态热切单例,线程安全。在需要静态热切初始化单例的地方,最好选择枚举。

java 复制代码
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;

public class SupplierSingleton {
    private SupplierSingleton() {}

    private static final Supplier<SupplierSingleton> singletonSupplier = Suppliers.memoize(()-> new SupplierSingleton());

    public static SupplierSingleton getInstance() {
        return singletonSupplier.get();
    }

    public static void main(String[] args) {
        SupplierSingleton supplierSingleton = SupplierSingleton.getInstance();
    }
}

函数式编程、函数提供者和记忆化有助于准备具有缓存机制的单例。当我们不想进行繁重的框架部署时,这非常有用。

6. 单例框架:Spring、Guice

为什么要担心通过供应商准备对象并维护缓存?Spring和Guice等框架使用POJO对象来提供和维护单例。

这在企业开发中被广泛使用,因为许多模块都需要自己的上下文和多层。每个上下文和每个层都是单例模式的良好候选者。

java 复制代码
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

class SingletonBean { }

@Configuration
public class SingletonBeanConfig {

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
    public SingletonBean singletonBean() {
        return new SingletonBean();
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(SingletonBean.class);
        SingletonBean singletonBean = applicationContext.getBean(SingletonBean.class);
    }
}

Spring 是一个非常流行的框架。上下文和依赖注入是 Spring 的核心。

java 复制代码
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;

interface ISingletonBean {}

class SingletonBean implements  ISingletonBean { }

public class SingletonBeanConfig extends AbstractModule {

    @Override
    protected void configure() {
        bind(ISingletonBean.class).to(SingletonBean.class);
    }

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new SingletonBeanConfig());
        SingletonBean singletonBean = injector.getInstance(SingletonBean.class);
    }
}

Google的Guice也是一个准备单例对象的框架,是Spring的替代品。

以下是利用单例工厂利用单例对象的方法。

  • 工厂方法、抽象工厂和构建器与 JVM 中特定对象的创建和构造相关。只要我们设想构造具有特定需求的对象,我们就可以发现单例的需求。可以检查和发现单例的更多位置如下。
  1. 原型或享元型
  2. 对象池
  3. 立面
  4. 分层
  5. 上下文和类加载器
  6. 缓存
  7. 横切关注点和面向方面编程

7. 结论

当我们解决业务问题和非功能性需求约束(如性能、安全性以及 CPU 和内存约束)时,其主要应用场景如数据库连接池、日志记录器、配置管理器、缓存管理器。给指定类的单例对象就是这样一种模式,其使用要求将会自然而然地被发现。类本质上是创建多个对象的蓝图,需要动态异构容器来准备"上下文"、"层"、"对象池"和"策略功能对象",这确实促使我们利用声明全局可访问或上下文可访问的对象。

相关推荐
bluebonnet271 分钟前
【Rust练习】22.HashMap
开发语言·后端·rust
古月居GYH2 分钟前
在C++上实现反射用法
java·开发语言·c++
ifanatic43 分钟前
[面试]-golang基础面试题总结
面试·职场和发展·golang
儿时可乖了1 小时前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
ruleslol1 小时前
java基础概念37:正则表达式2-爬虫
java
Iced_Sheep1 小时前
干掉 if else 之策略模式
后端·设计模式
xmh-sxh-13141 小时前
jdk各个版本介绍
java
XINGTECODE1 小时前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang