一文掌握 Object 类里的所有方法(wait、notify、finalize)

Object 概述

Object 类是 Java 中所有类的父类,这个类中包含了若干方法,这也就意味着所有类都将继承这些方法。因此,掌握这个类的方法是非常必要的,毕竟所有类都能为你提供这些方法。

Object 类位于 java.base 模块下 java.lang.Object,其结构如下:

其中 wait0 是私有方法,不用管。我把这些方法分为两类,一类是常用方法(如 hashCodeequals );一类是线程相关方法(如 waitnotify)。 下面就对这些方法一一说明,进而全面掌握这个类的所有方法。

常用方法

1. getClass() 方法

java 复制代码
public final native Class<?> getClass();

返回这个类对应的 Class 对象。final 方法,子类无法覆写。Class 对象是 Java 反射中最重要的一个类。有关反射的内容可以查看这个文章:Java Reflection 反射使用 完全指南 - 掘金 (juejin.cn)

2. hashCode() 方法

java 复制代码
public native int hashCode();

返回这个对象的哈希值。默认情况下,Object 是返回对象在堆内存中的地址。一般来说,如果重写了 equals 方法时,一般也会重写这个方法。 另外,当时你使用 HashMap 这种需要对象哈希值的集合的时候,Java 会自动调用这个方法,用以确定这个对象对应的值放到哪个位置。

3. equals(Object) 方法

java 复制代码
public boolean equals(Object obj) {
        return (this == obj);
    }

这个方法用于判断当前对象是否与传入的 obj 对象相等。在 Object 中,就是使用 == 来进行判断,即判断两个对象在内存中的地址是否相同。但是一般情况下,子类常常需要覆写此方法,来对不同的类做不同的相等判断。

重点注意,如果覆写了 equals 方法的话,也需要将 hashCode 覆写了。这一点也好理解,如果两个对象是相等的,那么这两者的所有内容包括哈希值也应该相同才对。在集合中(如 List),往往会调用 equals 方法,来判断存入的对象是否相同。

在写 equals 时,往往可以参考 Java 中其他类的 equals 方法。这里先给出一个取自于 android.health.connect.datatypes.units.Lengthequals 方法,大家在写的时候可以参照:

java 复制代码
@Override
public boolean equals(Object object) {
    if (this == object) return true;
    if (object instanceof Length) {
        Length other = (Length) object;
        return this.getInMeters() == other.getInMeters();
    }
    return false;
}

4. clone() 方法

java 复制代码
protected native Object clone() throws CloneNotSupportedException;

首先要注意到,这个方法是 protected 的。对于 Object 来说,这个方法返回当前对象的一个浅拷贝,而且只有实现了 Cloneable 接口才可以调用该方法,否则抛出 CloneNotSupportedException 异常。

另外提一点,通过调用 clone 方法创建的对象,是不会调用其构造方法的。 其实这个方法是比较鸡肋的方法,Cloneable 这个注解也并不是一个好的设计。应该避免使用。

5. toString() 方法

java 复制代码
public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

这个方法太常用了,一般子类都会覆写。用于返回对象信息。

线程相关方法

1. wait() 方法

java 复制代码
public final void wait() throws InterruptedException {
        wait(0L);
    }

其实线程相关的这几个方法都是关联的,懂了其中两个方法就懂了其他的。关键还是在于对锁的理解。

大家基本都知道这个方法就是让线程等待,但怎么等待,又怎么唤醒,估计大部分人很难说明其用法。

首先,这个 wait 方法确实是让线程等待,但其与 sleep 不同,如果你直接在线程中的调用,会出现java.lang.IllegalMonitorStateException 异常,如下:

java 复制代码
public class Hello {
    public static void main(String[] args) {
        Object obj = new Object();
        System.out.println("before wait---");
        try {
            obj.wait();
        } catch (InterruptedException exception) {
            exception.printStackTrace();
        }
        System.out.println("after wait---");
    }
}

异常信息:

ruby 复制代码
mi@mi-HP:~/develop/code/JavaCode$ java Hello.java 
before wait---
Exception in thread "main" java.lang.IllegalMonitorStateException: current thread is not owner
        at java.base/java.lang.Object.wait0(Native Method)
        at java.base/java.lang.Object.wait(Object.java:375)
        at java.base/java.lang.Object.wait(Object.java:348)
        at Hello.main(Hello.java:24)

可以看到,走到 obj.wait() 时发生了崩溃,IllegalMonitorStateException 是一个运行时异常,翻译过来就是"非法监视器状态异常"。它表示线程在没有持有相应监视器锁的情况下执行 waitnotify 等操作,而后面的描述 "current thread is not owner",也表示当前线程并不是持有者。那么当前线程不是谁的持有者呢?

Java 规定,只有已经获取锁的线程,才可以调用锁的 wait()notify()方法,这个锁是同步代码块,也可以是同步方法。上面说的线程不是持有者,其实就是这个锁的持有者。下面我们更改一下代码:

java 复制代码
    public static void main(String[] args) {
        Object obj = new Object();
        synchronized(obj) {            //同步代码块,持有锁 obj
            System.out.println("before wait---");
            try {
                obj.wait();
            } catch (InterruptedException exception) {
                exception.printStackTrace();
            }
            System.out.println("after wait---");
        }
    }

再运行一下,程序正常运行,没有抛出 IllegalMonitorStateException 异常,并在打印 "before wait---" 后等待在那里,线程进入阻塞状态:

ruby 复制代码
mi@mi-HP:~/develop/code/JavaCode$ java Hello.java 
before wait---

此处注意,你在 synchronized 中添加的锁对象,必须与你调用 wait 方法的对象一致,否则仍然会出现 IllegalMonitorStateException 异常。简单来说就是你在哪个对象上调用 wait,就应该将这个对象作为锁持有。

那么有人就问了,为啥这么设计,这么设计有什么用,适用于什么场景?

之所以 wait 方法需要在同步方法或是同步代码块中调用(synchronized),是因为 wait 就是释放当前的锁,既然要释放,那么就意味着必须得先得到这个锁。而调用 notifynotifyAll 是将锁交给含有 wait 方法的线程,让其继续执行下去。如果自身没有锁,那么唤醒其他 wait 的线程让其参与锁的竞争就无从谈起了。

在 Java 平台中,每个对象都有一个唯一与之对应的内部锁(Monitor),此外,Java 虚拟机会为每个对象维护两个集合:一个 EntrySet(入口集),一个 WaitSet(等待集)。对于任意对象 obj,其 EntrySet 用于存储等待获取 obj 对应的内部锁的所有线程,WaitSet 用于存储执行了 obj.waitobj.wait(long) 的线程。

对于对象的非同步方法,任意时刻,可以有任意个线程调用该方法。

对于对象的同步方法,只有拥有这个对象的锁,才能调用这个同步方法。如果这个锁被其他线程占用,那么另外一个调用该同步方法的线程就会处于阻塞状态,并进入这个对象的 EntrySet。 若一个已经拥有独占锁的线程调用了该对象 wait 方法,那么该线程会释放独占锁,并加入到 WaitSet

那么为什么线程都持有了这个锁了,明明可以执行相关任务,为什么会调用 wait 释放锁呢,这个线程的后续任务怎么执行呢?对于这种问题,我们可以这样想象这种场景,你占用了一个房间,准备把老师布置的作业写完,可你正写到一半,另一位同学突然进来,说要占用这个房间开个会。此时你就需要释放这个房间,然后等待这个同学开完会再把房间给你,你继续使用。

你占用房间那就是 synchronized(房间),你暂时释放房间就是 房间.wait,此时你进入到 房间的 WaitSet;别人用完了房间通知你,就是 房间.notify,然后你进入到 房间的EntrySet,等竞争到 房间 后继续写作业。

而某个线程调用 notify()notifyAll() 方法,就是将 WaitSet 中的线程转移到 EntrySet,然后让他们竞争锁。

由此可见,无论你是 wait 还是 notify,都是对这个对象的锁的操作,因此你必须先持有这个对象锁,否则就是 IllegalMonitorStateException 异常。

下面来看一个简单的代码:

java 复制代码
public static void main(String[] args) {
        Object obj = new Object();
        Thread thread_1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized(obj) {
                    try{
                        System.out.println("threa_1 before wait... "+Thread.currentThread().getState());
                        obj.wait();
                        System.out.println("threa_1 after wait... "+Thread.currentThread().getState());
                    } catch (IllegalMonitorStateException|InterruptedException exception) {
                        exception.printStackTrace();
                    }
                }
            }
        }, "thread_1");

        Thread thread_2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized(obj) {
                    try{
                        System.out.println("thread_2 sleep 2 seconds...");
                        Thread.sleep(2000);
                        System.out.println("thread_2 notify...");
                        obj.notify();
                    } catch (IllegalMonitorStateException|InterruptedException exception) {
                        exception.printStackTrace();
                    }
                }
            }
        }, "thread_2");

        thread_1.start();
        thread_2.start();
    }

thread_1 执行时调用 wait 方法,两秒后被 thread_2 调用 notify 唤醒,唤醒后 thread_1 继续执行。 输出结果:

ruby 复制代码
mi@mi-HP:~/develop/code/JavaCode$ java Hello.java 
threa_1 before wait... RUNNABLE
thread_2 sleep 2 seconds...
thread_2 notify...
threa_1 after wait... RUNNABLE

这里还有需要注意的一点就是,虽然 thread_1 被唤醒,但是 thread_1 线程并不是能立即执行的。被唤醒只是说明 thread_1objWaitSet 进入到了 EntrySet,此时的线程状态是 BLOCKED,还需要竞争 obj 锁。当得到 obj 锁之后,才能够继续执行。 诸位可以在 thread_2notify 之后加上 sleep 两秒看看效果。

好了,为了讲解 wait 方法,这里扯了一大堆关于线程等待与唤醒的内容,也只有理解了这些内容,才能明白 wait 方法的作用。

那么这里总结一下,wait 方法用于同步代码块中,用于让当前线程等待,进入对象的 WaitSet。其他线程需要调用对象的 notify 方法,使其被唤醒,进入 EntrySet,再竞争对象锁,获取锁之后将继续执行。

2. wait(long) 方法

java 复制代码
public final void wait(long timeoutMillis) throws InterruptedException {
    long comp = Blocker.begin();
    try {
        wait0(timeoutMillis);
    } catch (InterruptedException e) {
        Thread thread = Thread.currentThread();
        if (thread.isVirtual())
            thread.getAndClearInterrupt();
        throw e;
    } finally {
        Blocker.end(comp);
    }
}

wait 方法是无限期等待,必须其他线程调用 notify,而这个带参数的,就是限定了等待时间,超过了这个时间,线程会自己唤醒自己。

另外,通过 wait 的代码可以看到,当参数为 0 时,这个方法其实就是 wait 的无限期等待。而这个方法中,真正让线程进入等待的是 wait0 这个 native 方法:

java 复制代码
private final native void wait0(long timeoutMillis) throws InterruptedException;

3. wait(long, int) 方法

java 复制代码
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++;
    }

    wait(timeoutMillis);
}

该方法与 wait(long timeout) 方法类似,只是多了一个 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。

如果 timeoutnanos 参数都为 0,则不会超时,会一直进行等待,等同于 wait() 方法。

4. notify 方法

java 复制代码
public final native void notify();

这个方法前面说过,是用于唤醒 WaitSet 中的线程,使其进入到 EntrySet 中。但是往后会发现还有一个 notifyAll 的方法,那么这两个方法有什么区别呢?

当你调用 notify 时,只有一个等待线程会被唤醒而且它不能保证哪个线程会被唤醒,这取决于线程调度器。虽然如果你调用 notifyAll 方法,那么等待该锁的所有线程都会被唤醒,但是在执行剩余的代码之前,所有被唤醒的线程都将争夺锁定。简单来说,notify 只会唤醒一个线程,notifyAll 将唤醒所有线程。

5. notifyAll 方法

java 复制代码
public final native void notifyAll();

唤醒所有等待中的线程。

在线程中,生产者和消费者模型是我们常常用以演示线程同步的,下面是一个典型的生产者消费者例子,看懂了这个例子,waitnotify 基本就没什么问题了:

java 复制代码
public class Main {
    private static final Queue<Integer> queue = new LinkedList<>();
    private static final int MAX_SIZE = 5;
    private static final Object lock = new Object();

    public static void main(String[] args) {
        Thread producer = new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    while (queue.size() == MAX_SIZE) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.add(1);
                    lock.notifyAll();
                }
            }
        });

        Thread consumer = new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    while (queue.isEmpty()) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.poll();
                    lock.notifyAll();
                }
            }
        });

        producer.start();
        consumer.start();
    }
}

finalize() 方法

java 复制代码
protected void finalize() throws Throwable { }

最后,我们来说一下 finalize 方法,这个方法虽然被标记废弃,但是之前还是比较常用的。它在对象被 GC 回收之前调用,一般覆写这个方法完成这个对象的清理工作,例如清理相关的 native 资源或是其他资源(socket、文件)的释放。

当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了 finalize 方法,若未覆盖,则直接将其回收。否则,若对象未执行过 finalize 方法,将其移动到一个队列里,由一个低优先级线程执行该队列中对象的 finalize 方法。执行 finalize 方法完毕后,这些对象才成为真正的垃圾,等待下一轮垃圾回收。

以下是一个 finalize 使用例子:

java 复制代码
class FinalizeObj {

    private long nativePointer;

    public FinalizeObj() {
        nativePointer = createNative();
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        releaseNative(nativePointer);
        nativePointer = 0L;
    }

    private native long createNative();
    private native void releaseNative(long nativePointer);
}

这个例子,在构造方法中创建 native 底层资源,在 finalize 方法中释放 native 底层资源。

不过,现在由于 finalize 被标记为废弃,已经不推荐这么写了。至于为什么会被标记为废弃,主要是因为其被执行的不确定性太大,一个对象从不可达到 finalize 方法被执行,完全依赖 JVM。这无法保证此对象被占用的资源被及时回收,甚至都不能保证这个方法被执行。因此要避免使用。 其实如果这个方法真的好用的话,也不会有那么多的类要提供 closedestroy 等方法了。

那么既然这个方法不推荐,那我要释放上面那个例子中的 native 资源,应该怎么做呢?答案是使用 java.lang.ref.Cleaner,这是 Java 9 推出的一个轻量级垃圾回收机制。不过这个类加到文章里来就太长了。

总结

通过这篇文章,大家应该对 Object 里面的那些方法有一些了解,常用的5个方法较为简单。主要是与线程相关的方法,这才是 Object 类的重头戏。好在只要掌握的 waitnotify 方法,其他的就明白了。最后文章讲解了一下 finalize 方法,作为一个被废弃的方法,我们了解了它的使用方法,后续需要用 Cleaner 等方法替代。

相关推荐
Lisonseekpan16 小时前
Spring Boot Email 邮件发送完全指南
java·spring boot·后端·log4j
sheji341617 小时前
【开题答辩全过程】以 基于Springboot的体检中心信息管理系统设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
天河归来17 小时前
本地windows环境升级dify到1.11.1版本
java·spring boot·docker
超级种码17 小时前
Java:JavaAgent技术(java.instrument和java.attach)
java·开发语言·python
天天向上102417 小时前
go 配置热更新
开发语言·后端·golang
甜鲸鱼17 小时前
【Spring AOP】操作日志的完整实现与原理剖析
java·spring boot·spring
狗头大军之江苏分军17 小时前
年底科技大考:2025 中国前端工程师的 AI 辅助工具实战盘点
java·前端·后端
一 乐18 小时前
酒店客房预订|基于springboot + vue酒店客房预订系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端
计算机毕设指导618 小时前
基于Spring Boot的防诈骗管理系统【源码文末联系】
java·spring boot·后端·spring·tomcat·maven·intellij-idea
开心就好202518 小时前
IOScer 开发环境证书包括哪些,证书、描述文件与 App ID 的协同管理实践
后端