如何创建JVM全局Singleton?

我从这个stackoverflow问题中汲取灵感

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

事实上你可以实现这样的单身人士。 注释中描述的问题是类可能由多个ClassLoader 。 然后,这些ClassLoader的每一个都可以定义一个相同名称的类,它将错误地认为是唯一的。

但是,您可以通过实现单例的访问器来避免这种情况,该访问器明确依赖于检查特定ClassLoader以获取同样包含您的单例的给定名称的类。 这样,您可以避免单个实例由两个不同的ClassLoader提供,这样就可以复制整个JVM中唯一的实例。

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

 package pkg; class Singleton { static volatile Singleton instance; } 

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

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

 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运行, ClassLoaderSingleton的类加载委托给系统类加载器。 对于在应用程序服务器上运行的应用程序尤其如此,该应用程序服务器通常为其类加载器实现子级优先语义,并且不会向系统类加载器询问已知类型,而是首先尝试加载其自己的类型。 请注意,运行时类型具有两个特征:

  1. 它的完全限定名称
  2. 它的ClassLoader

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

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

如果你不能实现这一点,那么通过使用我的代码生成库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全局单例实例的调用。 请注意,我们需要重新加载reflection方法,即使它们是签名相同的,因为我们不能依赖委托的ClassLoader和JVM全局类是相同的。

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