Java虚拟机通过类的方法加锁实现类初始化,确保静态代码块仅执行一次;多线程首次使用同一类时,JVM用Class对象内置锁保证初始化排他性,异常会导致初始化失败且不可逆。

Java 虚拟机对类初始化的锁获取逻辑,本质上是通过类的 <clinit></clinit> 方法加锁实现的——这个过程对开发者透明,但它的行为直接反映在代码块执行顺序上。掌握它不需要深入 JVM 源码,只需观察“多线程首次主动使用同一类”时静态代码块的执行表现即可。
静态代码块执行一次就说明初始化锁已生效
当多个线程同时执行 new MyClass(),而 MyClass 尚未初始化,JVM 会确保只有一个线程进入 <clinit></clinit> 执行静态代码块,其余线程阻塞等待。这正是类初始化锁(java.lang.Class 对象的 intrinsic lock)的作用。
- 静态代码块只输出一次 → 表明 JVM 成功用锁排他性地完成了初始化
- 若出现两次或以上相同静态块输出 → 说明类被重复加载(如不同 ClassLoader),不是锁失效,而是根本没走同一条初始化路径
- 没有同步关键字、没有显式锁操作,但效果等价于:
synchronized(MyClass.class) { /* 执行静态初始化 */ }
用双重检查验证锁的阻塞与唤醒行为
写一个带延时和日志的静态块,配合两个线程启动,能直观看到锁的抢占和等待:
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
- 在静态块第一行打印“开始初始化”,最后一行打印“初始化完成”
- 主线程启动后立即 fork 两个子线程,都调用
MyClass.getValue()(一个静态方法) - 观察输出:必然先出现一个“开始初始化”,再出现“初始化完成”,之后才出现第二个线程的返回值 —— 中间空档就是另一个线程在等待锁释放
- 注意:不能用
System.out.println做精确计时,但日志顺序绝对可靠
继承场景下锁按类粒度独立获取
父类和子类各有自己的 <clinit></clinit> 和对应锁。它们的初始化锁互不干扰,但有严格触发依赖:
立即学习“Java免费学习笔记(深入)”;
- 子类初始化一定会触发父类初始化,所以父类锁先被获取并释放,子类锁才可能被争抢
- 若线程 A 正在初始化父类,线程 B 尝试 new 子类 → B 会先等待父类锁,父类完成后才尝试获取子类锁
- 若线程 C 同时调用
Parent.doStatic(),它只争抢Parent.class的锁,与子类初始化无直接竞争 - 每个类的锁只保护自己那块静态初始化逻辑,不跨类
异常会释放锁并标记类为“初始化失败”
这是实战中最容易忽略的关键点:静态块中抛出未捕获异常,会导致锁释放,且该类后续任何主动使用都会立即抛 ExceptionInInitializerError。
- 例如:静态块里读配置文件失败 → 第一个线程抛异常,锁释放;第二个线程进来直接报错,不再重试初始化
- 这种状态不可逆,重启应用或重新加载类(新 ClassLoader)才能恢复
- 因此,静态块内应尽量避免可能失败的 I/O 或网络操作;必须做时,要 try-catch 并提供默认值或明确兜底逻辑
- 可通过
Class.forName("X", false, loader)+loadClass绕过初始化,但无法绕过已失败的类

















