查看原文
其他

Thread的join()是如何控制多线程同步的

elsef SpringForAll社区 2021-05-27

点击上方☝SpringForAll社区 轻松关注!

及时获取有趣有料的技术文章

本文来源:http://r6d.cn/ab5nm



elseif

读完需要

15分钟

速读仅需 5 分钟

Thread 的 join()方法的使用一直很让人感到别扭,如果调用 t1.join() 这个方法该怎么理解呢?其实可以理解为「加塞」,让 t1 加塞先执行,当前线程阻塞等待,一直到 t1 执行完毕再继续执行。但是 join() 底层是如何让当前线程阻塞并在执行完毕后让当前线程继续运行的呢?猜测一定是依赖于 t1 对象的对象锁,否则无法在不增加额外锁对象同步控制的前提下使得所有调用 t1.join() 方法的线程都老老实实的听从指挥。本文将通过两个例子把 join 机制和锁同步的关系进行说明。




   

阻塞与等待的区别

  • 阻塞:当一个线程试图获取对象锁(非 java.util .concurrent 库中的锁,即使用 synchronized 关键字),而该锁被其他线程持有时,则该线程进入「阻塞状态」。

它的特点是使用简单,由 JVM 调度器来决定唤醒自己(阻塞的线程),而不需要由另一个线程来显式唤醒自己,故不响应中断,即 synchronized 不支持被中断。

  • 等待:当一个线程等待另一个线程通知调度器一个条件时,该线程进入「等待状态」。

它的特点是需要等待另一个线程显式地唤醒自己,实现灵活,语义更丰富,可响应中断。例如调用:Object.wait() 、Thread.join()以及等待 Lock 或 Condition。

需要强调的是虽然 synchronized 和 JUC 里的 Lock 都实现锁的功能,但线程进入的状态是不一样的。synchronized 会让线程进入阻塞态,而 JUC 里的 Lock 是用 LockSupport.park()/unpark()来实现阻塞/唤醒的,会让线程进入等待态。但话又说回来,虽然等锁时进入的状态不一样,但被唤醒后又都进入 runnable 态,从行为效果来看又是一样的。




   

Thread 类的 join()方法

join()方法是 Thread 中的一个 public 方法,它有几个重载版本:


1 join()2 join(long millis) //参数为毫秒3 join(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒


join()实际是利用了 wait(),只不过它不用等待 notify()/notifyAll(),且不受其影响。它结束的条件是:1)等待时间到;2)目标线程已经 run 完(通过 isAlive()来判断)。

先来看一下 JDK 源码:


/** * Waits at most {@code millis} milliseconds for this thread to * die. A timeout of {@code 0} means to wait forever. * * <p> This implementation uses a loop of {@code this.wait} calls * conditioned on {@code this.isAlive}. As a thread terminates the * {@code this.notifyAll} method is invoked. It is recommended that * applications not use {@code wait}, {@code notify}, or * {@code notifyAll} on {@code Thread} instances. * * @param millis * the time to wait in milliseconds * * @throws IllegalArgumentException * if the value of {@code millis} is negative * * @throws InterruptedException * if any thread has interrupted the current thread. The * <i>interrupted status</i> of the current thread is * cleared when this exception is thrown. */ public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0;
if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); }
// 0则需要一直等到目标线程run完 if (millis == 0) { while (isAlive()) {// 如果被调用join方法的线程是alive状态,则调用join的方法 wait(0); // == this.wait(0),注意这里释放的是「被调用」join方法的线程对象的锁 } } else { // 如果目标线程未run完且阻塞时间未到,那么调用线程会一直等待。 while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } /** * 每次最多等待delay毫秒时间后继续争抢对象锁,获取锁后继续从这里开始的下一行执行,也可能提前被notify() * /notifyAll()唤醒,造成delay未一次性消耗完,会继续执行while继续wait(剩下的delay) */ wait(delay); now = System.currentTimeMillis() - base; // 这个变量now起的不太好,叫elapsedMillis就容易理解了 } } }


这个方法有一些需要注意的地方:

  • 它是 Thread 类中的方法,并不是 Object 类中的,所以只能让一个线程对象执行join()

  • 它是synchronized修饰的方法,即执行该方法之前是需要获取该线程对象的锁的,可能会由于其他线程持有该线程对象的锁而阻塞

  • 它会抛出InterruptedException异常,即执行过程中如果被调用的线程被中断,上层代码可以通过该方法进行捕获,并恢复运行状态

  • 它只会导致调用该方法的线程释放「被调用线程的对象的 this 锁」,除此之外不会释放任何其他锁,比如如果调用代码在一个synchronized块里的话,而锁对象是一个新的对象



   

例子

关于第三点,通过一个具体的例子来模拟一下:


package com.java.test;
/** * 模拟Thread.join()在有锁情况下是否释放锁的例子 */public class HelloJava {
public static void main(String[] args) { String oo = new String(); MyThread t1 = new MyThread("线程t1--", oo, null); MyThread t2 = new MyThread("线程t2--", oo, t1); t2.start(); // 注意是t2先启动 t1.start(); // 注意t1一定要处于启动状态,否则t1.join()方法会直接返回 try { t1.join(); // 等待t1结束后main才会继续执行,进入t1的wait set,t1执行完毕后会主动唤醒它 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end"); }
}
class MyThread extends Thread{ private String name; private Object oo; private Thread t; public MyThread(String name,Object oo, Thread t){ super(name); this.name = name; this.t = t; this.oo = oo; } @Override public void run() { synchronized (oo) { System.out.println("running in " + Thread.currentThread().getName()); if (t != null) { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } for(int i = 0; i < 10; i++){ System.out.println(name + i); } } }}
Output:running in 线程t2--(hanging)


程序一直处于假死状态,hang 住了,用jstack命令看一下进程当前的 stack 信息。main 线程:


"main" #1 prio=5 os_prio=31 tid=0x00007fe965804800 nid=0x2803 in Object.wait() [0x000070000146f000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x000000076b0c86a8> (a com.java.test.MyThread) at java.lang.Thread.join(Thread.java:1252) - locked <0x000000076b0c86a8> (a com.java.test.MyThread) at java.lang.Thread.join(Thread.java:1326) at com.java.test.HelloJava.main(HelloJava.java:15)


首先可以看到主线程的状态是WAITING的, 然后下面是堆栈信息,需要从下往上看,main 线程在 15 行处调用 t1.join(),因为 join 方法是同步的,所以先获取了 t1 的对象锁,也就是 locked <0x000000076b0c86a8> (a com.java.test.MyThread)部分,最后在at java.lang.Thread.join(Thread .java:1252)处执行了 t1.wait(0)方法,导致主线程又释放了 t1 对象锁,所以最后就变成了waiting on <0x000000076b0c86a8> (a com.java .test .MyThread)。

t2 线程:


"Thread-1" #12 prio=5 os_prio=31 tid=0x00007fe968812800 nid=0xa903 in Object.wait() [0x00007000028ab000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x000000076b0c86a8> (a com.java.test.MyThread) at java.lang.Thread.join(Thread.java:1252) - locked <0x000000076b0c86a8> (a com.java.test.MyThread) at java.lang.Thread.join(Thread.java:1326) at com.java.test.MyThread.run(HelloJava.java:42) - locked <0x000000076b0c8678> (a java.lang.String)


同样 t2 线程的状态是WAITING的,从下往上分析堆栈,t2 先在 42 行代码处获取了locked <0x000000076b0c8678> (a java.lang.String)锁,然后它持有了 t1 的对象锁locked <0x000000076b0c86a8> (a com.java.test.MyThread),进入 t1.join()同步方法后,在at java.lang.Thread.join(Thread.java:1252)处执行了 t1.wait(0) 方法,导致又释放了 t1 对象锁,所以最后就变成了waiting on <0x000000076b0c86a8> (a com.java.test .MyThread),t2 和 main 线程一样,最后都在等待获取 t1 的对象锁。

t1 对应的线程:


"Thread-0" #11 prio=5 os_prio=31 tid=0x00007fe968849800 nid=0xa803 waiting for monitor entry [0x00007000029ae000] java.lang.Thread.State: BLOCKED (on object monitor) at com.java.test.MyThread.run(HelloJava.java:39) - waiting to lock <0x000000076b0c8678> (a java.lang.String)


t1 的线程状态是BLOCKED,被阻塞了,我们知道被阻塞一般都是没有获取到锁,这里是waiting to lock <0x000000076b0c8678> (a java .lang.String),因为该 String 对象锁此时被 t2 持有,并且 t2 进入了 WAITING 状态只能等待被唤醒。注意:wait()方法 虽然会释放锁,但是释放的是调用对象obj.wait()的对象 obj 锁,在这里即是释放的 t1 对象的锁,而不是 String 对象的锁。

关于join()中的wait(0)这里再解释一下,很多人都搞不清楚到底是什么锁,以及谁释放了这个锁。很简单 wait(0)是在 Thread 类中的,所以是 Thread 对象的锁,其次 t1.join ()也就意味着 Thread 对象是 t1,但注意这里仅仅是 t1 的对象锁,跟 t1 执行线程没关系。最后,是谁释放了 t1 上的锁呢?当然是执行代码的线程了,那么是谁执行的 t1.join()呢,很明显是 main 线程和 t2 线程,所以这两个线程会处于WAITING状态,进入了 t1 对象锁的wait set集合。




   

思考题

再给一个例子,让大家自己分析。这个例子的不同在于无论是join()还是外层的synchronized都是用的一个对象锁——线程对象的锁。


class A extends Thread { static A a; public void run() { try { synchronized(a) { System.out.println(Thread.currentThread().getName()+" acquired a lock on a"); Thread.sleep(1000); } } catch (InterruptedException e){ } } public static void main(String[] ar) throws Exception { a=new A(); a.start(); synchronized(a) { System.out.println(Thread.currentThread().getName()+" acquired a lock on a"); a.join(); System.out.println(Thread.holdsLock(a)); } }} Output: main acquired a lock on aThread-1 acquired a lock on atrue




   

References

  • https://coderanch.com/t/242419/certification/invocation-join-release-locks-objects ( https://coderanch.com/t/242419/certification/invocation-join-release-locks-objects )

  • https://segmentfault.com/q/1010000007260477 ( https://segmentfault.com/q/1010000007260477 )



2021Java深入资料领取方式回复“20210112”

墙裂推荐

【深度】互联网技术人的社群,点击了解!





 ElasticSearch聚合实战+优化

 合格的后端Coder都应该写好UT和Mock测试

 Java8中Stream原理分析

 Rate Limiter深度剖析



关注公众号,回复“spring”有惊喜!!!

如果资源对你有帮助的话


❤️给个在看,是最大的支持❤️

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存