【JDK】LockSupport 工具类

1  前言

LockSupport 工具类最近复习到这个类了,之前也没做笔记,这里简单回顾下哈。

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

2  重要函数

2.1  void park() 方法

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

如下代码直接在 main 函数里面调用 park 方法,最终只会输出 begin park!,然后当前 线程被挂起,这是因为在默认情况下调用线程是不持有许可证的。

public static void main( String[] args ) {
    System.out.println("begin park!");
    LockSupport.park();
    System.out.println("end park!");
}

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

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

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
        System.out.println("child thread start");
        // 阻塞  --阻塞时被中断不会抛出异常
        LockSupport.park();
        System.out.println("child thread end");
    });
    thread.start();
    TimeUnit.SECONDS.sleep(1);
    System.out.println("main begin unpark");
    // 唤醒
    // LockSupport.unpark(thread);
    // 中断
    thread.interrupt();
}

示例:

2.2  void unpark(Thread thread) 方法

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

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

该代码会输出:

begin park! 
end park!

下面再来看一个例子以加深对 park 和 unpark 的理解。

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();
    //主线程休眠1s
    Thread.sleep(1000);
    System.out.println("main thread begin unpark!");
    //调用unpark方法让thread线程持有许可证,然后park方法返回
    LockSupport.unpark(thread);
}

输出结果为:

child thread begin park! 
main thread begin unpark! 
child thread unpark!

上面代码首先创建了一个子线程 thread, 子线程启动后调用 park 方法,由于在默认情 况下子线程没有持有许可证,因而它会把自己挂起。

主线程休眠 1s 是为了让主线程调用 unpark 方法前让子线程输出 child thread begin park! 并阻塞。

主线程然后执行 unpark 方法,参数为子线程,这样做的目的是让子线程持有许可证, 然后子线程调用的 park 方法就返回了。

park 方法返回时不会告诉你因何种原因返回,所以调用者需要根据之前调用 park 方 法的原因,再次检查条件是否满足,如果不满足则还需要再次调用 park 方法。

例如,根据调用前后中断状态的对比就可以判断是不是因为被中断才返回的。

为了说明调用 park 方法后的线程被中断后会返回,我们修改上面的例子代码,删除 LockSupport.unpark(thread);,然后添加 thread.interrupt();,具体代码如下。

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方法,挂起自己,只有被中断才会退出循环
            while (!Thread.currentThread().isInterrupted()) {
                LockSupport.park();
            }
            System.out.println("child thread unpark!");
        }
    });
    // 启动子线程
    thread.start();
    // 主线程休眠1s
    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) 方法子线程也不会结束。

2.3  void parkNanos(long nanos) 方法

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

另外 park 方法还支持带有 blocker 参数的方法 void park(Object blocker) 方法,当线程 在没有持有许可证的情况下调用 park 方法而被阻塞挂起时,这个 blocker 对象会被记录到 该线程内部。

使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调用 getBlocker(Thread) 方法来获取 blocker 对象的,所以 JDK 推荐我们使用带有 blocker 参数的 park 方法 , 并且 blocker 被设置为 this,这样当在打印线程堆栈排查问题时就能知道是哪个类被阻塞了。

例如下面的代码。

public class TestPark {
    public void testPark(){
        LockSupport.park();//(1)
    }
    public static void main(String[] args) {
        TestPark testPark = new TestPark();
        testPark.testPark();
    }
}

运行代码后,使用 jstack pid 命令查看线程堆栈时可以看到如下输出结果:

修改代码(1)为 LockSupport.park(this) 后运行代码,则 jstack pid 的输出结果为:

使用带 blocker 参数的 park 方法,线程堆栈可以提供更多有关阻塞对象的信息。

2.4  park(Object blocker) 方法

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

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

2.5  void parkNanos(Object blocker, long nanos) 方法

相比 park(Object blocker) 方法多了个超时时间。

2.6  void parkUntil(Object blocker, long deadline) 方法

它的代码如下:

public static void parkUntil(Object blocker, long deadline) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    //isAbsolute=true,time=deadline;表示到deadline时间后返回
    UNSAFE.park(true, deadline);
    setBlocker(t, null);
}

其中参数 deadline 的时间单位为 ms,该时间是从 1970 年到现在某一个时间点的毫秒 值。这个方法和 parkNanos(Object blocker, long nanos) 方法的区别是,后者是从当前算等待 nanos 秒时间,而前者是指定一个时间点,比如需要等到 2017.12.11 日 12:00:00,则把 这个时间点转换为从 1970 年到这个时间点的总毫秒数。

最后再看一个例子:

class FIFOMutex {
        private final AtomicBoolean locked = new AtomicBoolean(false);
        private final Queue<Thread> waiters = new ConcurrentLinkedQueue<Thread>();
        public void lock() {
            boolean wasInterrupted = false;
            Thread current = Thread.currentThread();
            waiters.add(current);
            // 只有队首的线程可以获取锁(1)
            while (waiters.peek() != current || !locked.compareAndSet(false, true)) {
                LockSupport.park(this);
                if (Thread.interrupted()) // (2)
                    wasInterrupted = true;
            }
            waiters.remove();
            if (wasInterrupted) // (3)
                current.interrupt();
        }
        public void unlock() {
            locked.set(false);
            LockSupport.unpark(waiters.peek());
        }
    }

这是一个先进先出的锁,也就是只有队列的首元素可以获取锁。在代码(1)处,如 果当前线程不是队首或者当前锁已经被其他线程获取,则调用 park 方法挂起自己。

然后在代码(2)处判断,如果 park 方法是因为被中断而返回,则忽略中断,并且重 置中断标志,做个标记,然后再次判断当前线程是不是队首元素或者当前锁是否已经被其 他线程获取,如果是则继续调用 park 方法挂起自己。

然后在代码(3)中,判断标记,如果标记为 true 则中断该线程,这个怎么理解呢? 其实就是其他线程中断了该线程,虽然我对中断信号不感兴趣,忽略它,但是不代表其他 线程对该标志不感兴趣,所以要恢复下。

3  小结

好啦,关于 LockSupport 就看到这里哈。

posted @ 2024-02-09 15:05  酷酷-  阅读(7)  评论(0编辑  收藏  举报