首页 文章详情

挑错 |《Java 并发编程的艺术》中关于线程状态的三处错误

低并发编程 | 524 2021-03-25 21:07 0 0 0
UniSMS (合一短信)


低并发编程
战略上藐视技术,战术上重视技术

本文建议在阅读完《Java 线程的状态及转换》,或已对本知识点有了解之后,再食用。

上周写了一篇《Java 线程的状态及转换》,顺便发现了《并发编程的艺术》这本书中,关于线程状态及转换的三处错误,或者说问题吧。
下图来自本书第四章,4.1.4 线程的状态,这一节。
网上的很多关于线程状态的文章,好多都来自于这张图。
不废话,直接说我发现的错误。
 
1
 
这是个很明显的错误了,应该是个笔误。
join() 是 Thread 类的方法,不是 Object 类的。
而且准确说是 Thread 类的成员方法。
public final void join() throws InterruptedException {
    join(0);
}

public final synchronized void join(long millis)
throws InterruptedException 
{
    ...
    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    }
    ...
}
这个笔误还是挺严重的,我当时就一度怀疑是我记错了。
但我又想,join() 是让一个线程插队进来,直到这个插队线程运行结束,原线程才继续往下运行。
public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(...);
    t.start();
    t.join();
    System.out.println("此处 t 线程结束后才能输出");
}
那如果是 Object 的无参方法,那根本没有地方能体现是让哪个线程插队进来呀。
最后果然发现,它写错了。
 
2
 
这个不算错误吧,我觉得不太严谨,仍然和刚刚的 join 有关。
刚刚 join 的源码我们也看了,我再简化一下。
// Thread.java
// 无参的 join 有用的信息就这些,省略了额外分支
public synchronized void join() {
  while (isAlive()) {
      wait();
  }
}
也就是说,他的本质任然是执行了 wait() 方法,而锁对象就是 Thread t 对象本身。
主线程调用了 wait ,需要另一个线程 notify 才行,也就是为了达到 join 的效果,这个线程 t 必须在结束后调用一下 t.notifyAll()。
只不过,这部分是由虚拟机帮我们完成的。
我们看 JVM 源码
hotspot/src/share/vm/runtime/thread.cpp
void JavaThread::exit(...) { ... ensure_join(this); ...}
static void ensure_join(JavaThread* thread) { ... lock.notify_all(thread); ...}
我们看到,虚拟机在一个线程的方法执行完毕后,执行了个 ensure_join 方法,看名字就知道是专门为 join 而设计的。
而继续跟进会发现一段关键代码,lock.notify_all,这便是一个线程结束后,会自动调用自己的 notifyAll 方法的证明。
所以
WAITING 到 RUNNING 的转换,我觉得需要加上这样一种场景,因为它毕竟和显示调用 Object.notifyAll() 不同嘛。
同时也是个重要的知识点,不然你会对 join 的原理很迷惑的。
 
3
 
我们看这一部分
RUNNABLE 到 BLOCKED 转换,该图写了两点,其实就是一点,进入 synchronized 区。
因此我认为网上好多文章有这么一句话

线程状态从 RUNNABLE 变为 BLOCKED,当且仅当进入 synchronized 方法或 synchronized 块。

这句话的自信应该就来自于这张图,或搬运这张图的网络文章。
其实只要翻看一下 jdk 文档就知道了。
/**
 * A thread in the blocked state is waiting for a monitor lock
 * to enter a synchronized block/method or
 * reenter a synchronized block/method after calling
 * {@link Object#wait() Object.wait}.
 */

BLOCKED,
翻译一下就是
/**
 * 在如下场景下等待一个锁(获取锁失败)
 * 1. 进入 synchronized 方法
 * 2. 进入 synchronized 块
 * 3. 调用 wait 后(被 notify)重新进入 synchronized 方法/块
 */

BLOCKED,
注释第三点清清楚楚写了。

当一个阻塞在 wait 的线程,被另一个线程 notify 后,重新进入 synchronized 区域,此时需要重新获取锁,如果失败了,就变成 BLOCKED 状态。

这也是个很重要的知识点,如果没写这个的话,估计很多人会认为,wait 被 notify 后就直接可以等待 CPU 分配时间片往下运行了。
当然这个知识点,过于强调时,还有另外一个普遍犯的错误,也是好多技术文章写出来的一段话。

wait 后线程会进入该对象的等待队列,线程状态变为 WAITING。

当被另一个线程执行 notify 时,需要重新竞争锁,如果获取不到,就会进入该对象的同步队列,线程状态变为 BLOCKED。

因此会得出如下的转换流程。

WAITING -- BLOCKED

这是不对的。
因 wait 阻塞在 WAITING 状态的线程,被 notify 后,会先转换为 RUNNABLE,等待 CPU 时间片分配。
当有机会真正运行时,才会去尝试抢锁,此时如果抢锁成功,直接就运行了。
如果抢锁失败,再从 RUNNABLE 变为 BLOCKED。
所以是有个过程的,并不能直接从 WAITING 变为 BLOCKED。
 
总结
 
所以,最终我画的图是这样的。
通过本篇文章,我希望大家能明白一点。
现在网上的博客鱼龙混杂,一定要有自己的判断,并带着怀疑的态度去学习。
《Java 并发编程的艺术》这么经典的书,应该是很多人必读的 Java 经典书籍吧,它仅在线程状态这张图中,都有这么多问题,更何况网上的博客呢。
据我观察,好多文章都引自这张图,更有甚者在这张图的基础之上,还缺斤少两,或者有其他错误。
所以我也一直强调一手资料的重要性。
大家千万不要害怕一手资料,觉得离自己太远。
比如今天的勘误,我就是简简单单打开 jdk 源码中的 Thread 类。
仅仅几行注释,就清清楚楚写明了状态及转换。又简单,可信度又高,还就在你的身边,多棒的资料啊。
我自己写文章,也尽量每一个点都是经过我的验证,或者在一手资料中找到证据。这也使得我每写一篇文章,不论对读者还是对我自己,帮助都很大。
大家也监督我,随时提出文章中的错误,我们共同进步。

good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter