LockSupport 原理解析

683 阅读5分钟

「这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战」。

中文文档地址:www.apiref.com/java11-zh/j…

LockSupport 是什么?

解决了线程等待唤醒机制(wait/notify)

核心方法:

LockSupport 中的 park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程

线程唤醒和等待的方法

三种让线程等待和唤醒的方法

方式1: 使用 Object 中的 wait() 方法让线程等待,使用 Object 中的 notify() 方法唤醒线程。

方式2: 使用 JUC 包中 Condition 的 await() 方法让线程等待,使用 signal() 方法唤醒线程。

方式3: LockSupport 类可以阻塞当前线程以及唤醒指定被阻塞的线程。

方式1: Object 类中的 wait 和 notify 方法实现线程等待和唤醒

试验代码:

public class LockSupportDemo {

    static Object objectLock = new Object();

    public static void main(String[] args) {
        // 1、wait、notify 需要配合 synchronized 使用
        new Thread(() -> {
            // 2、唤醒线程只能唤醒当前 wait 的线程
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (objectLock) {
                System.out.println(Thread.currentThread().getName() + "\t =======> 进入锁");
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t =======> 被唤醒");
            }
        }, "A").start();

        new Thread(() -> {
            synchronized (objectLock) {
                objectLock.notify();
                System.out.println(Thread.currentThread().getName() + "\t =======> 发起通知");
            }
        }, "B").start();
    }
}

总结:

1、wait、notify 需要配合 synchronized 使用不然会抛出异常

结论:Object 类中的 wait、notify 、notifyAll 用与线程等待和唤醒的方法,都必须要在 synchronized 内部执行(必须要使用 synchronized 关键在)


2、需要先阻塞后唤醒

/**
 * 要求:t1 线程等待 3 秒,3 秒后, t2 线程唤醒 t1 线程继续工作
 *
 * 将 notify 放在 wait 方法之前执行,t1 先 notify 了, 3 秒钟后 t2 线程再执行 wait 方法
 * 现象:
 *   程序一直无法结束
 * 结论:
 *   先 wait 后 notify 、notifyAll方法、等待中的线程才会被唤醒,否则无法被唤醒
 */
public class LockSupportDemo {

    static Object objectLock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            // 2、唤醒线程只能唤醒当前 wait 的线程
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (objectLock) {
                System.out.println(Thread.currentThread().getName() + "\t =======> 进入锁");
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t =======> 被唤醒");
            }
        }, "A").start();

        new Thread(() -> {
            synchronized (objectLock) {
                objectLock.notify();
                System.out.println(Thread.currentThread().getName() + "\t =======> 发起通知");
            }
        }, "B").start();
    }
}

方式2: Condition 接口中的 await 后 singnal 方法实现线程的等待和唤醒

试验代码:

public class LockSupportConditionDemo {

    static Lock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    public static void main(String[] args) {
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t ====== 进入锁");
                condition.await();
                System.out.println(Thread.currentThread().getName() + "\t ====== 被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "A").start();

        new Thread(() -> {
            lock.lock();
            try {
                condition.signal();
                System.out.println(Thread.currentThread().getName() + "\t ====== 通知");
            } finally {
                lock.unlock();
            }
        }, "B").start();
    }
}

结论:

1、condition 需要配合 Lock 一起使用。

2、需要先阻塞后唤醒

注:方式3 后续单独说明

传统的 synchronized 和 Lock 实现等待唤醒通知的约束

1、线程需要先获得并且持有锁,必须在锁块(synchronized 或 lock)中

2、必须要先等待后唤醒,线程才能够被唤醒

LockSupport 类中的 park 等待和 unpark 唤醒

是什么?

通过 park() 和 unpark() 犯法来实现阻塞和唤醒线程的操作

LockSupport 类使用了一种名为 Permit ( 许可) 的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),

permit 有两个之 1 和 0 , 默认是 0。

可以把许可堪称是一种 (0,1)信号量(Semaphore), 但与 Semaphore 不同的是,许可的累加上限是 1。

主要的方法

核心方法:

核心方法 park()/park(Object blocker)

阻塞当前线程/阻塞传入的具体线程

public static void park() {
    UNSAFE.park(false, 0L);
}

permit 默认是 0 ,所以一开始就调用 park() 方法, 当前线程就会阻塞, 知道别的线程将当前线程的 permit 设置为 1 时, park 方法会被唤醒, 然后会将 permit 再次设置为 0 并返回。

核心方法 unpark(Thread thread)

唤醒处于阻塞状态的指定线程

public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

代码实践

Thread a = new Thread(() -> {
    System.out.println(Thread.currentThread().getName() + " \t ======= 进入锁");
    LockSupport.park();
    System.out.println(Thread.currentThread().getName() + "\t ======== 被唤醒");
}, "A");
a.start();

TimeUnit.SECONDS.sleep(3);

Thread b = new Thread(() -> {
    LockSupport.unpark(a);
    System.out.println(Thread.currentThread().getName() + "\t ======== 通知了");
}, "A");
b.start();

试验结论:

1、支持无锁的情况调用,执行线程的阻塞;

2、支持先 unpark , 然后 park 操作依然有效。

重点说明

LockSupport 是用来创建锁和其他同步类的基本线程阻塞原语。

LockSupport 是一个线程阻塞工具类, 所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。

归根结底, LockSupport 调用 Unsafe 的 native 代码

LockSupport 提供 park() 和 unpark() 方法实现阻塞吓成和解除线程阻塞的过程。

LockSupport 和每个使用它的线程都有一个许可(permit)关联。permit 相当于 1, 0 的开关,默认是0,

调用一次 unpark 就加 1 变成 1。

调用一次 park 会消费 permit , 也就是将 1 变成 0, 同时 park 立即返回。

如果再次调用 park 就会变成阻塞(因为 permit 为 0 了会阻塞在这里,直到 permit 变为 1),这时候调用 unpark 会把 permit 设置为 1。每个线程都有一个相关的 permit, permit 最多只有一个, 重复调用 unpark 也不会累积凭证。

形象的理解

线程阻塞需要消耗凭证(permit), 这个凭证最多只有 1个

当调用 park 方法时

  • 如果有凭证,则会直接消耗掉这个凭证然后正常退出。
  • 如果无凭证,就必须阻塞等待凭证可用。

而 unpark 则相反,它会增加一个凭证,但凭证最多只能有 1 个,累加无效。

问题总结

为什么可以先唤醒线程后阻塞线程?

因为 unpark 获取到一个凭证,之后在调用 park 方法,就可以名正言顺的凭证消费,故不会阻塞。

为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

因为凭证的数量最多为 1 ,连续两次调用 unpark 和调用一次 unpark 效果一样,只会增加一个凭证: 而调用两次 park 却需要消费两个凭证,证不够,不能放行。