Java并发编程(十九)Java 并发包中锁原理剖析 LockSupport 工具类

逆流者 2021年02月21日 78次浏览

此文为读书笔记,欢迎评论,讨论问题,共同进步!


LockSupport 工具类

JDK 中的rt.jar包里面的LockSupport是个工具类,它的主要作用是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础。
LockSupport类与每个使用它的线程都会关联一个许可证,在默认情况下调用 LockSupport类的方法的线程是不持有许可证的。LockSupport是使用 Unsafe类实现的,下面介绍 LockSupport 中的几个主要函数。

  • void park() 方法
    如果调用 park方法的线程已经拿到了与LockSupport关联的许可证,则调用 LockSupport.park()时会马上返回,否则调用线程会被禁止参与线程的调度,也就是会被阻塞挂起。
public class LockSupportTest {

    public static void main(String[] args) {
        System.out.println("start park!");
        // 默认是没有许可证的
        LockSupport.park();
        System.out.println("end part!");
    }
}

在这里插入图片描述
从执行结果看出: 线程被阻塞了.

在其他线程调用unpark(Thread thread)方法并且将当前线程作为参数时,调用 park()方法而被阻塞的线程会返回。另外,如果其他线程调用了阻塞线程的interrupt()方法,设置了中断标志或者线程被虚假唤醒,则阻塞线程也会返回。所以在调用 park() 方法时最好也使用循环条件判断方式。

需要注意的是,因调用 park()方法而被阻塞的线程被其他线程中断而返回时并不会抛出 InterruptedException异常。

  • void unpark(Thread thread) 方法
    当一个线程调用unpark时,如果参数 thread线程没有持有 thrad与LockSupport类关联的许可证,则让thread线程持有。如果thread之前因调用park()而被挂起,则调用 unpark后,该线程会被唤醒。如果 thread之前没有调用park,则调用unpark方法后,再调用park方法,其会立刻返回。代码如下:
public class LockSupportTest2 {

    public static void main(String[] args) {
        System.out.println("start park!");
        // 使当前线程获取到许可证
        LockSupport.unpark(Thread.currentThread());
        LockSupport.park();
        System.out.println("end part!");
    }
}
start park!
end part!

从执行结果看, 线程没有被阻塞!

再看一个示例:

public class LockSupportTest3 {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("child thread begin park!");
                // 调用park方法,挂起自己
                LockSupport.park();

                System.out.println("child thread unpark!");
            }
        });

        thread.start();

        Thread.sleep(1000);

        System.out.println("main thread begin unpark!");
        LockSupport.unpark(thread);
    }
}
child thread begin park!
main thread begin unpark!
child thread unpark!

子线程一上来就阻塞, 等待主线程休眠结束后执行unpart方法让子线程获取许可证.

public class LockSupportTest4 {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("child thread begin park!");

                // 只有被中断才推出循环
                while (!Thread.currentThread().isInterrupted()) {
                    // 调用park方法,挂起自己
                    LockSupport.park();
                }

                System.out.println("child thread unpark!");
            }
        });

        thread.start();

        Thread.sleep(1000);

        System.out.println("main thread begin unpark!");

        // 中断子线程
        thread.interrupt();
    }
}
child thread begin park!
main thread begin unpark!
child thread unpark!

只有中断子线程,子线程才会运行结束,如果子线程不被中断, 即使调用 unpark(thread) 方法子线程也不会结束。

  • void parkNanos(long nanos) 方法
    和park方法类似,如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.parkNanos(long nanos) 方法后会马上返回。该方法的不同在于,如果没有拿到许可证,则调用线程会被挂起 nanos 时间后修改为自动返回。

  • park(Object blocker) 方法
    park方法还支持带有 blocker 参数的方法void park(Object blocker) 方法,当线程在没有持有许可证的情况下调用 park方法而被阻塞挂起时,这个blocker对象会被记录到该线程内部;
    使用带blocker参数的park方法,线程堆栈可以提供更多有关阻塞对象的信息。
    使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调用getBlocker(Thread)方法来获取blocker对象的,所以JDK推荐我们使用带有blocker 参数的park方法,并且 blocker被设置为this,这样当在打印线程堆栈排查问题时就能知道是哪个类被阻塞了。

public static void park(Object blocker) {
	// 获取该线程
    Threadt = Thread.currentThread();
    // 设置该线程的blocker变量
    setBlocker(t, blocker);
    // 挂起线程
    UNSAFE.park(false, 0L);
    // 线程被激活后清除blocker变量, 因为一般线程阻塞时才分析原因
    setBlocker(t, null);
}

比如对这个程序, 我们使用下诊断命令:

public class TestPark {

    public void testPark() {
        LockSupport.park();
    }

    public static void main(String[] args) {
        TestPark testPark = new TestPark();
        testPark.testPark();
    }
}
// 3777 是 pid
jstack 3777
....
"JPS event loop" #10 prio=5 os_prio=31 tid=0x00007fc36b00d000 nid=0x5503 runnable [0x000070000d567000]
   java.lang.Thread.State: RUNNABLE
	at sun.nio.ch.KQueueArrayWrapper.kevent0(Native Method)
	at sun.nio.ch.KQueueArrayWrapper.poll(KQueueArrayWrapper.java:198)
	at sun.nio.ch.KQueueSelectorImpl.doSelect(KQueueSelectorImpl.java:117)
	at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
	- locked <0x00000007b57101f0> (a 
...

把上面代码LockSupport.park();改成LockSupport.park(this); 再用诊断命令看下结果:

"main" #1 prio=5 os_prio=31 tid=0x00007f8e79009800 nid=0xe03 waiting on condition [0x0000700003602000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x000000076ac295c8> (a base.thread.book.TestPark)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at base.thread.book.TestPark.testPark(TestPark.java:12)
	at base.thread.book.TestPark.main(TestPark.java:17)

Thread类里面有个变量volatile Object parkBlocker,用来存放park方法传递的blocker对象,也就是把 blocker变量存放到了调用 park方法的线程的成员变量里面。

  • void parkNanos(Object blocker,long nanos) 方法
    相比 park(Object locker)方法多了个超时时间。
  • void parkUntil(Object blocker,long deadline)方法
public static void parkUntil(Object blocker, long deadline) {
    Thread t =Thread.currentThread();
    setBlocker(t, blocker);
    // isAbsolute=true, 绝对时间
    // 参数 deadline 的时间单位为 ms,该时间是从1970年到现在某一个时间点的毫秒值。这个方法和parkNanos(Object blocker,long nanos)方法的区别是,后者是从当前算等待 nanos 秒时间,而前者是指定一个时间点,比如需要等到2017.12.11日12∶00∶00,则把这个时间点转换为从 1970 年到这个时间点的总毫秒数。
    UNSAFE.park(true, deadline);
    setBlocker(t, null);
}

看一个先进先出的锁的简单demo:

public class FIFOMutex {

    private final AtomicBoolean locked = new AtomicBoolean(false);
    private final Queue<Thread> waiters = new ConcurrentLinkedDeque<>();

    public void lock() {
        boolean wasInterrupted = false;
        Thread current = Thread.currentThread();
        waiters.add(current);

        // 只有队首的线程可以获取锁
        // 如果当前线程不是队首或者当前锁已经被其他线程获取,则调用 park 方法挂起自己。
        while (waiters.peek() != current || !locked.compareAndSet(false, true)) {
            LockSupport.park(this);
            // 如果 park方法是因为被中断而返回,则忽略中断,并且重置中断标志,做个标记,然后再次判断当前线程是不是队首元素或者当前锁是否已经被其他线程获取,如果是则继续调用 park 方法挂起自己。
            if (Thread.interrupted()) {
                wasInterrupted = true;
            }
        }

        waiters.remove();
        // 如果标记为true 则中断该线程,这个怎么理解呢?
        // 其实就是其他线程中断了该线程,虽然我对中断信号不感兴趣,忽略它,但是不代表其他线程对该标志不感兴趣,所以要恢复下。
        if (wasInterrupted) {
            current.interrupt();
        }
    }

    public void unlock() {
        locked.set(false);
        LockSupport.unpark(waiters.peek());
    }
}