Java并发和多线程-JUC锁-LockSupport
LockSupport是锁中的基础,是一个提供锁机制的工具类。
带着BAT大厂的面试问题去理解
- 为什么LockSupport也是核心基础类? AQS框架借助于两个类:Unsafe(提供CAS操作)和LockSupport(提供park/unpark操作)
- 写出分别通过wait/notify和LockSupport的park/unpark实现同步?
- LockSupport.park()会释放锁资源吗? 那么Condition.await()呢?
- Thread.sleep()、Object.wait()、Condition.await()、LockSupport.park()的区别? 重点
- 如果在wait()之前执行了notify()会怎样?
- 如果在park()之前执行了unpark()会怎样?
LockSupport简介
LockSupport用来创建锁和其他同步类的基本线程阻塞原语。内部是通过虚拟机的Unsafe(可以直接操作内存)实现的。
LockSupport类的核心方法其实就两个:**park()和unpark()**,其中park()
方法用来阻塞当前调用线程,unpark()
方法用于唤醒指定线程。
这其实和Object类的wait()和signal()方法有些类似,但是LockSupport的这两种方法从语意上讲比Object类的方法更清晰,而且可以针对指定线程进行阻塞和唤醒。
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1。
初始时,permit为0,当调用unpark()方法时,线程的permit加1;当调用park()方法时,如果permit为0,则调用线程进入阻塞状态。
LockSupport源码分析
基于JDK 8
类的属性
1 | public class LockSupport { |
UNSAFE字段表示sun.misc.Unsafe类,查看其源码,点击在这里,一般程序中不允许直接调用;
而long型的表示实例对象相应字段在内存中的偏移地址,可以通过该偏移地址获取或者设置该字段的值。
类的构造函数
1 | // 私有构造函数,无法被实例化 |
LockSupport只有一个私有构造函数,无法被实例化。
核心方法分析
sun.misc.Unsafe
在分析LockSupport函数之前,先引入sun.misc.Unsafe类中的park和unpark函数,因为LockSupport的核心函数都是基于Unsafe类中定义的park和unpark函数。
下面给出两个函数的定义:
1 | // Unsafe |
- Unsafe#park
阻塞线程,并且该线程在下列情况发生之前都会被阻塞:- ① 调用unpark函数,释放该线程的许可。
- ② 该线程被中断。
- ③ 设置的时间到了。并且,当time为绝对时间时,isAbsolute为true,否则,isAbsolute为false。当time为0时,表示无限等待,直到unpark发生。
- Unsafe#unpark
释放线程的许可,即激活调用park后阻塞的线程。这个函数不是安全的,调用这个函数时要确保线程依旧存活。
park方法
1 | // LockSupport |
park函数有两个重载版本,两个函数的区别在于park()函数没有没有blocker,即没有设置线程的parkBlocker字段。
- park(Object blocker)
调用LockSupport#park函数时,首先获取当前线程,然后设置当前线程的parkBlocker字段,即调用setBlocker函数,之后调用Unsafe类的park函数,之后再调用setBlocker函数。
为什么要在LockSupport#park函数中要调用两次setBlocker函数呢?
原因其实很简单,调用LockSupport#park函数时,当前线程首先设置好parkBlocker字段,然后再调用Unsafe类的park函数,这时当前线程就已经阻塞了,等待该线程的LockSupport#unpark(currentThread)被调用,所以第二个setBlocker(t, null)
暂时无法运行;unpark函数被调用,该线程获得许可后,就可以继续运行了,此时运行setBlocker(t, null)
,这样就完成了整个LockSupport#park的逻辑。
如果没有setBlocker(t, null)
,那么之后没有调用**LockSupport#park(object),而直接调用getBlock函数,得到的是前一个LockSupport#park(object)设置的blocker,显然是不合理的。
setBlocker(t, null)
,是保证在LockSupport#park(object)**整个函数执行完后,该线程的parkBlocker字段又恢复为null。 - park()
调用了park()函数后,会禁用当前线程,除非许可可用。
在以下三种情况之一发生之前,当前线程都将处于休眠状态,即下列情况发生时,当前线程会获取许可,可以继续运行。- 其他某个线程将当前线程作为目标调用 unpark(currentThread)。
- 其他某个线程中断当前线程。
- 该调用不合逻辑地(即毫无理由地)返回。
- parkNanos(Object blocker, long nanos)
nanos参数表示相对时间,表示等待多长时间 - parkUntil(Object blocker, long deadline)
deadline参数表示绝对时间,表示指定的时间
unpark方法
此函数表示如果给定线程的许可尚不可用,则使其可用。如果线程在 park 上受阻塞,则它将解除其阻塞状态。否则,保证下一次调用 park 不会受阻塞。如果给定线程尚未启动,则无法保证此操作有任何效果。
1 | public static void unpark(Thread thread) { |
Thread - parkBlocker属性
线程对象 Thread 里面有一个重要的属性 parkBlocker,它保存当前线程因为什么而 park。
1 | class Thread { |
当线程被 unpark 唤醒后,这个属性会被置为 null。
Unsafe.park 和 unpark 并不会帮我们设置 parkBlocker 属性,负责管理这个属性的工具类是 LockSupport,它对 Unsafe 这两个方法进行了简单的包装。
Java 的AQS锁数据结构正是通过调用 LockSupport 来实现休眠与唤醒的。
线程对象里面的 parkBlocker 字段的值就是「排队管理器」- AbstractQueuedSynchronizer。
1 | // AbstractQueuedSynchronizer |
LockSupport示例
使用wait/notify实现线程同步
使用park/unpark实现线程同步
中断响应
更深入的理解
Thread.sleep()和Object.wait()的区别
最大的区别是Thread.sleep()不会释放锁资源,Object.wait()会释放锁资源。
Object.wait()和Condition.await()的区别
Thread.sleep()和LockSupport.park()的区别
Object.wait()和LockSupport.park()的区别
Object.wait()
- Object.wait()方法需要在synchronized块中执行;
- Object.wait()方法抛出了中断异常,调用者需要捕获或者再抛出;
- Object.wait()不带超时的,需要另一个线程执行notify()来唤醒,但不一定继续执行后续内容;
LockSupport.park()
- LockSupport.park()可以在任意地方执行;
- LockSupport.park()不需要捕获中断异常;
- LockSupport.park()不带超时的,需要另一个线程执行unpark()来唤醒,一定会继续执行后续内容;
如果在wait()之前执行了notify()会怎样?
如果当前线程不是此对象锁的所有者,却调用该对象的notify()或wait()方法时,会抛出IllegalMonitorStateException异常。
如果当前线程是此对象锁的所有者,在wait()之前执行了notify(),wait()后将一直阻塞,因为后续将没有其他线程notify()来唤醒它。如果在park()之前执行了unpark()会怎样?
线程不会被阻塞,直接跳过park(),继续执行后续内容。
原因是unpark()先释放了一个凭证,所以park()的时候,凭证已经存在则不阻塞继续执行。
park()/unpark()底层的原理是“二元信号量”,可以把它想象成只有一个许可证的Semaphore,只不过这个信号量在重复执行unpark()的时候也不会再增加许可证,最多只有一个许可证。
LockSupport.park()会释放锁资源吗?
不会,它只负责阻塞当前线程,释放锁资源实际上是在Condition的await()方法中实现的。