如何创建JVM全局Singleton? [英] How to create a JVM-global Singleton?
问题描述
如何创建一个保证整个JVM进程只能使用一次的Java类实例?然后,在该JVM上运行的每个应用程序都应该能够使用该单例实例。
How can one create a Java class instance that is guaranteed to be available only once for the entire JVM process? Every Application that runs on that JVM should then be able to use that singleton instance.
推荐答案
您可以实际上实现这样的单身人士。注释中描述的问题是类可能由多个 ClassLoader
加载。然后,这些 ClassLoader
中的每一个都可以定义一个相同名称的类,它会错误地认为是唯一的。
You can as a matter of fact implement such a singleton. The problem that was described to you in the comments is the possibility of a class being loaded by multiple ClassLoader
s. Each of these ClassLoader
s can then define a class of identical name which would erroneously assume to be unique.
然而,您可以通过实现单例的访问器来避免这种情况,该访问器明确依赖于检查特定的 ClassLoader
给定名称的类,它再次包含您的单例。这样,您可以避免单个实例由两个不同的 ClassLoader
提供,并且这样复制您需要在整个JVM中唯一的实例。
You can however avoid this by implementing an accessor to your singleton which explicitly relies on checking a specific ClassLoader
for a class of a given name which again contains your singleton. This way, you can avoid that a singleton instance is provided by two different ClassLoader
s and such duplicating the instance you required to be unique throughout the JVM.
由于后面会解释的原因,我们将拆分 Singleton
和 SingletonAccessor
分为两个不同的班级。对于以下类,我们稍后需要确保始终使用特定的 ClassLoader
访问它:
For reasons that are explained later, we will split up the Singleton
and the SingletonAccessor
into two different classes. For the following class, we need to later make sure that we always access it by using a specific ClassLoader
:
package pkg;
class Singleton {
static volatile Singleton instance;
}
一个方便的 ClassLoader
对于这个问题是系统类加载器。系统类加载器知道JVM类路径上的所有类,并且每个定义都有扩展名和引导类加载器作为其父类。这两个类加载器通常不知道任何特定于域的类,例如我们的 Singleton
类。这可以保护我们免受意外的惊喜。此外,我们知道它在JVM的运行实例中是可访问的并且是全局已知的。
A convenient ClassLoader
for this matter is the system class loader. The system class loader is aware of all classes on the JVM's class path and has per definition the extension and the bootstrap class loaders as its parents. Those two class loaders do not normally know about any classes that are domain-specific such as our Singleton
class. This safes us from unwanted surprises. Furthermore, we know that it is accessible and known globally throughout a running instance of the JVM.
现在,让我们假设 Singleton
类在类路径上。这样,我们可以使用反射通过此访问器接收实例:
For now, let us assume that the Singleton
class is on the class path. This way, we can receive the instance by this accessor using 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
类的情况下使未使用的实例生效已经加载。
By specifying that we explicitly want to load pkg.Singleton
from the system class loader, we make sure that we always receive the same instance despite of which class loader loaded our SingletonAccessor
. In the above example, we additionally make sure that Singleton
is only instantiated once. Alternatively, you could put the instantiation logic into the Singleton
class itself and make the unused instances rot in case other Singleton
classes are ever loaded.
但是有一个很大的缺点。你错过了所有类型安全的方法,因为你不能假设你的代码总是从 ClassLoader
运行,它委托 Singleton的类加载
到系统类加载器。对于在应用程序服务器上运行的应用程序尤其如此,该应用程序服务器通常为其类加载器实现子级优先语义,并且不向系统类加载器询问已知类型但首先尝试加载其自己的类型。请注意,运行时类型具有两个特征:
There is however a big drawback. You miss all means of type-safety as you cannot assume that your code is always run from a ClassLoader
which delegates the class loading of Singleton
to the system class loader. This is in particularly true for an application run on an application server which often implements child-first semantics for its class loaders and does not ask the system class loader for known types but first tries to load its own types. Note that a runtime type is characterized by two features:
- 其完全限定名称
-
ClassLoader
- Its fully-qualified name
- Its
ClassLoader
因此, SingletonAccessor: :get
方法需要返回 Object
而不是 Singleton
。
For this reason, the SingletonAccessor::get
method needs to return Object
instead of Singleton
.
另一个缺点是必须在类路径上找到 Singleton
类型才能使其工作。否则,系统类加载器不知道此类型。如果你可以将 Singleton
类型放到类路径上,那么你就完成了。没有问题。
Another drawback is the fact that the Singleton
type must be found on the class path for this to work. Otherwise, the system class loader does not know about this type. If you can put the Singleton
type onto the class path, you are done here. No problems.
如果你不能做到这一点,那么有另一种方法可以使用例如我的代码生成库Byte Buddy 。使用这个库,我们可以在运行时简单地定义这样一个类型并将其注入系统类加载器:
If you cannot make this happen, there is however another way by using for example my code generation library Byte Buddy. Using this library, we can simply define such a type at runtime and inject it into the system class loader:
new ByteBuddy()
.subclass(Object.class)
.name("pkg.Singleton")
.defineField("instance", Object.class, Ownership.STATIC)
.make()
.load(ClassLoader.getSytemClassLoader(),
ClassLoadingStrategy.Default.INJECTION)
您刚刚为系统类加载器定义了一个类 pkg.Singleton
,并且上述策略再次适用。
You just defined a class pkg.Singleton
for the system class loader and the above strategy is applicable again.
此外,您可以通过实现包装类型来避免类型安全问题。你也可以在Byte Buddy的帮助下实现自动化:
Also, you can avoid the type-safety issues by implementing a wrapper type. You can also automatize this with the help of 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的所有方法
class并将其调用委托给JVM全局单例实例的调用。请注意,我们需要重新加载反射方法,即使它们是签名相同的,因为我们不能依赖委托的 ClassLoader
和JVM全局类是相同的。
You just created a delegator which overrides all methods of the Singleton
class and delegates their invocation to invocations of the JVM-global singleton instance. Note that we need to reload the reflective methods even though they are signature-identical because we cannot rely on the ClassLoader
s of the delegate and the JVM-global classes to be the same.
实际上,您可能希望将调用缓存到 SingletonAccessor.get()
,甚至可能是反射方法查找(与反射方法调用相比,它们相当昂贵)。但这种需求在很大程度上取决于您的应用领域如果你的构造函数层次结构有问题,你也可以将方法签名分解到一个接口中,并为上面的访问者和你的 Singleton
类实现这个接口。
In practice, you might want to cache the calls to SingletonAccessor.get()
and maybe even the reflective method look-ups (which are rather expensive compared to the reflective method invocations). But this need depends highly on your application domain. If you have trouble with your constructor hierarchy, you could also factor out the method signatures into an interface and implement this interface for both the above accessor and your Singleton
class.
这篇关于如何创建JVM全局Singleton?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!