我最近了解到一个java程序可以有多个类加载器。我发现一些 StackOverflow 帖子解释了如何创建单例对象。
常见的方法是这样的:
if (instance == null) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if (classLoader == null) {
classLoader = Singleton.class.getClassLoader();
}
Class<?> sclass = (classLoader.loadClass(Singleton.class.getCanonicalName()));
}
我不太明白这是如何摆脱多个类加载器的问题的。如果您有多个 ClassLoader,则当每个 ClassLoader 尝试加载该类时,该实例将为 null。这不是正确的吗?如果我能得到关于其工作原理的简短解释,我会发现它会很有帮助。
此外,我们什么时候能够检索实际的 Singleton 实例?我可以做类似
instance = (Singleton) sclass.newInstance();
的事情,但这将为每个 ClassLoader
创建一个新实例。
这如何摆脱多个类加载器的问题。
我不明白你指的是哪个问题,但它并没有消除任何问题。
当它们每个人尝试加载类时,实例将为空。
实例不会为空。
我们能够检索实际的 Singleton 实例吗?
当线程拥有所需的ClassLoader时,您可以使用任一方法获取SIngleton实例。
Singleton s = Singleton.INSTANCE;
或
Singleton s = Singleton.getInstance();
取决于您通常如何访问单例。如果您需要在另一个类加载器中使用单例,则必须执行相同的操作,除非通过反射。
有很多这样的例子,但它们似乎是从JavaWorld上的一篇文章复制的。
它并没有消除多个类加载器的问题。您仍然可以拥有多个实例。
它尝试通过线程上下文类加载器加载类。问题是您不能依赖它作为任何特定的类加载器,或者对于每个线程或调用它的位置都相同。如果这不可用,那么它会尝试,
Singleton.class.getClassLoader()
这是无论如何都会使用的类加载器,并且不保证每次都相同。
如果上下文类加载器始终相同,那么每次都会获得相同的实例,但需要通过反射访问该实例。您无法将其转换为正确的类型,否则您会得到一个
ClassCastException
,因为加载该类的类加载器与当前的类加载器不匹配。
这里是一个演示
getClass
方法用法的示例,使用 ByteBuddy 库创建类加载器:
public class Main {
public static void main(String[] args) throws Exception {
run();
run();
}
public static void run() throws Exception {
ClassLoader parent = Main.class.getClassLoader();
byte[] runnerBytes = Files.readAllBytes(new File("/path/to/bin/Runner.class").toPath());
byte[] singletonBytes = Files.readAllBytes(new File("/path/to/bin/Singleton.class").toPath());
Map<String, byte[]> classes = new HashMap<>();
classes.put("Runner", runnerBytes);
classes.put("Singleton", singletonBytes);
ByteArrayClassLoader classLoader = new ByteArrayClassLoader.ChildFirst(
parent,
classes,
null,
ByteArrayClassLoader.PersistenceHandler.MANIFEST,
PackageDefinitionStrategy.NoOp.INSTANCE
);
Class<?> clazz = classLoader.loadClass("Runner");
Thread.currentThread().setContextClassLoader(classLoader);
Object instance = clazz.newInstance();
clazz.getMethod("run").invoke(instance);
}
}
public class Runner {
public void run() {
try {
Object instance = getClass("Singleton").getMethod("getInstance").invoke(null);
Object result = instance.getClass().getMethod("getValue").invoke(instance);
System.out.println("RESULT: " + result);
} catch(ReflectiveOperationException e) {
e.printStackTrace();
}
}
private static Class<?> getClass(String className) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null) {
classLoader = Singleton.class.getClassLoader();
}
return (classLoader.loadClass(className));
}
}
public class Singleton {
private static final Singleton instance = new Singleton();
private double value = Math.random();
public static Singleton getInstance() {
return instance;
}
public double getValue() {
return value;
}
}
结果:
RESULT: 0.8998675708591397
RESULT: 0.7230302140857906
所以这不会创建相同的实例。
这不是创建单例对象的常见方法。
常见的egar做法是这样的:
public class SomeClass {
public static final SomeClass INSTANCE = new SomeClass();
private SomeClass() {}
}
如果你想偷懒,就这样:
public class SomeClass {
private static SomeClass instance;
private SomeClass() {}
public static SomeClass getInstance() {
if (instance == null) instance = new SomeClass();
return instance;
}
}
当类加载器加载类时,静态代码确实会运行。并且该类有可能被多个类加载器加载。在大多数情况下,按照惯例,这是不可能的。正确实现的类加载器应该在尝试尝试之前要求其父级加载该类。在您的情况下,如果上下文类加载器是基类加载器的子级,则无论您询问哪个类加载器,您的 Singleton.class 都应该是完全相同的类。
话虽如此,有一些类加载器打破了这一约定,其中最著名的是 Tomcat 类加载器。当类加载器违反此规则时,您可能会遇到一些非常奇怪的事情。例如,两个具有相同名称和字节码的类彼此不相等。这意味着如果一个对象是使用一个类加载器实例化的,而另一个对象是使用另一个类加载器实例化的,即使它们具有相同的值(它们的 .equals() 方法应该返回 true),但也不会,因为它们的类不匹配。 instanceof 将不起作用,如果您要绕过实例并仅强制转换它,它将抛出 ClassCastException。
这里的底线是你会遇到更大的问题