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

Clazz clazz = new Clazz();
  • 1.

我们可以按如下方式读取代码片段:

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

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

  1. 所有构造函数都需要声明为 “” 构造函数。private
  • 它防止在类外部创建带有“”运算符的对象。new
  1. 需要一个私有常量/变量对象持有器来保存单例对象;即,需要声明私有静态或私有静态最终类变量。
  • 它保存单例对象。它充当单例对象的单一引用源
  • 按照惯例,变量命名为 或 。INSTANCEinstance
  1. 需要允许其他对象访问单一实例对象的静态方法。
  • 此静态方法也称为静态工厂方法,因为它控制类对象的创建。
  • 按照惯例,该方法被命名为 。getInstance()

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

1. 静态热切单例类

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

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();
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.


在  JVM 中加载类本身时创建单例对象,并将其分配给常量。 提供对此常量的访问。INSTANCEgetInstance()

虽然编译时对属性 的依赖关系很好,但有时需要运行时依赖关系。在这种情况下,我们可以利用静态块来实例化单例。

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;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.


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

2. 动态懒惰单例类

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

public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton(){}

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

 

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

public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton(){}

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

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.


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

3. 动态懒惰改进的单例类

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;
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.


我们可以只通过双重检查或双重检查锁定来锁定块,而不是锁定整个方法,以提高性能和线程争用。getInstance()

public class EagerAndLazySingleton {

    private EagerAndLazySingleton(){}

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

    public static EagerAndLazySingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.


仅当调用该方法时,才会创建单一实例对象。它是一个 Java 内存安全的单例类。它是一个线程安全的单例,并且是延迟加载的。它是使用最广泛和推荐的。getInstance()

尽管性能和安全性有所提高,但为类创建一个对象的唯一目标仍然受到 Java 中内存引用、反射序列化的挑战。

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

所有这些都会影响静态和动态单例。为了克服这些挑战,它要求我们将实例持有者声明为易失性和覆盖,并且是 Java 中所有类的默认父类。equals()hashCode()readResolve()Object.class

4. 带枚举的单例

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

public enum EnumSingleton {
    INSTANCE;
}
  • 1.
  • 2.
  • 3.


这些是伪装的静态渴望单例,线程安全。最好选择需要静态急切初始化的单例的枚举。

5. 具有函数和库的单例

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

我有幸使用  Guava 库来解释基于供应商的单例对象实例化,以避免大量繁重的代码行。将函数作为参数传递 函数式编程的关键特性虽然 supplier 函数提供了一种实例化对象生产者的方法,但在我们的例子中,生产者必须只生产一个对象,并且应该在单个实例化后重复返回相同的对象。我们可以记住/缓存创建的对象。使用 lambda 定义的函数通常被延迟调用以实例化对象,而记忆技术有助于延迟调用动态单例对象的创建。

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();
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.


函数式编程、供应商函数和记忆有助于使用缓存机制准备单例。当我们不想进行繁重的框架部署时,这是最有用的。

6. 带框架的单例:Spring、Guice

为什么还要担心通过供应商准备对象和维护缓存呢? 像 Spring 和 Guice 这样的框架在 POJO 对象上工作,以提供和维护单例。

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

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);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.


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

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);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.


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

以下是在“单例工厂”中利用单例对象的方式。

  • Factory Method、Abstract Factory 和 Builders 与 JVM 中特定对象的创建和构造相关联。无论我们设想在哪里构建具有特定需求的对象,我们都可以发现单例的需求。可以查看和发现单例的其他地方如下。
  1. 原型或蝇量级
  2. 对象池
  3. 立 面
  4. 压条
  5. 上下文和类装入器
  6. 缓存
  7. 贯穿各领域的关切和面向方面的方案编制

当我们解决业务问题和非功能性需求约束(如性能、安全性以及 CPU 和内存限制)的用例时,就会出现模式。给定类的单例对象就是这样一种模式,它的使用要求将落到位以发现。该类本质上是创建多个对象的蓝图,但需要动态异构容器来准备“上下文”、“层”、“对象池”和“战略功能对象”,这确实促使我们利用声明全局可访问或上下文可访问的对象。