最近做项目,想要把一个服务线程Worker做成单例模式。但是,发现在多ClassLoader实例加载下,很难控制Worker实例的数量,实现全局的单例。
类加载层次
我们系统为了方便各个服务热启动,给每个服务分配一个ClassLoader实例,来实现动态加载。结合JDK的ClassLoader加载链,可以分成下面5层:
BootstrapLoader -> ExtClassLoader -> AppClassLoader -> ServerClassLoader-> ServiceClassLoader
前三层是JDK的,这里就不细说,具体可以参考见我的另一篇博文:JVM类加载机制-ClassLoader
后面两层,ServerClassLoader负责加载我们框架的主要代码;而ServiceClassLoader负责加载各个服务的业务代码。这里实现服务的热启动加载就是通过多个ServiceClassLoader实例来实现的。
单例 变 多例
我在服务A创建了一个单例的Worker来给其他服务调用。如下图所示,我的服务A和其他服务平级,所以使用各自ServiceClassLoader实例来加载。
我启动服务A,会生成一个worker单例,而当其他服务调用worker的时候,他们的ServiceClassLoader实例会找不到服务A的worker,又会重新创建一个worker实例。这样worker就没办法单例了,至少每个调用到的服务都会创建一个。
方案对比
l 目前方案
只能保证在一个ServiceClassLoader实例中worker的单例,即每个服务中worker的单例,而不能实现整个应用worker的单例。
而且,在worker代码变更,需要重新加载的时候,调用到worker的服务也需要重启。
l 单例化worker
把worker交给ServerClassLoader来加载。因为整个应用只有一个ServerClassLoader实例,所以能保证整个应用worker的单例。
但是这样就不能热加载worker了。如果worker的代码变化了,只能重启整个应用,丧失了服务热加载的特性。同时,弱化代码层次,worker就得归到系统层,而不再是服务层了。
Demo
说了半天,下面来复现一下多个ClassLoader实例加载多个“单例”的情况。
首先,我们简单实现一个单例:使用静态成员,实际上是通过JVM的机制来保证 MultiLoadBean.own的全局单例性。
下面代码,如果使用JDK的ClassLoader来加载,在MultiLoadBean被加载的时候,只会创建一个MultiLoadBean.own并打印。
public class MultiLoadBean {
private static MultiLoadBean own = new MultiLoadBean();
static {
System.out.println(own);
}
}
为了实现多个ClassLoader实例加载MultiLoadBean的效果,下面要做两件事情:
l 把classpath加载好的MultiLoadBean.class移到classpath外,如和src同级的urlClass目录下,并按路径存放好:/urlClass/jscai/classLoader/MultiLoadBean.class 。
l 使用不同的URLClassLoader实例来加载MultiLoadBean。
public static void main(String[] args) throws Exception {
String folder = "urlClass";
String url = "file:/" + System.getProperty("user.dir") + File.separator + folder + File.separator;
URL[] arrUrl = new URL[]{new URL(url)};
ClassLoaderloader1 = new URLClassLoader(arrUrl);
ClassLoaderloader2 = new URLClassLoader(arrUrl);
loader1.loadClass("jscai.classLoader.MultiLoadBean").newInstance();
loader2.loadClass("jscai.classLoader.MultiLoadBean").newInstance();
}
=============Console==================
jscai.classLoader.MultiLoadBean@3764951d
jscai.classLoader.MultiLoadBean@3cd1a2f1
如上所示,MultiLoadBean被打印了两次,而且内存地址都不同,说明MultiLoadBean被加载了两次。这就是“单例”被多个ClassLoader实例加载,就会变成“多例”。
要变回单例很简单,就是把MultiLoadBean.class放回classpath,再运行一次,就会发现,又回到了单例时代了。
具体原因见我的另一篇博文:JVM类加载机制-ClassLoader