如何创建一个 JVM 全局单例?

Posted

技术标签:

【中文标题】如何创建一个 JVM 全局单例?【英文标题】:How to create a JVM-global Singleton? 【发布时间】:2014-05-03 14:02:51 【问题描述】:

我的灵感来自this *** question

如何创建一个保证对整个 JVM 进程只可用一次的 Java 类实例?然后,在该 JVM 上运行的每个应用程序都应该能够使用该单例实例。

【问题讨论】:

由于这些应用程序使用的所有自定义类加载器,您无法获得该保证。 如果您试图限制对某些对象的访问,您将需要使用某种物理分区。这是一篇关于“什么时候单例不是单例?”的好文章。 oracle.com/technetwork/articles/java/singleton-1577166.html 有很多方法可以获得非单例的单例。 这完全取决于您的执行环境。您最好使用某种文件或操作系统作业属性来存储持久值。 将类打包成*.jar文件,并将jar文件放到jre/lib/ext @Bhesh Gurung 是的,你可以。请参阅下面的答案。 【参考方案1】:

事实上,您可以实现这样的单例。 cmets 中向您描述的问题是一个类可能被多个ClassLoaders 加载。然后,这些ClassLoaders 中的每一个都可以定义一个相同名称的类,这会错误地认为是唯一的。

但是,您可以通过实现对您的单例的访问器来避免这种情况,该访问器显式依赖于检查特定的ClassLoader 以查找再次包含您的单例的给定名称的类。这样,您可以避免由两个不同的ClassLoaders 提供一个单例实例,从而避免复制您需要在整个 JVM 中唯一的实例。

出于稍后解释的原因,我们将SingletonSingletonAccessor 分成两个不同的类。对于以下类,我们需要稍后确保始终使用特定的ClassLoader 访问它:

package pkg;
class Singleton 
  static volatile Singleton instance;

一个方便的ClassLoader 是系统类加载器。系统类加载器知道 JVM 类路径上的所有类,并且根据定义将扩展和引导类加载器作为其父类。这两个类加载器通常不知道任何特定于域的类,例如我们的Singleton 类。这可以保护我们免受不必要的惊喜。此外,我们知道它可以在整个 JVM 运行实例中全局访问和知晓。

现在,让我们假设Singleton 类在类路径上。这样,我们就可以通过这个访问器使用反射来接收实例:

class SingletonAccessor 
  static Object get() 
    Class<?> clazz = ClassLoader.getSystemClassLoader()
                                .findClass("pkg.Singleton");
    Field field = clazz.getDeclaredField("instance");
    synchronized (clazz) 
      Object instance = field.get(null);
      if(instance == null) 
        instance = clazz.newInstance();
        field.set(null, instance);
      
      return instance;
    
  

通过明确指定我们要从系统类加载器加载pkg.Singleton,我们确保我们始终接收到相同的实例,无论哪个类加载器加载了我们的SingletonAccessor。在上面的例子中,我们还确保Singleton 只被实例化一次。或者,您可以将实例化逻辑放入 Singleton 类本身,并让未使用的实例失效,以防加载其他 Singleton 类。

但是有一个很大的缺点。你错过了所有类型安全的方法,因为你不能假设你的代码总是从ClassLoader 运行,它将Singleton 的类加载委托给系统类加载器。对于在应用程序服务器上运行的应用程序尤其如此,该应用程序通常为其类加载器实现子优先语义并且向系统类加载器询问已知类型但首先尝试加载其自己的类型.请注意,运行时类型具有两个特征:

    它的完全限定名称 它的ClassLoader

因此,SingletonAccessor::get 方法需要返回 Object 而不是 Singleton

另一个缺点是必须在类路径中找到Singleton 类型才能使其工作。否则,系统类加载器不知道这种类型。如果您可以将 Singleton 类型放到类路径中,那么您就完成了。没问题。

如果您无法做到这一点,还有另一种方法,例如使用我的code generation library Byte Buddy。使用这个库,我们可以简单地在运行时定义这样一个类型并将其注入到系统类加载器中:

new ByteBuddy()
  .subclass(Object.class)
  .name("pkg.Singleton")
  .defineField("instance", Object.class, Ownership.STATIC)
  .make()
  .load(ClassLoader.getSytemClassLoader(), 
        ClassLoadingStrategy.Default.INJECTION)

您刚刚为系统类加载器定义了一个类pkg.Singleton,上述策略再次适用。

此外,您可以通过实现包装类型来避免类型安全问题。您还可以在 Byte Buddy 的帮助下自动执行此操作:

new ByteBuddy()
  .subclass(Singleton.class)
  .method(any())
  .intercept(new Object() 
    @RuntimeType
    Object intercept(@Origin Method m, 
                     @AllArguments Object[] args) throws Exception 
      Object singleton = SingletonAccessor.get();
      return singleton.getClass()
        .getDeclaredMethod(m.getName(), m.getParameterTypes())
        .invoke(singleton, args);
    
  )
  .make()
  .load(Singleton.class.getClassLoader(), 
        ClassLoadingStrategy.Default.INJECTION)
  .getLoaded()
  .newInstance();

您刚刚创建了一个委托器,它覆盖了 Singleton 类的所有方法,并将它们的调用委托给 JVM 全局单例实例的调用。请注意,即使它们签名相同,我们也需要重新加载反射方法,因为我们不能依赖委托的 ClassLoaders 和 JVM 全局类相同。

实际上,您可能希望缓存对SingletonAccessor.get() 的调用,甚至可能是反射方法查找(与反射方法调用相比,这相当昂贵)。但是这种需求在很大程度上取决于您的应用程序域。如果您的构造函数层次结构有问题,您还可以将方法签名分解为一个接口,并为上述访问器和您的 Singleton 类实现此接口。

【讨论】:

我有类似的要求,并尝试了使用 bytebuddy 定义单例的示例。我可以定义类并检查它,但是,我看不到该字段(即 Class:getFields 返回一个空数组)。我正在使用 Java 11。知道为什么会这样吗? 我想通了:将defineField行更改为:.defineField("instance", Map.class, Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC)似乎可以解决这个问题。

以上是关于如何创建一个 JVM 全局单例?的主要内容,如果未能解决你的问题,请参考以下文章

设计模式:单例设计模式

设计模式4 - 单例模式 Singleton Pattern

单例模式

C++ 这个类和单例的全局声明有啥好的选择吗? [复制]

是时候学习23种设计模式了-单例模式

你真的会用单例模式?