我们在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();
}
}
以上代码已经达到触发死锁的条件,我们来分析一下死锁产生的时序:
- [bg thread]
TcpRequestHandler
执行类加载并获取initialization lock
进行类初始化 - [main thread]
UdpRequestHandler
执行类加载并获取initialization lock
进行类初始化 - [bg thread]
TcPRequestHandler
的static块触发RequestHandlerRegistrar
的类加载并获取initialization lock
进行类初始化 - [main thread]
UdpRequestHandler
的static块触发RequestHandlerRegistrar
的类加载并等待initialization lock
的释放 - [bg thread]
RequestHandlerRegistrar
的static块触发UdpRequestHandler
的类加载并等待initialization lock
的释放
4.5 为了请求对方资源分别进入等待状态,形成死锁。
需要注意下面的路径因为发生在单个线程中,根据Synchronized的可重入机制不会造成死锁
- [bg thread]
TcpRequestHandler
执行类加载并获取initialization lock
进行类初始化 - [bg thread]
TcPRequestHandler
的static块触发RequestHandlerRegistrar
的类加载并获取initialization lock
进行类初始化 - [bg thread]
RequestHandlerRegistrar
的static块触发TcPRequestHandler
的类加载,虽然1中的初始化尚未完成,由于可重入锁机制,此时能够再次获取锁,不会阻塞。但是会发现初始化正在进行中,所以不会进行再次初始化,否则会形成死循环。
参考:javase规范