我们在Java的Class中通过statc{...}
可以进行静态变量初始化等工作,被称作静态初始化块( 简称static块)
,static块在类第一次加载的时候执行
哪些场景会触发类加载呢?
Class.forName(“..”)
Class.newInstance()
或者 new
关键字创建实例无论上述何种方式触发的类加载,都会触发类初始化,类初始化过程中会执行static块,而且在类的整个生命周期中只执行一次。这看起来是理所当然,但是大家有没有想过,当在多线程环境下是如何保证类初始化只执行一次的?
类初始化被JVM当做Synchronized
代码块对待,每个类持有一个initialization lock
,第一个触发类初始化的线程获取该锁并执行static块,执行完毕后设置fully initialized
状态并释放锁。其他类加载被触发时,可以通过fully initialized
状态判断是否需要执行类初始化。整个过程类似一个线程安全的单例模型。
因为类初始化的本质是Synchronized
代码块,多个类同时初始化时在多线程环境下有可能造成死锁,需要我们特别小心,举例如下:
我们定义一个用来进行服务发现的接口IRequestHandler
package com.my;
public interface IRequestHandler {
void handleRequest();
}
通过RequestHandlerRegistrar
类注册实际的服务实现。static块中为了保证所有服务实现类都被加载,会触发各服务实现类的加载和初始化。
package com.my;
import java.util.concurrent.CopyOnWriteArrayList;
public class RequestHandlerRegistrar {
private static String[] handlerNames = new String[]{"com.my.TcpRequestHandler", "com.my.UdpRequestHandler"};
private static CopyOnWriteArrayList<IRequestHandler> registeredHandlers = new CopyOnWriteArrayList<IRequestHandler>();
static {
try {
// through the service locator this class would find handler class names in real world scenario
final Class<?> aClass1 = Class.forName(handlerNames[1]);
aClass1.newInstance();
final Class<?> aClass = Class.forName(handlerNames[0]);
aClass.newInstance();
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
public static synchronized void register(IRequestHandler handler) {
if (!registeredHandlers.contains(handler)) {
registeredHandlers.add(handler);
}
}
}
TcpRequestHandler
和UdpRequestHandler
分别是IRequestHandler
的一个实现。
package com.my;
//TcpRequestHandler
public class TcpRequestHandler implements IRequestHandler {
static {
RequestHandlerRegistrar.register(new TcpRequestHandler());
}
@Override
public void handleRequest() {
System.out.println("Handle request in TCP request handler");
}
}
//UdpRequestHandler
public class UdpRequestHandler implements IRequestHandler {
static {
RequestHandlerRegistrar.register(new UdpRequestHandler());
}
@Override
public void handleRequest() {
System.out.println("Handle request in UDP request handler");
}
}
static块中分别通过RequestHandlerRegistrar
的静态方法注册自己,需要注意此处可能触发RequestHandlerRegistrar
的类加载和初始化
主进程中需要加载并使用两个Handler,如下:
package com.my;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
//bg treahd
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(() -> {
try {
Class.forName("com.my.TcpRequestHandler").newInstance();
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
);
try {
Class.forName("com.my.UdpRequestHandler").newInstance();
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
executorService.shutdown();
}
}
以上代码已经达到触发死锁的条件,我们来分析一下死锁产生的时序:
TcpRequestHandler
执行类加载并获取initialization lock
进行类初始化UdpRequestHandler
执行类加载并获取initialization lock
进行类初始化TcPRequestHandler
的static块触发RequestHandlerRegistrar
的类加载并获取initialization lock
进行类初始化UdpRequestHandler
的static块触发RequestHandlerRegistrar
的类加载并等待initialization lock
的释放RequestHandlerRegistrar
的static块触发UdpRequestHandler
的类加载并等待initialization lock
的释放4.5 为了请求对方资源分别进入等待状态,形成死锁。
需要注意下面的路径因为发生在单个线程中,根据Synchronized的可重入机制不会造成死锁
TcpRequestHandler
执行类加载并获取initialization lock
进行类初始化TcPRequestHandler
的static块触发RequestHandlerRegistrar
的类加载并获取initialization lock
进行类初始化RequestHandlerRegistrar
的static块触发TcPRequestHandler
的类加载,虽然1中的初始化尚未完成,由于可重入锁机制,此时能够再次获取锁,不会阻塞。但是会发现初始化正在进行中,所以不会进行再次初始化,否则会形成死循环。参考:javase规范