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

news2024/11/25 0:45:05

Object 概述

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

Object 类位于 java.base 模块下 java.lang.Object,其结构如下:
Object 中的那些方法
其中 wait0 是私有方法,不用管。我把这些方法分为两类,一类是常用方法(如 hashCodeequals );一类是线程相关方法(如 waitnotify)。
下面就对这些方法一一说明,进而全面掌握这个类的所有方法。

常用方法

1. getClass() 方法

public final native Class<?> getClass();

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

2. hashCode() 方法

public native int hashCode();

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

3. equals(Object) 方法

public boolean equals(Object obj) {
        return (this == obj);
    }

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

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

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

@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() 方法

protected native Object clone() throws CloneNotSupportedException;

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

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

5. toString() 方法

public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

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

线程相关方法

1. wait() 方法

public final void wait() throws InterruptedException {
        wait(0L);
    }

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

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

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

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---");
    }
}

异常信息:

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()方法,这个锁是同步代码块,也可以是同步方法。上面说的线程不是持有者,其实就是这个锁的持有者。下面我们更改一下代码:

    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—” 后等待在那里,线程进入阻塞状态:

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 异常。

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

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 继续执行。
输出结果:

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) 方法

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 方法:

private final native void wait0(long timeoutMillis) throws InterruptedException;

3. wait(long, int) 方法

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 方法

public final native void notify();

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

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

5. notifyAll 方法

public final native void notifyAll();

唤醒所有等待中的线程。

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

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() 方法

protected void finalize() throws Throwable { }

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

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

以下是一个 finalize 使用例子:

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 等方法替代。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1876224.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python系列30:各种爬虫技术总结

1. 使用requests获取网页内容 以巴鲁夫产品为例&#xff0c;可以用get请求获取内容&#xff1a; https://www.balluff.com.cn/zh-cn/products/BES02YF 对应的网页为&#xff1a; 使用简单方法进行解析即可 import requests r BES02YF res requests.get("https://www.…

Apache IoTDB 监控详解 | 分布式系统监控基础

IoTDB 分布式系统监控的基础“须知”&#xff01; 我这个环境的系统性能一直无法提升&#xff0c;能否帮我找到系统的瓶颈在哪里&#xff1f; 系统优化后&#xff0c;虽然写入性能有所提升&#xff0c;但查询延迟却增加了&#xff0c;下一步我该如何排查和优化呢&#xff1f; 请…

办公软件的答案?ONLYOFFICE 桌面应用编辑器会是最好用的 Office 软件?ONLYOFFICE 桌面编辑器使用初体验

文章目录 &#x1f4cb;前言&#x1f3af;什么是 ONLYOFFICE&#x1f3af; 主要功能介绍及 8.1 新功能体验&#x1f3af; 在线体验&#x1f4dd;最后 &#x1f4cb;前言 提到办公软件&#xff0c;大家最常用的可能就是微软的 Microsoft Office 和国产的 WPS Office。这两款软件…

IBCS 虚拟专线用哪些特点!

当今数字化时代&#xff0c;高效、稳定、安全的网络连接对于企业和个人来说至关重要。IBCS 虚拟专线作为一种创新的网络解决方案&#xff0c;凭借其众多显著的优势&#xff0c;正逐渐成为众多用户的首选。 IBCS 虚拟专线最突出的优势之一在于其卓越的网络性能。它通过优化网络路…

Go 语言切片遍历地址会发生改变吗?

引言&#xff1a;今天面试的时候&#xff0c;面试官问了一道学 Go 语言的同学都会的简单代码&#xff0c;是关于 Go 语言 for 循环问题的&#xff0c;他询问了一个点&#xff0c;循环中共享变量的地址会发生改变吗&#xff1f; 相信听到这个问题的你&#xff0c;第一反应肯定是…

你还能顶几天?

A总&#xff1a;你还能顶几天&#xff1f; 汪汪队&#xff1a;顶到奉命撤退的那一天 A总&#xff1a;你在这守散钱点几十年了&#xff0c;从来没跟我提过任何的要求&#xff0c;难道你不困难吗&#xff1f; 汪汪队&#xff1a;有困难&#xff0c;但是我提了有什么用呢&#…

Java异常处理详解【入门篇】

Java异常处理详解【入门篇】 Java异常处理详解1. 异常的概念2. 异常的分类2.1 检查异常&#xff08;Checked Exception&#xff09;2.2 非检查异常&#xff08;Unchecked Exception&#xff09;2.3 错误&#xff08;Error&#xff09; 3. 异常处理机制3.1 try-catch3.2 finally…

kubuadm 方式部署 k8s 集群

准备三台机器 主机名IP地址CPU/内存角色K8S版本Docker版本k8s231192.168.99.2312C4Gmaster1.23.1720.10.24k8s232192.168.99.2322C4Gwoker1.23.1720.10.24k8s233192.168.99.2332C4Gwoker1.23.1720.10.24 需要在K8S集群各节点上面安装docker&#xff0c;如未安装则参考 …

如何3分钟上手传得神乎其神的AI绘画!一篇文章带你搞懂!

前言 今年 AI 绘画绝对是大火的概念之一&#xff0c;这次加入了生财 AI 绘画小航海的船&#xff0c;今天是体验的第1天&#xff0c;那么 AI 绘画是什么呢&#xff1f; 简单来说就是利用 AI 实现绘画&#xff0c;在特定的软件或者程序中&#xff0c;输入一定的关键词或者指令&…

springboot系列八: springboot静态资源访问,Rest风格请求处理, 接收参数相关注解

文章目录 WEB开发-静态资源访问官方文档基本介绍快速入门注意事项和细节 Rest风格请求处理基本介绍应用实例注意事项和细节思考题 接收参数相关注解基本介绍应用实例PathVariableRequestHeaderRequestParamCookieValueRequestBodyRequestAttributeSessionAttribute ⬅️ 上一篇…

数据结构复习指南

数据结构复习指南 本文中列举了数据结构期末考试可能存在的考点 绪论 数据的基本单位 数据元素是数据的基本单位 数据项 数据项是组成数据的、有独立含义的、不可分割的最小单位。 数据对象 数据对象是性质相同的数据元素的集合&#xff0c;是数据的一个子集。 数据结…

【DevExpress】WPF DevExpressMVVM 24.1版本开发指南

DevExpressMVVM WPF 环境安装 前言重要Bug&#xff08;必看&#xff09;环境安装控件目录Theme 主题LoginWindow 登陆窗口INavigationService 导航服务DockLayout Dock类型的画面布局TreeView 树状列表注意引用类型的时候ImageSource是PresentationCore程序集的博主找了好久&am…

【python】OpenCV—QR Code

文章目录 1 QR Code2 准备工作3 生成 QR 码4 读取 QR 码5 与 Zbar 比较 1 QR Code QR Code&#xff08;Quick Response Code&#xff09;是一种二维条码&#xff0c;由日本Denso-Wave公司于1994年发明。QR Code的主要特点是存储信息量大、编码范围广、容错能力强、识读速度快&…

如何让ubuntu开机自动启动DHCP呢?

在Ubuntu系统中&#xff0c;确保DHCP服务&#xff08;通常是ISC DHCP服务&#xff09;在系统启动时自动运行&#xff0c;需要进行以下几步操作&#xff1a; ### 1. 确保DHCP服务已安装 如果你还没有安装DHCP服务&#xff0c;可以使用下面的命令进行安装&#xff1a; bash sudo…

TCP:TCP连接的建立与终止

TCP连接的建立与终止 建立连接第一次握手第二次握手第三次握手 终止连接第一次挥手第二次挥手第三次挥手TIME_WAIT 状态 第四次挥手 连接建立超时 T C P是一个面向连接的协议。无论哪一方向另一方发送数据之前&#xff0c;都必须先在双方之间建立一条连接。本文将详细讨论一个T…

文心一言用户达3亿!文心大模型4.0 Turbo发布,支持API,真GPT-4 Turbo国产来了!

文心一言用户规模达到3亿了&#xff01; 这是笔者在今天的百度Wave Summit 2024大会上的看到的数字。需要强调的是&#xff0c;文心一言的用户规模是在去年12月破亿的。这意味着&#xff0c;仅仅隔了6个月&#xff0c;文心一言用户数量在亿这个级别的数字上竟然直接翻了三倍。…

2024最出色的代理软件评估及推荐

随着网络技术的飞速发展&#xff0c;代理软件已成为许多网络活动不可或缺的工具&#xff0c;特别是在数据抓取、网络安全防护等方面。在众多代理软件中&#xff0c;哪些能真正满足用户需求&#xff0c;提供卓越的性能和服务呢&#xff1f;我们的测评团队经过深入研究和测试&…

Reid系列论文学习——无人机场景下基于 Transformer 的轻量化行人重识别

今天介绍的一篇论文是针对无人机场景下的行人重识别&#xff0c;论文题目为&#xff1a;"无人机场景下基于 Transformer 的轻量化行人重识别"。该论文针对无人机场景下行人呈现多角度多尺度的特点、以及传统CNN网络在行人重识别任务中受限于感受野和下采样导致的无法…

Go-知识测试-单元测试

Go-知识测试-单元测试 1. 定义2. 使用3. testing.common 测试基础数据4. testing.TB 接口5. 单元测试的原理5.1 context 单元测试的调度5.1.1 等待并发执行 testContext.waitParallel5.1.2 并发测试结束 testContext.release 5.2 测试执行 tRunner5.3 启动测试 Run5.4 启动并发…

《晨集》开源软件平台的创新与发展

一、引言 在数字化浪潮的推动下&#xff0c;开源软件平台已成为推动软件创新、促进知识共享的重要力量。《晨集》作为新兴的开源软件平台&#xff0c;其上线标志着开源生态圈的又一重要里程碑。本文旨在探讨《晨集》开源软件平台的创新特点、对开发者社区的影响以及未来发展趋…