如何创建一个 JVM 全局单例?
我的灵感来自 这个 stackoverflow 问题
如何创建一个保证对整个 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
为此,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
类的所有方法,并将它们的调用委托给 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.
相关文章