LockSupport类的字段和方法

总体描述

image.png
通过上面图片可以看到,LockSupport具有的字段:parkBlockerOffset、UNSAFE、SEED、PROBE、SECONDARY ;具有的方法:getBlocker(Thread t)、park()、park(Object blocker)、parkNanos(long nanos)、parkNanos(Object blocker, long nanos)、parkUntil(long deadline)、parkUntil(Object blocker, long deadline)、setBlocker(Thread t, Object arg)、unpark(Thread thread);此外,还有一块静态代码块。

字段

image.png
它的字段主要是一个引用类型UNSAFE对象,这个类前面提到过;而其它4个字段其实是Thread类中四个属性的内存偏移地址。可以看到在静态代码块中获取了Thread对象的四个属性的偏移地址,那就是说LockSupport类中需要用到这四个偏移地址。以parkBlocker字段为例,示意图如下:
image.png

方法

getBlocker和setBlocker

image.png
image.png
可以看到这两个方法其实就是分别给上图的Thread对象的blocker字段取值和赋值操作,并且这种操作是通过unsafe这个类操作的,也就是直接操作的内存,与这个Thread类对象是否处于运行状态没有关系。

park类型的方法

image.png
首先是park()方法,其实可以看到它调用的是unsafe类的park方法,而这个方法是一个native方法,首先需要先了解unsafe的park和unpark方法:
image.png
而unsafe类的park和unpark底层是cpp实现,具体怎么实现可以参考:
https://juejin.cn/post/6844903703623761933#heading-3
此外,park方法还有一个重载方法:
image.png
可以其实就是设置线程对象的blocker字段,而这个blocker字段是用来标识当前线程等待的对象(简称为阻塞对象),该对象主要用于问题排查和系统监控。

由于在Java 5之前,当线程阻塞时(使用synchronized关键字)在一个对象上时,通过线程dump能够查看到该线程的阻塞对象。方便问题定位,而Java 5退出的Lock等并发工具却遗漏了这一点,致使在线程dump时无法提供阻塞对象的信息。因此,在Java 6中,LockSupport新增了含有阻塞对象的park方法。用以替代原有的park方法。

可见调用了两次setBlocker方法,这个方法是直接操作内存的。当执行到“UNSAFE.park”之后,当前线程就阻塞了,无法执行到下一个setBlocker,而当线程t被唤醒(比如调用unpark)就会接着执行第二个setBlocker方法,并把阻塞对象设置为null,线程t重新进入到运行状态。如下代码所示:

  1. class LockSupportDemo {
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread a = new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. LockSupport.park("线程a的blocker数据");
  7. System.out.println("我是被线程b唤醒后的操作");
  8. }
  9. });
  10. a.start();
  11. //让当前主线程睡眠1秒,保证线程a在线程b之前执行
  12. Thread.sleep(1000);
  13. Thread b = new Thread(new Runnable() {
  14. @Override
  15. public void run() {
  16. String before = (String) LockSupport.getBlocker(a);
  17. System.out.println("阻塞时从线程a中获取的blocker------>" + before);
  18. LockSupport.unpark(a);
  19. //这里睡眠是,保证线程a已经被唤醒了
  20. try {
  21. Thread.sleep(1000);
  22. String after = (String) LockSupport.getBlocker(a);
  23. System.out.println("唤醒时从线程a中获取的blocker------>" + after);
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. });
  29. b.start();
  30. }
  31. }

image.png
而上述例子,完全知道了blocker可以在线程阻塞的时候获取数据,也就证明了当我们对线程进行问题排查和系统监控的时候blocker的有着非常重要的作用。

剩下的几个park类型的方法就是加了时间限制,如下所示:
image.png
image.png
image.png
image.png

unpark方法

image.png
LockSupport类的unpark方法其实调用的是unsafe类的unpark方法,只需要一个需要unpark的线程对象即可。

LockSupport的优点

这里说的LockSupport的优点主要是对比Object类的wait/notify的优点。

简单

相比wait/notify,parl/unpark其实更加简单易用,因为wait/notify必须在同步代码块中使用,比如下面的代码是错误的,会抛出异常:

  1. public class TestObjWait {
  2. public static void main(String[] args) throws Exception {
  3. final Object obj = new Object();
  4. Thread t = new Thread(() -> {
  5. int sum = 0;
  6. for (int i = 0; i < 10; i++) {
  7. sum += i;
  8. }
  9. try {
  10. obj.wait();
  11. } catch (Exception e) {
  12. e.printStackTrace();
  13. }
  14. System.out.println(sum);
  15. });
  16. t.start();
  17. //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
  18. Thread.sleep(1000);
  19. obj.notify();
  20. }
  21. }

image.png

至于为什么会抛出异常,其实就是wait/notify机制的实现原理,可以参考:https://www.yuque.com/docs/share/4bddd325-236c-491e-b2eb-012a2422afaf?# 《wait-notify机制》

修改为下面这样才是正确的:

  1. public class TestObjWait {
  2. public static void main(String[] args) throws Exception {
  3. final Object obj = new Object();
  4. Thread t = new Thread(() -> {
  5. int sum = 0;
  6. for (int i = 0; i < 10; i++) {
  7. sum += i;
  8. }
  9. try {
  10. synchronized (obj) {
  11. obj.wait();
  12. }
  13. } catch (Exception e) {
  14. e.printStackTrace();
  15. }
  16. System.out.println(sum);
  17. });
  18. t.start();
  19. //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
  20. Thread.sleep(1000);
  21. synchronized (obj) {
  22. obj.notify();
  23. }
  24. }
  25. }

image.png
而park/unpark其实就没有这个限制了,如下代码所示:

  1. public class TestObjWait {
  2. public static void main(String[] args) throws Exception {
  3. Thread t = new Thread(() -> {
  4. int sum = 0;
  5. for (int i = 0; i < 10; i++) {
  6. sum += i;
  7. }
  8. LockSupport.park();
  9. System.out.println(sum);
  10. });
  11. t.start();
  12. //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
  13. Thread.sleep(1000);
  14. LockSupport.unpark(t);
  15. }
  16. }

灵活

相比wait/notify机制,park/unpark更加灵活。因为如果先调用notify,然后才调用wait。如下代码:

  1. public class TestObjWait {
  2. public static void main(String[] args) throws Exception {
  3. final Object obj = new Object();
  4. Thread A = new Thread(new Runnable() {
  5. @Override
  6. public void run() {
  7. int sum = 0;
  8. for(int i=0;i<10;i++){
  9. sum+=i;
  10. }
  11. try {
  12. synchronized (obj){
  13. obj.wait();
  14. }
  15. }catch (Exception e){
  16. e.printStackTrace();
  17. }
  18. System.out.println(sum);
  19. }
  20. });
  21. A.start();
  22. //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
  23. //Thread.sleep(1000);
  24. synchronized (obj){
  25. obj.notify();
  26. }
  27. }
  28. }

上面的代码有的时候能够正常打印结果并退出程序,但有的时候线程无法打印结果阻塞住了。原因就在于:主线程调用完notify后,线程t才进入wait方法,导致线程t一直阻塞住,从而导致整个程序无法退出。
image.png
而这个其实就是之前所说的“通知遗漏”,程序不停止的原因就是线程t一直在wait:
image.png
但是,使用park/unpark却不会出现通知遗漏,因为先unpark后park不会导致线程一直处于等待状态(具体实现还得是底层的c++),如:

  1. public class TestObjWait {
  2. public static void main(String[] args) throws Exception {
  3. final Object obj = new Object();
  4. Thread t = new Thread(() -> {
  5. int sum = 0;
  6. for(int i=0;i<10;i++){
  7. sum+=i;
  8. }
  9. LockSupport.park();
  10. System.out.println(sum);
  11. });
  12. t.start();
  13. //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
  14. //Thread.sleep(1000);
  15. LockSupport.unpark(t);
  16. }
  17. }

image.png
image.png

参考文章: https://www.cnblogs.com/qingquanzi/p/8228422.html

几个方法的对比

image.png

Thread.sleep()和Object.wait()的区别

image.png

Thread.sleep()和Condition.await()的区别

image.png

Thread.sleep()和LockSupport.park()的区别

image.png

这里特别注意LockSupport.park()是不会释放锁资源的。

Object.wait()和LockSupport.park()的区别

image.png

参考文章: https://www.cnblogs.com/tong-yuan/p/11768904.html