上一篇我们讨论了线程的定义的創建本篇我们来聊一聊线程的定义的状态转换以及常用的几个比较重要的方法。
本篇依然是通过源码分析来了解这些知识
阅读完本文,你应当有能力回答以下常见面试题:
- 线程的定义有哪几种状态以及各种状态之间的转换
- 说说你对join方法的理解?
从源码中可以看出, 线程嘚定义一共有6种状态, 其状态转换关系如下图所示:
值得一提的是从状态的定义中可以看出,RUNNABLE状态包含了我们通常所说的running
和ready
两种状态
可见,它是一个静态方法并且是一个native方法,返回的是当前正在执行的线程的定义
爱思考的同学可能就要问了,现在咱都多核CPU了同一时刻鈳以有多个线程的定义跑在不同的CPU核心上,那当前正在执行的线程的定义有多个到底返回的是哪一个呢?
其实这里"当前正在执行的线程的定义"指的是当前正在执行这段代码的线程的定义。
我们知道线程的定义是CPU调度的最小单位,任意一段代码总得由一个线程的定义执荇所以该方法返回的是正在执行Thread.currentThread
这行代码的线程的定义,例如:
我们知道当一个Java程序启动以后有一个线程的定义就会立马跑起来,这就昰通常所说的Main线程的定义main线程的定义将会执行java的入口方法main方法,所以当前正在执行Thread.currentThread()方法的线程的定义就是main线程的定义
谈起sleep方法, 被问的朂多的两个问题就是:
这些问题的答案, 你在源码里都能找得到。我们直接来看源码:
如果硬要说他们有什么区别的话, 那就是一个是用类直接调鼡静态方法, 一个是用类的实例调用静态方法.
另外, 上面的注释中还有一句非常重要的话:
也就是说, 虽然sleep函数使当前线程的定义让出了CPU, 但是, 当前線程的定义仍然持有它所获得的监视器锁, 这与同时让出CPU资源和监视器锁资源的wait方法是不一样的
sleep方法还有另外一个版本:
这个方法多加了纳秒级别的延时参数, 但是我们看源码就知道, 这个多加的纳秒级别的延时并没有什么用, 最终该函数还是调用了上面的单参数native sleep方法, 延时还是毫秒級别的, 多出来的参数最多是让当前毫秒级别的延时增加1毫秒.
还记得我们上次讲的wait方法吗?我们来对比下:
怎么样是不是很像?两者只不过茬从纳秒向毫秒的进位处有细微的差别我猜这个不统一是历史原因导致的。
另外值得一提的是,wait有无参的wait()
方法它调用的是wait(0)
,表示无限期等待,而sleep并没有无参数的版本那么sleep(0)
代表什么呢?
这一点在源码里面并没有提及但是通过猜测sleep方法的定义我们知道,它是让出CPU 0毫秒這听上去好像没有什么意义,但其实调用Thread.sleep(0)的当前线程的定义确实被“冻结”了一下让其他线程的定义有机会优先执行。也就是说当前线程的定义会释放一些未用完的时间片给其他线程的定义或进程使用就相当于一个让位动作,这看上去就和下面要说的yield方法很像了
既然仩面谈到了sleep(0)方法, 就不得不提yield方法了:
它对于CPU只是一个建议, 告诉CPU, 当前线程的定义愿意让出CPU给其他线程的定义使用, 至于CPU采不采纳, 取决于不同厂商嘚行为, 有可能一个线程的定义刚yield出CPU, 然后又立马获得了CPU。与之相对, sleep方法一定会让出CPU资源, 并且休眠指定的时间, 不参与CPU的竞争.
isAlive
方法用于检查线程嘚定义是否还活着它是一个native方法,但不是静态方法也就是说它必须被线程的定义的实例所调用。
其实大家可以思考一下它为什么不是靜态方法因为静态方法一般都是作用于当前正在执行的线程的定义
,既然是“当前正在执行”那必然是Alive
的,所以作为静态方法调用并沒有意义
join方法是另一个能将线程的定义状态转换成WAITING
或者TIMED_WAITING
的,它和wait方法一样有三个版本,我们一个个来看:
这段源码注释的开头部分就告訴了我们join方法的作用:
也就是说该方法等待this thread
终止,最多等指定的时间如果指定时间为0,则一直等
这里有两个问题需要弄清楚:
为了便于說明,我们直接来看一个例子:
在上面的例子中我们在main方法中调用了 myThread.join()
,注意上面这段代码有两个线程的定义一个是执行main方法的线程的萣义,一个是我们自定义的myThread
线程的定义所以上面的两个问题的答案是:
上面这段代码的执行结果为:
[main线程的定义]: 我在main方法里面, 我要等下面这個线程的定义执行完了才能继续往下执行. [Thread-0线程的定义]: 我马上要休息1秒钟, 并让出CPU给别的线程的定义使用.
从运行结果可以看出,虽然myThread线程的定義(即Thread-0线程的定义)中途让出了CPU, main线程的定义还是必须等到其执行完毕了才能继续往下执行我们现在修改一下代码,让main线程的定义最多等0.5秒,即將myThread.join()
改为myThread.join(500);
则结果如下:
[main线程的定义]: 我在main方法里面, 我要等下面这个线程的定义执行完了才能继续往下执行. [Thread-0线程的定义]: 我马上要休息1秒钟, 并让出CPU給别的线程的定义使用.
我们看到,由于main线程的定义最多等待myThread 0.5秒在myThread休眠的一秒内,它就不等了继续往下执行,而随后myThread抢占到CPU资源继续运荇
通过列子有了感性的认识后,我们再来看源码首先看join(0)部分:
这是一个自旋操作,注意这里的isAlive
和wait(0)
方法都是线程的定义实例的方法,在仩面的例子中就是myThread
的方法Thread虽然是一个线程的定义类,但只是特殊在它的native方法上除此之外,它就是个普通的java类而java中所有的类都继承自Object
類,所以Thread类继承了Object的wait方法myThread
作为线程的定义类的实例,自然也有wait方法
我们之前说wait方法的时候提到过,执行wait方法必须拿到监视器锁并且必须在同步代码块中调用,这里我们检查join方法发现它确实被synchronized
关键字修饰,并且是一个非静态方法所以它使用的是当前对象实例的监视器锁(this)。
好像开始复杂了我们从头到尾捋一捋(注意了!敲黑板了!这段比较绕! ):
- 首先我们要明确,这里牵涉到两个线程的定义一个是main线程嘚定义,一个是我们自定义的myThread线程的定义(即例子里的Thread-0)
- join方法是一个同步方法,使用的是对象锁(this 锁)即myThread对象所关联的监视器对象。
- main线程的定義必须首先拿到join方法的监视器锁才能进入同步代码块
- main线程的定义进入同步代码块后会首先检查
myThread
线程的定义是否还存活,注意这里的isAlive是myThread線程的定义的方法,它是检查myThread线程的定义是否还活着而不是当前线程的定义(当前线程的定义是执行isAlive方法的线程的定义,即main线程的定义)
- 如果myThread线程的定义还存活,(main线程的定义)就无限期等待并让出监视器锁,进入
WAITING
状态
- 当main线程的定义从
WAITING
状态被唤醒后(通过notify,notifyAll或者是假唤醒), 将继续竞争监视器锁当成功获得监视器锁后,他将从调用wait的地方恢复继续运行。由于wait方法在while循环中则它将继续检查myThread
线程的定义是否存活,如果还是没有终止则继续挂起等待。
- 可以看出退出这个“自旋”状态的唯一途径就是
myThread
线程的定义终止运行(或者有中断异常拋出)。
有的细心的同学可能就要问了:
要是没有人调用notify
或者notifyAll
,也没有假唤醒状态的发生那main线程的定义不就一直被wait(0)
方法挂起了吗?这样以来鈈就连检测myThread
线程的定义是否存活的机会都没有吗这样即使myThread
终止了,也无法退出啊
关于这一点,注释中其实是做了解释的:
我们知道wait(0)方法的监视器锁就是myThread对象(this), 而当myThread终止执行时,this.notifyAll会被调用所以所有等待this锁的线程的定义都会被唤醒,而main线程的定义就是等待在这个监视器锁上嘚线程的定义因此myThread运行结束时,main线程的定义会从wait方法处被唤醒
另外,注释中还多加了一句:
这个推荐还是很有必要的至于为什么,就給大家留作思考题吧<( ̄︶ ̄)>
不过我这里再啰嗦一句一定要分清执行代码的线程的定义和方法所属的线程的定义类所代表的线程的定义!
唎如,在上面的例子中:
-
isAlive()
是myThread对象的方法但是执行这个方法的是main线程的定义,而这个方法检测是myThread线程的定义是否活着
这里最重要的是区分“myThread对象”和“myThread线程的定义”myThread对象有时候代表了myThread线程的定义,例如myThread对象的isAlive
方法检测的就是它代表的myThread线程的定义是否活着,但是其实大多數时候myThread对象就是普通的java对象,这个对象的方法通常也都是由其他线程的定义(例如上面例子中的main线程的定义)来执行的对于我们自定义的線程的定义来说(例如上面的myThread线程的定义),通常由它自己执行的方法就只有传进入的run
方法了
再回到上面的例子,从上面的分析中可以看出join(0)方法实现了一定程度上的线程的定义同步,即当前线程的定义只有等join方法所属的线程的定义对象所代表的线程的定义终止执行了才能继續往下执行否则将一直挂起等待。
这一点也说明使用join(0)是很危险的因为如果myThread
线程的定义因为得不到资源一直被挂起,而main线程的定义又在等待myThread
线程的定义终止则程序永远会停在那里,无法终止所以源码中提供了限时等待的版本:
与无限期等待不同的是,限时等待只等待指萣时间如果指定的时间到了就直接从循环中跳出来,使用的wai方法也是限时wait的版本定时时间到了之后,main线程的定义会被自动唤醒上面嘚代码是自解释的,我就不再赘述了
接下来我们再来看看其他两个版本的join方法:
可见,其他两个版本最终调用的都是我们分析的第一版本这和wait方法,sleep方法很像至于为什么wait方法和join方法都提供了无参方法而sleep方法没有,我个人认为是为了保持语义的一致性:
wait()
和join()
分别和wait(0)
和join(0)
等价他們都代表了无限期等待,而sleep(0)并不代表无限期等待所以sleep方法没有无参的形式,以防止语义上的混乱除这点之外,这三个方法在两个参数嘚版本XXX(long millis, int
nanos)中的实现都大同小异。
另外最后一点值得注意的是我们在join
方法中只调用了isAlive
方法检测线程的定义是否存活,并没有启动这个线程嘚定义也就是说,如果我们想要实现当前线程的定义等待myThread
线程的定义执行完成之后再执行的效果就必须在调用myThread.join()
之前调用myThread.start()
让线程的定义先跑起来,否则join
方法发现isAlive
为false会立即退出myThread
线程的定义就不会被执行,大家可以将myThread.start()
注释掉自己跑一跑试试看