如何创建一个保证整个JVM进程只能使用一次的Java类实例?然后,在该JVM上运行的每个应用程序都应该能够使用该单例实例。
答案 0 :(得分:13)
事实上你可以实现这样的单身人士。在评论中向您描述的问题是类可能由多个ClassLoader
加载。然后,这些ClassLoader
中的每一个都可以定义一个相同名称的类,它将错误地认为是唯一的。
但是,您可以通过实现单例的访问器来避免这种情况,该访问器明确依赖于检查特定ClassLoader
类的给定名称,该类又包含您的单例。通过这种方式,您可以避免单个实例由两个不同的ClassLoader
提供,并且复制您需要在整个JVM中唯一的实例。
由于后面会解释的原因,我们会将Singleton
和SingletonAccessor
分成两个不同的类。对于以下课程,我们稍后需要确保始终使用特定的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全局单例实例的调用。请注意,我们需要重新加载反射方法,即使它们是签名相同的,因为我们不能依赖委托的ClassLoader
和JVM全局类是相同的。
实际上,您可能希望将调用缓存到SingletonAccessor.get()
甚至可能是反射方法查找(与反射方法调用相比,它们相当昂贵)。但这种需求在很大程度上取决于您的应用领域如果您的构造函数层次结构有问题,您还可以将方法签名分解为一个接口,并为上述访问者和Singleton
类实现此接口。