Object
Object 源码分析
所有类的根父类,定义了 Java 对象的最小契约:身份(getClass / hashCode / equals)、表示(toString)、复制(clone)、以及基于对象监视器的线程协作(wait / notify)。
HashMap 的键、synchronized 的锁对象、对象判等全部落在这几个方法上。
代码块收起展开
// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.Object
public class Object {
@IntrinsicCandidate // JVM 内建实现:JIT 会用固化的快速版本替换调用,下同
public Object() {}
// final:运行时类型由 JVM 保证,子类不许伪造。返回的 Class 对象就是 static synchronized 方法锁的那个对象
@IntrinsicCandidate
public final native Class<?> getClass();
// 默认实现是"身份哈希":首次调用时生成并缓存进对象头 mark word,之后终身不变(GC 移动对象也不变)。
// 契约:equals 相等 => hashCode 必须相等。只重写 equals 不重写 hashCode,对象进 HashMap 会散到不同桶,"存了却查不到"
@IntrinsicCandidate
public native int hashCode();
// 默认语义 = 是不是同一个对象。按业务字段判等必须重写,且满足自反/对称/传递/一致
public boolean equals(Object obj) {
return (this == obj);
}
// 浅拷贝:逐字段复制,引用字段仍指向同一对象。protected + 要求实现 Cloneable 双重门槛,防止随手拷贝可变状态
@IntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode()); // 重写了 hashCode,这里的默认输出也跟着变
}
// ... wait/notify 线程协作部分见下一段 ...
// 已废弃且待移除(JEP 421):执行时机不可控、能复活对象、异常被吞。清理资源用 Cleaner 或 try-with-resources
@Deprecated(since="9", forRemoval=true)
protected void finalize() throws Throwable { }
}线程协作三件套。调用前提:当前线程必须持有该对象的 monitor,否则抛 IllegalMonitorStateException:
代码块收起展开
// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.Object 线程协作部分
// 从该对象的等待集(WaitSet)任选一个线程唤醒,选哪个由 JVM 决定,业务代码不能依赖顺序
@IntrinsicCandidate
public final native void notify();
// 全部唤醒,各自重新抢锁、重查条件。多种线程等不同条件时只能用它,否则可能"唤醒错人"导致信号丢失
@IntrinsicCandidate
public final native void notifyAll();
public final void wait() throws InterruptedException {
wait(0L); // 0 = 不计时,等到被唤醒为止
}
// 坑:wait(long) 早已不是 native(JDK 19 起)。Java 层做参数校验和虚拟线程适配,真正的挂起在 native 的 wait0
public final void wait(long timeoutMillis) throws InterruptedException {
if (timeoutMillis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (Thread.currentThread() instanceof VirtualThread vthread) {
try {
wait0(timeoutMillis);
} catch (InterruptedException e) {
// virtual thread's interrupt status needs to be cleared
vthread.getAndClearInterrupt(); // 虚拟线程的中断位存在 VirtualThread 对象上,native 层清不到,Java 层补清后再抛
throw e;
}
} else {
wait0(timeoutMillis);
}
}
// final modifier so method not in vtable
private final native void wait0(long timeoutMillis) throws InterruptedException;
public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
if (timeoutMillis < 0) {
throw new IllegalArgumentException("timeoutMillis value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0 && timeoutMillis < Long.MAX_VALUE) {
timeoutMillis++; // "纳秒精度"是假的:nanos 只要 > 0 就把毫秒向上取整 +1,然后被丢弃
}
wait(timeoutMillis);
}wait 必须放在循环里判断条件,不能用 if:
代码块收起展开
synchronized (lock) {
while (!conditionMet()) { // 用 while 不用 if:应对虚假唤醒 + 唤醒后条件可能又被别人消费掉
lock.wait(); // 释放锁并挂起,被唤醒后重新拿锁、回到 while 再查一遍
}
// 走到这里才真正满足条件
}重写 equals + hashCode 的标准写法(决定对象能否正确用作 Map 的键):
代码块收起展开
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age); // 参与 equals 的字段,就要参与 hashCode
}原理串讲
以生产者-消费者为例走一遍完整链路。消费者线程进入 synchronized (queue),发现队列为空,调用 queue.wait()。
Java 层依次经过 wait() → wait(0L) → wait(long) 的负数校验,平台线程直接落到 native 的 wait0(0L)。
JVM 在 wait0 里操作这个对象关联的 ObjectMonitor:把当前线程挂进 monitor 的 WaitSet,然后完全释放锁——重入了几层就退几层,同时记下重入次数——最后把线程 park 挂起。
释放锁这一步是 wait 与 Thread.sleep 的本质区别:不释放锁,生产者就永远进不了临界区,条件永远不会变,等待就成了死等。
生产者随后拿到锁、放入元素、调用 queue.notify()。JVM 从 WaitSet 里取一个线程移入 EntryList(抢锁队列)。
注意 notify 本身不交出锁:被唤醒的线程要等生产者退出 synchronized 块后,和其他线程一起公平竞争 monitor;抢到后 JVM 恢复它之前记下的重入次数,wait0 返回,线程回到 Java 层 while 的条件判断——从被 notify 到真正重新持锁之间存在窗口,条件完全可能已被别的线程消费掉,这是必须用 while 的第二个理由(第一个是规范明确允许的虚假唤醒:底层条件变量原语本身就可能无故返回,JVM 不为屏蔽它付出代价,把复查责任交给调用方)。
为什么 wait/notify 定义在 Object 上而不是 Thread 上?因为 Java 把 monitor 做进了每个对象的对象头,任何对象都能当锁;而条件等待必须和某把锁绑定——只有先持锁,“检查条件”和”进入等待”才是原子的,否则两步之间别的线程完成”改条件 + notify”,唤醒信号就永久丢失(lost wakeup)。
等待集合跟着锁走,API 自然就挂在锁对象上,这也是三件套都要求先持有该对象锁的根源。
为什么 JDK 19 起把 wait(long) 从 native 拆成 Java 壳 + wait0?为虚拟线程留钩子:虚拟线程的中断状态记录在 VirtualThread 对象里,native 代码处理不了这种 Java 层的类型分支,于是把校验、instanceof VirtualThread 判断和中断位清理都放在 Java 层,native 只保留最小的挂起原语。
wait0 标成 private final 是为了不在虚方法表里占条目(源码注释原话:final modifier so method not in vtable)。
设计取舍
- notify 只省了无效唤醒的开销,却要求”所有等待者都在等同一个条件”;拿不准就用 notifyAll,正确性优先于性能。
- 默认 hashCode 与内存地址没有必然关系(HotSpot 默认用随机数生成后缓存进 mark word),“返回内存地址”是流传很广的口误。
- clone 的设计公认失败:Cloneable 是个空标记接口,clone 却定义在 Object 上还是 protected。工程上优先拷贝构造器。
wait(timeoutMillis, nanos)的纳秒参数不提供纳秒精度,只做向上取整;要高精度定时等待用LockSupport.parkNanos或 Condition。- 业务代码优先 BlockingQueue / Condition,但 synchronized + wait/notify 是所有条件队列的心智原型,值得吃透。