线程重要的相关方法
本文是关于JAVA多线程和并发的第四篇,本篇文章主要来看看线程相关的几个重要方法。
wait和sleep
他们最基本的差异是:
wait
是Object
的一个方法,sleep
是Thread
类的方法sleep
可以在任何地方使用,但是wait
方法只能在synchronied
方法或synchronied
块中使用
本质的区别是:
Thread.sleep
只会让出CPu,不会导致锁行为的改变Object.wait
不仅让出CPU,还会释放已经占用的同步资源锁
这个区别也就解释了为什么wait
方法只能在synchronied
方法或synchronied
块中使用,因为没有获取过锁哪里来的释放锁呢?所以释放锁的前提是要获取锁。
下面来验证一下,眼见为实!
对于sleep
来说是没有锁的要求的,既不用获取锁也不用释放锁,关于这一点就不再验证了。
notify和notifyAll
先来了解一下锁池和等待池的概念。
- 锁池EntryLisy
假设线程A已经拥有了某个对象(不是类)的锁,而其他线程B、C想要调用这个对象的某个synchronized方法或块,由于B、C线程在进入对象的synchronized方法或块之前必须先获得该对象锁得拥有权,而恰巧该对象的锁正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待所得释放,这个地方就是该对象得锁池。
- 等待池WaitSet
假设线程A调用了某个对象的wait方法后,线程A就会释放该对象得锁,同时线程A句进入到该对象得等待池中,进入到等待池中得线程不会去竞争该对象的锁。
notify
的作用就是随机唤醒一个线程进入等待池的线程,而notifyAll
是唤醒所有处于等待池中线程,唤醒之后就可以再去竞争获得锁的机会了。
刚才的例子稍微改造一下,来了解一下notify
的作用。还拿刚才那个例子:
yield
当调用Thread.yield()
函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示。并且它也不会释放当前线程占用的锁。
yield()
与无参的wait()
的区别:
- 执行
yield()
后,当前线程由运行状态变为就绪状态。执行wait
后,当前线程会失去对象的锁,状态变为WAITING
状态。 - 执行
yield()
后,当前线程不会释放锁。执行wait
后,当前线程会释放锁。
比较简单,就不举例了。
interrupt
它只是通知线程应该中断了。
- 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个
interruptedException
异常 - 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
也就是说,中断一个线程是由被调用的线程状态和自己程序判断决定的。
- 阻塞状态下,线程会立即退出,并抛出异常
- 正常状态下,需要被调用的线程检查中断标志位,然后再根据中断标志位自行地停止线程
下面写一个demo来验证一下:
1 | public class InterruptDemo { |
打印结果为:
1 | t1 (NEW) is new. |
首先,是就绪状态,为new
;接下来启动这个线程,状态变为started
,由于此时一切安好,没有“打扰”这个线程的执行,所以每隔100毫秒打印一句(RUNNABLE) loop i
出来;在400毫秒的安好之后,给他一个t1.interrupt();
,此时线程可能恰好在执行sleep
睡觉呢,这个interrupt
一看你在阻塞(睡觉),那还得了,立马停止这个线程并且抛出异常。
但是话说回头,本程序还用了if判断,只要标志位为false
就不停循环,一旦标志位变为true
则立马退出循环。所以即使你不睡觉,但是我还是能通过这个If来终止你的循环。
join
join
是加入的意思,非常形象生动。
1 | /** |
具体的实现如下:
我们知道wait
是需要释放当前线程所占的对象锁的,而join
基于wait
实现,显然是可以的。
这里判断如果线程还在运行中的话,则继续等待,如果指定时间到了,或者线程运行完成了,则代码继续向下执行,调用线程就可以执行后面的逻辑了。
但是在这里没有看到哪里调用notify
或者notifyAll
方法,如果没有调用的话,那调用方线程会一直等待下去,那是哪里调用了唤醒它的方法呢?通过查证得知,原来在线程结束时,java虚拟机会执行该线程的本地exit
方法,这个exit
方法里面会调用notifyAll
方法,唤醒所有等待的线程。
下面来两个例子来彻底理解它的用法。
例子一:有耐心的男孩:
男孩和女孩准备出去逛街
女孩开始化妆,男孩在等待。。。
女孩化妆完成!,耗时5000
男孩和女孩开始去逛街了
就是男孩和女孩准备去逛街,女孩要化妆先,等女孩化妆完成了,再一起去逛街。
例子二:没有耐心的男孩:
男孩和女孩准备出去逛街
女孩开始化妆,男孩在等待。。。
男孩等了2000, 不想再等了,去逛街了
女孩化妆完成!,耗时5000
男孩等了join(time)
中的time
时间,如果这个time
时间到达之后,女孩所在的线程还没执行完,则不等待了,继续执行后面的逻辑,就是不等女孩了,自己去逛街。
总结
了解了这些核心方法之后,就可以对下面这幅图简单说一说啦:
首先是new Thread()
只是新建状态,只有start
之后才会进入runnable
状态,注意这个状态里面可能有两种状态,一种是正在运行,即running
,还有一种是就绪状态即ready
,这两个状态归属于一类的原因是他们之间是在不断切换的,即CPU的时间片内临幸到这个进程,这个进程中有若干个线程的话,就会高速地切换各个线程逐个执行,达到宏观上是并行执行的效果。我们知道yield
是给线程调度器一个暗示让出当前执行的线程的时间片,至于这个线程调度器听不听那就不知道了,所以存在一定的随机性。如果正常执行结束就进入最后的终止状态。往右边看,如果发生带时间的超时等待,如sleep(100)
,本线程会阻塞,让出CPU执行权并且不改变锁状态,与之区别的是wait(100)
这个方法不仅让出CPU执行权,还会释放锁,所以要调用wait
方法必然要先获取锁,所以一般都是在synchronized
中调用它。至于join(100)
是指阻塞当前线程,让其他的线程先执行,底层是wait
所以也会释放锁。超时等待只要等它时间过了就可以跳出阻塞状态了,或者用notify
或者interrupt
之类的来唤醒或者打断它。往左下角看,是锁获取的时候可能发生阻塞,这个时候只能等其他线程释放锁才行了。往左边看,是无限期等待的代表,唤醒手段与有限期等待是一样的。