线程等待方法大乱斗
引言
很多人一直困惑于 join wait await sleep park 这几个方法,这几个方法都能阻塞线程,而从翻译字面意思来看,有些词看起来似乎还有点反义词的意思,为什么还是经常将他们并列提起,而且说实际作用类似,都会将当前线程阻塞?他们的关系究竟如何,到底完成了什么功能呢?别急,咱们先一个个捋,最后再总结。
wait——抢别人的房子,然后睡觉
首先一定要注意wait方法是每个Object对象都有的,这是Object的native方法,而不是Thread的方法,而同样每个对象都有的东西是什么?没错,是对象锁!也就是synchrnized方法会竞争的东西,而wait方法正是基于每个对象都有锁这一设定,
wait完成的功能就是让抢到这个对象锁的线程,在执行到wait方法时,会进入这个锁的条件队列等待,而后放弃该对象的锁。注意,仅仅是释放这个对象的锁,如果这个线程身上有其他锁,其他的锁不会释放的。
例如:在代码中写上a.wait()这种东西, 那么当有线程执行到此处方法时,意味着这个线程将阻塞自身并进入a的锁等待队列,放弃a的锁。
眼尖的同学会发现,放弃锁?没错,这意味着这个线程在执行到wait()之前其实是拥有a的锁的,这有就是为什么wait()方法必须都写在synchrnized的方法或代码块内,不然将无法通过。如下图。
join —— 抢同行的房子,然后睡觉
join方法是Thread类带有的实例方法,也就是说这是一个针对线程对象执行的方法,当一个线程要使用该方法,必须对另一个指定线程来执行,作用就是让本线程在指定线程结束后再执行。
例如我们有个线程a,我们想让他在线程b结束后执行,就可以在代码上写上b.join(),然后让a执行,a执行到这里就会阻塞
那么它的原理是什么呢?其实是利用了上面提到的的wait()方法,wait方法是Object的方法,所有对象都能用。线程对象也是对象,自然会有这个方法,换句话说,你在这里使用t1.join() 和 t1.wait()其实是一样的,都是让你进入t1对象的等待队列去,事实上,他的源码就是这么写的。
这里我们必须保持清醒,join 是基于 wait的,他是把线程对象当做普通对象,然后进入这个对象的等待队列中去。你也许会好奇,那我光看见进入等待队列了,怎么没看见notify呢?其实这里利用了线程在结束前会自动notifyAll的能力,所以我们说当我们主线程执行了t1.join后,会等到t1线程结束销毁才会被唤醒,达到了线程排序的效果。
sleep—— 定个闹钟,倒头就睡
sleep和join一样,都属于Thread类的方法,但join因为是A线程把B线程当对象进行wait(),所以是B线程的实例方法,而sleep则是个Thread的静态native方法
例如我们有一段代码Thread.sleep(),任何执行到这段代码的线程都将进入睡眠
关于其原理,其实很简单,就是阻塞,或者说改变线程状态,这可以说是最原始的线程阻塞功能,调用这个方法,其实就是调用linux内核的sys_pause(),进入可中断的等待状态,这是非常纯粹的功能,和wait的等待最大的区别就是它会保持线程一直持有的各种锁。
park——睡自己房间,居然会失败
park()方法和上面又不太一样,他是隶属于sun.misc.Unsafe类,是这个类的实例native方法。 这个类我们在学习线程的时候会经常遇到,以后会详谈。
当然,我们一般使用的是LockSupport.park(),而LockSupport.park内调用的其实还是unsafe.park,我们先看方法
第一个参数是是否是绝对时间,如果isAbsolute是true则代表确切时间点,以ms为单位,是false则代表指定一段时间后,以ns计时。
第二个参数是等待时间值。
public native void park(boolean isAbsolute, long time);
那么这个方法的原理是什么呢?其实还是锁,或者我们叫互斥量。每一个java线程都内置了一个Parker对象,该对象包含一个互斥量,当你调用park()时,调用的其实是Parker::park ,线程会进入该互斥量的条件队列中去并进入等待,没错,是不是和wait()很像?
- wait依赖于指定对象,a.wait()是线程进入你指定的对象a的条件队列,使线程进入WAITING 或 TIMED-WAITING状态
- park不需指定对象,unsafe.park()则会使线程进入线程自带的Parker对象的条件队列,使线程进入WAITING 或 TIMED-WAITING状态
当然了,park() 方法内部还有一些修饰和判断,这使他的功能和wait()产生了区别
void Parker::park(bool isAbsolute, jlong time) {
//如果_counter > 0,则将_counter置为0,直接返回,否则_counter为0
if (Atomic::xchg(0, &_counter) > 0) return;
//获取当前线程
Thread* thread = Thread::current();
assert(thread->is_Java_thread(), "Must be JavaThread");
JavaThread *jt = (JavaThread *)thread;
//如果当前线程设置了中断标志,调用park则直接返回
if (Thread::is_interrupted(thread, false)) {
return;
}
*****
}
如上图,我们可以看到如果_counter > 0 (即之前调用过unpark) ,或者线程有中断标记(即之前调用过interrupt),park都会直接返回。
两者区别就是,我们可以说unpark是我赐予你的一次性丹书铁券,给你了,当我使用一次park()来拘捕你,我这次无功而返,但我会收回你的丹书铁券,下次就没这好运了。而interrupt则是国际上赐予你的丹书铁券,我无法收回,只有当国际上收回之后(清除中断标志),我才能拘捕你(使线程陷入等待)。
同样的,即便我成功拘捕你(使线程等待)以后,unpark 和 interrupt 这两种丹书铁券也可以让你从牢狱中脱困而出(线程恢复)。
await——java层级的wait
await 与上述几种不同,他不是native方法,而是Condition对象的方法。他内部调用了我们上面提到的Park进行线程的睡眠,睡眠前将AQS里的独占线程置为null,r然后进入等待队列。所以使用者角度来说,他释放了lock锁而后进入等待,和wait的功能一模一样,只是lock锁的实现逻辑基于java代码
public final void await() throws InterruptedException {
// 这个方法是响应中断的
if (Thread.interrupted())
throw new InterruptedException();
// 添加到条件队列中
Node node = addConditionWaiter();
// 释放同步资源,也就是释放锁
int savedState = fullyRelease(node);
int interruptMode = 0;
// 如果这个节点的线程不在同步队列中,说明该线程还不具备竞争锁的资格
while (!isOnSyncQueue(node)) {
// 挂起线程
LockSupport.park(this);
// 如果线程中断,退出
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 上面的循环退出有两种情况:
// 1. isOnSyncQueue(node) 为true,即当前的node已经转移到阻塞队列了
// 2. checkInterruptWhileWaiting != 0, 表示线程中断
// 退出循环,被唤醒之后,进入阻塞队列,等待获取锁 acquireQueued
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
表格总结
- wait sleep park 才是最原始的方法,即native方法;而 join 依赖wait , await 依赖 park ,则是java方法
- await是juc包下的,针对的是Lock锁,而这个锁是java层级的,所以它调用park只是为了沉睡,它释放锁的逻辑是用java实现的,而其他方法都更加底层,释不释放锁都由c++层级代码决定。