资讯专栏INFORMATION COLUMN

从Thread.join说开去

incredible / 2711人阅读

摘要:在进入状态前,线程会将持有的锁先释放掉。被唤醒后的线程,拿不到锁的线程将进入状态,直到它们拿到锁为止。在等待之后测试条件,如果条件不成立的话继续等待,这对于确保安全性是必要的。

WAITING(TIMED_WAITING) 与 BLOCKED

看《Java特种兵》的时候发现,Thread.join可以使线程进入WAITING状态,再结合姊妹篇线程的状态我们可以了解到,有两个类状态非常接近:WAITING(TIMED_WAITING) 与 BLOCKED,这两者都会让线程看上去“阻塞”在某处了。

什么时候线程会进入WAITING(无限期等待)的状态中呢?常用的有两个,分别是①Object.wait without timeout,②Thread.join without timeout【另外还有③LockSupport的park方法,④Conditon的await方法】;TIMED_WAITING除了①Object.wait with timeout、②Thread.join with timeout,还需要添加一条③Thread.sleep方法【另外还有④LockSupport的parkNanos方法,带有时间】。

在进入WAITING状态前,线程会将持有的锁先释放掉。WAITING状态中的线程需要被其他线程对同一个对象调用notify()或notifyAll()方法才能唤醒。被notifyAll()唤醒后的线程,拿不到锁的线程将进入BLOCKED状态,直到它们拿到锁为止。简而言之,WAITING类状态中的线程和BLOCKED状态中的线程的区别在于:WAITING状态的线程需要被其他线程唤醒;BLOCKED中的线程,需要等待其他线程释放锁,此处的锁特指synchronized块。

见下图

Join为什么会使线程进入WAITING(TIMED_WAITING) 状态中呢?我们看一下底层代码

    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");
        }
​
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

从12、20行中可以看出,join方法底层调用的就是wait方法。

sleep与wait

同样会使线程进入TIMED_WAITING状态中的sleep和wait方法,它们之间有什么区别呢?

wait()

wait()方法释放锁

wait()是Object类的方法

wait()是非静态方法

public final void wait() throws InterruptedException { //...}

wait()应该被notify()或者notifyAll()方法唤醒

wait()方法需要在循环中调用(推荐不强制)

waint()方法必须放在synchronized上下文(synchronized方法或代码块)中,不然会抛出IllegalMonitorStateException

sleep()

sleep()方法不会释放锁

sleep()是java.lang.Thread类的方法

sleep()是静态方法

public static void sleep(long millis, int nanos) throws InterruptedException { //... }

sleep()方法是在特定时间后结束

sleep()方法最好不在放在循环中

sleep()方法可以在任意地方执行

wait为什么要放在循环中?
synchronized(obj) {
    while (!condition) { 
      obj.wait(); 
    }
}
这里引用一段《Effective Java》
始终应该使用wait循环模式来调用wait方法;永远不要在循环之外调用wait方法。循环会在等待之前和之后测试条件。
在等待之前测试条件,当条件已经成立时就跳过等待,这对于确保活性(liveness)是必要的。如果条件已经成立,并且在线程等待之前,notify (或者notifyAll)方法已经被调用, 则无法保证该线程将会从等待中苏醒过来。
在等待之后测试条件,如果条件不成立的话继续等待,这对于确保安全性(safety)是必要的。当条件不成立的时候,如果线程继续执行,则可能会破坏被锁保护的约束关系。当条件不成立时,有下面一些理由可使一个线程苏醒过来:
- 另一个线程可能已经得到了锁,并且从一个线程调用notify那一刻起,到等待线程苏醒过来的这段时间中,得到锁的线程已经改变了受保护的状态。 
- 条件并不成立,但是另一个线程可能意外地或恶意地调用了 notify。在公有可访问的对象上等待,这些类实际上把自己暴露在了这种危险的境地中。公有可访问对象的同步方法中包含的wait都会出现这样知问题。
- 通知线程(notifying thread)在唤醒等待线程时可能会过度“大方”。例如,即使只有某一些等待线程的条件已经被满足,但是通知线程可能仍然调用notifyAll。 
- 在没有通知的情况下,等待线程也可能(但很少)会苏醒过来。这被称为“伪唤醒 (spurious wakeup)”

我们针对【跳过等待】和【继续等待】举个形象的例子:

针对【前置判断,跳过等待】:如果是两个狙击手,在同时等待一个人(锁),判断条件是这个人还活着。如果没有前置的判断,在等待前不校验这个人是否活着,那么当狙击手甲杀死目标并通知狙击手乙之后,乙才进入等待状态,那么乙将会死等一个已经被杀死的目标,乙将失去活性(liveness)。

针对【后置判断,继续等待】:还是两个狙击手,如果他们被唤醒后,没有后置校验,那么将导致可笑的错误,比如狙击手甲已经将目标杀死了,狙击手乙被唤醒后,没有再校验条件,直接开枪杀人,将会杀死目标两次。如果是幂等的还则罢了,不幂等的将导致错误。

综上所述,wait前后都要校验,而最好的办法就是循环。

Thread.join后谁在WAITING?

从上文中我们已经知道了,join可以使线程处于WAITING状态,那问题来了,是子线程处于WAITING状态还是父线程处于WAITING状态?我们做个小试验:

public class TestJoin {
    public static void main(String[] args) throws InterruptedException {
        Thread.currentThread().setName("TestJoin main....");
        Thread joinThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (; ; ) {
​
                }
            }
        }, "join thread");
​
        joinThread.start();
        joinThread.join();
    }
}

按照在线程的状态中提供的方法,我们可以得到:

子线程即join的线程依旧是RUNNABLE状态

"join thread" #10 prio=5 os_prio=31 tid=0x00007fca1b801000 nid=0x5503 runnable [0x0000700001725000]

java.lang.Thread.State: RUNNABLE

    at com.meituan.java8.thread.TestJoin$1.run(TestJoin.java:13)

    at java.lang.Thread.run(Thread.java:748)

Locked ownable synchronizers:

    - None

父线程(在此例中是主线程)为WAITING状态

"TestJoin main...." #1 prio=5 os_prio=31 tid=0x00007fca1c003000 nid=0x1903 in Object.wait() [0x00007000003ec000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076ad94fa0> (a java.lang.Thread)
        at java.lang.Thread.join(Thread.java:1252)
        - locked <0x000000076ad94fa0> (a java.lang.Thread)
        at java.lang.Thread.join(Thread.java:1326)
        at com.meituan.java8.thread.TestJoin.main(TestJoin.java:20)
   Locked ownable synchronizers:
        - None

我们对代码做稍稍改动,可以验证sleep后的线程在什么状态

        Thread joinThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.MINUTES.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "sleeping thread");
"sleeping thread" #10 prio=5 os_prio=31 tid=0x00007f92620bc000 nid=0x5503 waiting on condition [0x00007000077b7000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at java.lang.Thread.sleep(Thread.java:340)
        at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
        at com.meituan.java8.thread.TestJoin$1.run(TestJoin.java:16)
        at java.lang.Thread.run(Thread.java:748)
   Locked ownable synchronizers:
        - None
参考文档

https://javaconceptoftheday.c...

A thread enters into WAITING state when it calls wait() or join() method on an object. Before entering into WAITING state, thread releases the lock of the object it holds. It will remain in WAITING state until any other thread calls either notify() or notifyAll() on the same object.
Once the other thread calls notify() or notifyAll() on the same object, one or all the threads which are WAITING for lock of that object will be notified. All the notified threads will not get the object lock immediately. They will get the object lock on a priority basis once the current thread releases the lock. Until that they will be in BLOCKED state.
In simple terms, a thread will be in WAITING state if it is waiting for notification from other threads. A thread will be in BLOCKED state if it is waiting for other thread to release the lock it wants.

https://stackoverflow.com/que...

The difference is relatively simple.
In the BLOCKED state, a thread is about to enter a synchronized block, but there is another thread currently running inside a synchronized block on the same object. The first thread must then wait for the second thread to exit its block.
In the WAITING state, a thread is waiting for a signal from another thread. This happens typically by calling Object.wait(), or Thread.join(). The thread will then remain in this state until another thread calls Object.notify(), or dies.

https://stackoverflow.com/a/3...

wait()
wait() method releases the lock.
wait() is the method of Object class.
wait() is the non-static method - public final void wait() throws InterruptedException { //...}
wait() should be notified by notify() or notifyAll() methods.
wait() method needs to be called from a loop in order to deal with false alarm.
wait() method must be called from synchronized context (i.e. synchronized method or block), otherwise it will throw IllegalMonitorStateException
sleep()
sleep() method doesn"t release the lock.
sleep() is the method of java.lang.Thread class.
sleep() is the static method - public static void sleep(long millis, int nanos) throws InterruptedException { //... }
after the specified amount of time, sleep() is completed.
sleep() better not to call from loop(i.e. see code below).
sleep() may be called from anywhere. there is no specific requirement.

4.《Effective Java》第10章

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73928.html

相关文章

  • 两段简单的程序开去

    摘要:有这么两段小程序。毫无疑问,把这两段小程序贴到浏览器里跑一下,能很快地得到答案。而在第二段小程序中,由于在中,表达式的值在运行之前将会被转化,将会把表达式和声明提升到当前作用域的顶部。两段小程序,考考作用域和变量声明提升,怎么样,答对了么 有这么两段小程序。 var goo = hello; function foo(){ if(true){ goo = world; ...

    young.li 评论0 收藏0
  • python综合学习一之多线程

    摘要:如下面的例子,在学习线程时,将文件名命名为脚本完全正常没问题,结果报下面的错误。最大的问题就是的多线程程序并不能利用多核的优势比如一个使用了多个线程的计算密集型程序只会在一个单上面运行。 本文记录学习Python遇到的问题和一些常用用法,注本开发环境的Python版本为2.7。 一、python文件命名 在python文件命名时,一定要注意不能和系统默认的模块名冲突,否则会报错。如下面...

    cjie 评论0 收藏0
  • Thread类源码解读(2)——线程状态及常用方法

    摘要:如果线程还存活,线程就无限期等待,并让出监视器锁,进入状态。当线程从状态被唤醒后通过,或者是假唤醒将继续竞争监视器锁,当成功获得监视器锁后,他将从调用的地方恢复,继续运行。 前言 系列文章目录 上一篇我们讨论了线程的创建,本篇我们来聊一聊线程的状态转换以及常用的几个比较重要的方法。 本篇依然是通过源码分析来了解这些知识。 本文源码基于jdk1.8 。 阅读完本文,你应当有能力回答以...

    luqiuwen 评论0 收藏0
  • python并发4:使用thread处理并发

    摘要:如果某线程并未使用很多操作,它会在自己的时间片内一直占用处理器和。在中使用线程在和等大多数类系统上运行时,支持多线程编程。守护线程另一个避免使用模块的原因是,它不支持守护线程。 这一篇是Python并发的第四篇,主要介绍进程和线程的定义,Python线程和全局解释器锁以及Python如何使用thread模块处理并发 引言&动机 考虑一下这个场景,我们有10000条数据需要处理,处理每条...

    joywek 评论0 收藏0

发表评论

0条评论

incredible

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<