Java中6种单例实现方法

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

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

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

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

    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 中加载类本身时创建的,因为所有静态块都是在加载时执行的。对实例变量的访问由 getInstance() 静态方法提供。

    2.动态懒惰单例类
    单例更适合应用配置和应用属性。考虑到异构容器创建、对象池创建、层创建、门面创建、轻量级对象创建、每个请求的上下文准备以及会话等:它们都需要动态构建一个单例对象,以实现更好的 "关注分离"。在这种情况下,就需要动态的懒单件。

    public class LazySingleton {

        private static LazySingleton instance;

        private LazySingleton(){}

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

    只有在调用 getInstance() 方法时才会创建单例对象。与静态急切单例类不同,该类不是线程安全的。

    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.动态懒惰改进单件类

    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() 方法,我们可以只锁定具有双重检查或双重检查锁定的代码块,以提高性能和线程竞争性。

    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),脏对象读取可能会随时发生。
    • 反射:通过反射,可以将私有构造函数公开,并创建一个新实例。
    • 序列化:序列化后的实例对象可用于创建同一类的另一个实例。

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

    4.使用枚举的单例
    如果将枚举用于静态急切单例,就可以避免内存安全性、反射和序列化问题。

    public enum EnumSingleton {
        INSTANCE;
    }

    这些都是变相的静态急迫单例,是线程安全的。在需要静态急于初始化的单例时,最好选择枚举。

    5.使用函数和库的单例
    虽然了解单例中的挑战和注意事项是必须的,但既然可以利用成熟的库,为什么还要担心反射、序列化、线程安全和内存安全呢?Guava 就是这样一个广受欢迎的成熟库,它处理了许多编写高效 Java 程序的最佳实践。

    我有幸使用 Guava 库解释了基于供应商的单例对象实例化,从而避免了大量繁重的代码行。将函数作为参数传递是函数式编程的主要特点。虽然供应商函数提供了一种实例化对象生产者的方法,但在我们的例子中,生产者必须只生产一个对象,并且在一次实例化后应不断重复返回相同的对象。我们可以对创建的对象进行 memoize/缓存。使用 lambdas 定义的函数通常会被懒散地调用来实例化对象,而 memoization 技术有助于懒散地调用动态单例对象创建。

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

    函数式编程、供应商函数和 memoization 有助于利用缓存机制准备单子。这在我们不需要大量框架部署时最为有用。

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

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

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

    以下是使用 "单子工厂 "利用单子对象的方法。

    • 工厂方法、抽象工厂和构建器与在 JVM 中创建和构建特定对象有关。只要我们设想构建一个有特定需求的对象,就能发现单例的需求。以下是可以查看和发现单例的其他地方。
    • 原型 或轻量级
    • 对象池
    • facade
    • 分层
    • 上下文和类加载器
    • 缓存
    • 横向关注点和面向方面的编程 AOP

    结论
    当我们为业务问题和非功能性需求约束(如性能、安全性、CPU 和内存约束)解决用例时,就会出现模式。给定类的单例对象就是这样一种模式,对其使用的要求也会落到实处。类的本质是创建多个对象的蓝图,然而,动态异构容器需要准备 "上下文"、"层"、"对象池 "和 "函数对象",这确实促使我们利用声明全局可访问或上下文可访问的对象。