大家好,我是小米!今天来跟大家聊一个常见但很容易让人迷惑的Java面试题。面试官有时会问:为什么线程通信的 wait()、notify() 和 notifyAll() 方法被定义在 Object 类里?它们为什么必须在同步方法或同步块中被调用? 这两个问题在很多社招面试中都会出现,尤其是对于一些经验较为丰富的开发者来说,理解这些问题不仅能够帮助我们更好地掌握线程间通信,也能更深入理解Java的内存模型和多线程的精髓。接下来,让我们通过一个故事来深入探讨这些问题。 线程通信的故事:小明和小红的任务 想象一下,假设小明和小红是两个程序员,他们在同一个公司里工作。今天,公司交给了他们一个任务:他们需要交替处理一个共享资源(比如说一个文件),但是只能有一个人同时处理这个文件。于是,他们决定采用线程同步的方式来解决这个问题。 为了确保只有一个人能够处理文件,他们用到了“互斥锁”的概念:每当一个人正在处理文件时,另一个人就必须等待。于是小明和小红的工作模式如下: 小明:如果他正在处理文件,他就通知小红“你可以开始了”。 小红:如果她正在处理文件,她就通知小明“你可以开始了”。 为了让工作能够按计划顺利进行,小明和小红商定了一些规则。这里的“等待”和“通知”机制是非常重要的,保证了两个人的任务交替进行。 但是,问题来了!他们怎么才能保证正确的同步呢? Java 中的 wait()、notify() 和 notifyAll() 方法 在 Java 中,线程通信就是让一个线程等待,直到其他线程通知它可以继续执行。为了解决小明和小红的问题,Java 提供了 wait()、notify() 和 notifyAll() 这三个方法。 首先,wait() 方法使当前线程进入等待状态,直到被其他线程唤醒。调用 notify() 方法会唤醒一个正在等待的线程,而 notifyAll() 会唤醒所有等待的线程。 这些方法的目的非常简单:让线程间可以协调工作,避免了资源争抢,确保了任务按照预期顺序进行。 为什么 wait()、notify() 和 notifyAll() 被定义在 Object 类中? 回到小明和小红的工作场景,如果我们要让他们的线程能“互相通知”,我们需要确保每个线程都能够“沟通”或者说共享同一个对象。 在 Java 中,每个对象都可以作为“锁”来进行线程同步。而 wait()、notify() 和 notifyAll() 这些方法必须与“锁”的概念紧密结合,因此它们被定义在 Object 类中,因为每个 Java 对象都拥有这个锁,而这些方法正是利用了对象锁来进行线程通信。 想象一下,wait() 就像是小明说的“我还在处理文件,等一下”,notify() 就是小红说的“好了,可以开始了”,它们相互“通知”对方,这种通信方式需要“共用一个资源”才能协调。所以它们是作为 Object 类的一部分,这样所有对象都可以通过 wait() 和 notify() 机制来实现线程间的协作。 wait()、notify() 和 notifyAll() 为什么必须在同步方法或者同步块中被调用? 接下来,我们再来看为什么 wait()、notify() 和 notifyAll() 必须在同步方法或者同步块中被调用。 我们知道,在 Java 中,线程是通过“锁”来实现同步的。每当一个线程持有一个对象的锁时,其他线程就无法访问该对象。因此,如果一个线程需要调用 wait() 或者 notify(),它就必须持有这个对象的锁,才能保证线程的正确协调。 如果我们不在同步方法或同步块中调用这些方法,会发生什么呢? 不加锁的情况下调用 wait() 或 notify() 方法,可能会导致线程之间的协调失效,甚至可能引发程序的死锁。 小明和小红的例子也可以帮助我们理解:如果没有锁的保护,他们可能会在没有同步的情况下“互相通知”,这就像是两个没有时间顺序的人在交换信息,可能导致逻辑上的混乱。因此,Java 强制要求在同步方法或者同步块中使用 wait()、notify() 和 notifyAll() 方法,以确保线程通信在恰当的时机发生。 如何使用同步方法和同步块? 同步方法和同步块都依赖于对象的锁。我们可以通过 synchronized 关键字来标识同步代码块或方法。 1. 同步方法 同步方法是将整个方法体都加上了同步锁。比如:
在这个例子中,processFile 方法是一个同步方法,当小明或小红在处理文件时,其他线程必须等待。通过 wait() 方法让当前线程进入等待状态,直到被另一个线程调用 notify() 唤醒。 2. 同步块 同步块是将代码块加上同步锁,它可以在方法内部实现局部同步:
这里我们使用 synchronized (this) 来加锁 processFile() 方法中的代码块,这样可以更精细地控制同步,确保 wait() 和 notify() 的正确使用。 wait() 和 notify() 的执行时机和规则 了解了同步的基本概念之后,我们还需要掌握一些细节: wait() 调用时,必须持有对象的锁。当调用 wait() 时,当前线程会释放锁并进入阻塞状态,直到有其他线程调用 notify() 或 notifyAll() 来唤醒它。 notify() 唤醒一个等待中的线程,如果有多个线程在等待,notify() 只会唤醒其中一个。notifyAll() 则会唤醒所有等待的线程。 唤醒的线程会竞争重新获得锁,被唤醒的线程必须重新获得锁才能继续执行。这里的关键点在于,notify() 或 notifyAll() 只唤醒线程,但它们不会直接使线程进入执行状态,线程仍然需要等待锁的释放。 总结 通过小明和小红的工作场景,我们理解了 wait()、notify() 和 notifyAll() 这些方法背后的核心思想——线程之间的协调与通信。它们之所以被定义在 Object 类中,是因为每个对象都有锁,而线程通信是基于这些锁的。至于为什么必须在同步方法或同步块中调用它们,原因在于线程的通信需要确保在持有锁的情况下进行,否则就会导致线程的协调失败,甚至可能产生死锁等问题。 掌握了这些,你就可以轻松应对面试中的多线程相关问题啦!如果你对线程同步、线程池等其他多线程问题有兴趣,也可以随时来找我聊聊哦! END 以上就是今天的分享,希望能帮助你在面试中脱颖而出!如果你喜欢这篇文章,记得点赞、收藏、转发,和你的朋友们一起分享吧!我们下期见! 我是小米,一个喜欢分享技术的29岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!