侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 285 篇文章
  • 累计创建 125 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

Java并发编程之LookSupport原理及实战

孔子说JAVA
2022-07-28 / 0 评论 / 0 点赞 / 51 阅读 / 7,212 字 / 正在检测是否收录...

java.util.concurrent 包里的源码频繁使用的 LockSupport 是一个线程阻塞的工具类,是用来创建锁和其他同步类的基本线程阻塞原语。如 AQS 的底层实现就用到了 LockSupport.park()方法和 LockSupport.unpark()方法。该类中的所有方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的解锁方法,底层调用了Unsafe中的native方法。主要核心API为 park 和 unpark 方法,分别用来阻塞线程和解除阻塞线程。

1、线程等待和唤醒的方式

1.1 方式

java多线程中,常用的线程等待和唤醒的方式有3种。

  • 使用Object类中的 wait 和 notify 方法进行线程的等待和唤醒。
  • 使用JUC中的 Conditon 的 await 和 singal 方法进行线程的等待和唤醒。
  • 使用 LockSupport 的 park 和 unpark 方法阻塞唤醒当前线程。

1.2 区别

上述三种线程等待唤醒方式的区别如下:

Object的 wait 和 notify

  1. 必须使用 sycnchronized 关键字包裹
  2. wait必须要写在synchronized代码块中,否则会报异常。
  3. 必须由同一个lock对象调用wait、notify方法
  4. 如果 notify 放在 wait 之前,线程无法被唤醒,必须先 wait 后 notify。

JUC中的 Condition的 await 和 singal

  1. 必须使用 lock 和 unlock 方法包裹
  2. 如果先使用 singal 再 await,线程无法被唤醒,必须先 await 后 singal

LockSupport 的 park 和 unpark方法

  1. LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,提供了一个Permit语序的概念来做到阻塞和唤醒线程功能。每个线程都有一个Permit,Permit是一个0/1的信号量,只用0和1两个值,默认是0。与Semaphore不同的是,许可的累加上限是1。
  2. 核心API park()/park(Object blocker)、unpark(Thread thread)分别用于阻塞和唤醒线程
  3. 没有锁代码块的要求
  4. 先unpark后park也可以正常执行
  • Object 的 wait()/notify 方法需要获取到对象锁之后在同步代码块里才能调用。
  • 而 LockSupport 不需要获取锁。所以使用 LockSupport 线程间不需要维护一个共享的同步对象,从而实现了线程间的解耦。
  • unark 方法可提前 park 方法调用,所以不需要担心线程间执行的先后顺序,而 wait & notify 不能先 notify。
  • park 和 unpark 的使用不会出现死锁的情况
  • unpark 可以先于 park 执行,就相当于给这个线程一块免死金牌,但是只有一块,就算提前 unpark 了 100 次也只有一块。有了免死金牌之后,第一次 park 这个线程,该线程是不会被 park 住,只是收走了这块免死金牌,下一次park,该线程就进入等待状态了。
  • 多次调用 unpark 方法和调用一次 unpark 方法效果一样,因为 unpark 方法是直接将_counter 赋值为 1,而不是加 1。
  • 许可不可重入,也就是说只能调用一次 park()方法,如果多次调用 park()线程会一直阻塞。
  • park & unpark 是以线程为单位来【阻塞】和 【唤醒】,notify 则是随机唤醒一个等待的线程,notifyAll 是唤醒所有等待线程,不精确。
  • 两者的阻塞队列并不交叉。object.notifyAll()不能唤醒LockSupport的阻塞Thread。
  • 暂停线程的机制也不一样,wait 是让线程在monitor 的 waitSet 中等待,而LockSupport 是与被park的线程的互斥量进行控制。

2、LockSupport原理介绍

原理:

每个java 线程都有一个Parker实例,parker实例的_counter字段,映射了操作系统底层的互斥量(mutex)。使用它来进行线程的并发控制。

LockSupport和每个使用它的线程都与一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit, 也就是将1变成0,同时park立即返回。再次调用park会变成block(因为permit为0了,会阻塞在这里,直到permit变为1), 这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累。

特点:

  1. LockSupport是用来创建锁和其他同步类的基本线程阻塞原语LockSupport是一个线程阻塞的工具类
  2. 所有方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的解锁方法
  3. 底层调用了Unsafe中的native方法
  4. 每一个使用它的线程都有一个许可证Permit,调用一次unpark就+1,调用一次park会消费permit,将1变成0.同时park立刻返回,如果再调用一次park会变成阻塞(permit为0会阻塞在这里,一直到permit变成1)
  5. 每个线程都有相关的permit最多一个,重复调用unpark也不会累积凭证
  6. 调用park方法,如果有凭证则会消耗掉这个凭证然后正常退出,如果无凭证,就必阻塞等待凭证可用,而unpark相反,他会添加一个凭证,但凭证最多只有1个累加无效

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

因为unpark提供了一个凭证,Permit=1,之后再调用park,Permit变成0,正常的凭证消费则不会阻塞

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

因为Permit最多数量是1,连续调用2次unpark方法,结果Permit的值还是1,调用第一次park方法将Permit改为0,第二次调用 park 时 Permit还是0,故阻塞。

3、LockSupprot 方法介绍

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

阻塞线程的方法:

void park():阻塞当前线程,如果调用 unpark 方法或者当前线程被中断,才能从 park()方法中返回
void park(Object blocker):功能同方法 1,入参增加一个 Object 对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;
void parkNanos(long nanos):阻塞当前线程,最长不超过 nanos 纳秒,增加了超时返回的特性;
void parkNanos(Object blocker, long nanos):功能同方法 3,入参增加一个 Object 对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;
void parkUntil(long deadline):阻塞当前线程,直到 deadline;
void parkUntil(Object blocker, long deadline):功能同方法 5,入参增加一个 Object 对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;

每个 park 方法都对应有一个带有 Object 阻塞对象的重载方法。增加了一个 Object 对象作为参数,此对象在线程受阻塞时被记录,以允许监视工具和诊断工具确定线程受阻塞的原因。

唤醒线程的方法:

void unpark(Thread thread):唤醒处于阻塞状态的指定线程

简单示例:

public class LockSupportDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            LockSupport.park();
            System.out.println("thread线程被唤醒");
        });
        thread.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LockSupport.unpark(thread);
    }
}

该示例中,首先thread线程会被 park 阻塞,主线程在睡眠3秒之后,执行unpark(thread)方法,这时候thread线程 被唤醒,输出"thread线程被唤醒"。

4、LockSupport实战示例

4.1 基本使用示例

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class LockSupportDemo {

    public static void main(String[] args) {
        simpleLockSupport();
    }


    /**
     * lockSupport最基本的使用
     */
    private static void simpleLockSupport() {
        Thread t1 = new Thread("A") {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " 进入 !");
                LockSupport.park();
                System.out.println(Thread.currentThread().getName() + " 被唤醒 !");
            }
        };
        t1.start();
        Thread t2 = new Thread("B") {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " 进入 !");
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                }
                LockSupport.unpark(t1);
                System.out.println(Thread.currentThread().getName() + " 唤醒 线程A !");
            }
        };
        t2.start();
    }
}

image-1658883820914

4.2 unpark调用时机

需要注意的是unpark方法如果在线程启动之前调用没有任何作用,在线程启动之后unpark,然后再调用park。可以正常解锁。因为unpark本质把permit改成了1,此时调用park正常消费。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class LockSupportDemo {

    public static void main(String[] args) {
        unparkBeforePark();
    }

    /**
     * unpark必须在 线程启动之后调用 如果线程启动之前调用 unpark没有效果
     * unpark在线程启动之后先调用了 再调用park 可以正常解锁,因为unpark本质把permit改成了1 调用park正常消费
     */
    private static void unparkBeforePark() {
        Thread t1 = new Thread("A") {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " before park !");
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                }
                LockSupport.park();
                System.out.println(Thread.currentThread().getName() + " after park !");
            }
        };
        // 线程启动之前先unpark 没有任何效果 park还是阻塞
//        LockSupport.unpark(t1);
        System.out.println("start");
        t1.start();
        // 线程启动之后先调用unpark,线程里面 park 可以正常执行完
        LockSupport.unpark(t1);
    }
}

image-1658884051170

5、LockSupport与wait&notify比较示例

使用LockSupport与wait&notify实现同样逻辑的代码:

  • 线程A执行一段业务逻辑后调用wait/park阻塞住自己。主线程调用notify/unpark方法唤醒线程A,线程A然后打印自己执行的结果。

5.1 wait&notify方式

import java.util.concurrent.locks.LockSupport;

/**
 * LockSupport与wait&notify的使用对比
 */
public class LockSupportAndWait {
    public static void main(String[] args) throws Exception {
        waitNotifyDemo();
    }

    // wait&notify示例
    public static void waitNotifyDemo() throws Exception {
        final Object obj = new Object();
        Thread A = new Thread(() -> {
            int sum = 0;
            for (int i = 0; i < 10; i++) {
                sum += i;
            }
            try {
                // wait和notify/notifyAll方法只能在同步代码块里用, 下述代码会报java.lang.IllegalMonitorStateException
//                obj.wait();
                // 使用synchronized包裹wait方法才是正确的
                synchronized (obj) {
                    obj.wait();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("waitNotify方式:sum = " + sum);
        });
        A.start();
        //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
        Thread.sleep(1000);
        synchronized (obj) {
            obj.notify();
        }
    }
}
  • 注意:wait和notify/notifyAll方法只能在同步代码块里用(在synchronized代码块内部),否则会报 java.lang.IllegalMonitorStateException 异常。

5.2 LockSupport方式

import java.util.concurrent.locks.LockSupport;

/**
 * LockSupport与wait&notify的使用对比
 */
public class LockSupportAndWait {
    public static void main(String[] args) throws Exception {
        lockSupportDemo();
    }

    // lockSupport示例
    public static void lockSupportDemo() throws Exception {
        Thread A = new Thread(() -> {
            int sum = 0;
            for (int i = 0; i < 10; i++) {
                sum += i;
            }
            LockSupport.park();
            System.out.println("LockSupport方式:sum = " + sum);
        });
        A.start();
        //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
        Thread.sleep(1000);
        LockSupport.unpark(A);
    }
}

可以看出,LockSupport使用不需要在同步代码块里,线程间不需要维护一个共享的同步对象,实现了线程间的解耦。比Object的wait/notify方式简单很多。

5.3 wait&notify和Locksupport去掉sleep后的对比

仔细观察上述2个示例,可以发现在主线程唤醒线程之前都有一个睡眠操作 Thread.sleep(1000);,主线程调用了Thread.sleep(1000)方法主要用来等待线程A计算完成进入wait状态。

waitNotifyDemo去掉Thread.sleep()调用后:

多次执行后,我们会发现:有的时候能够正常打印结果并退出程序,但有的时候线程无法打印结果阻塞住了。原因就在于主线程调用完notify后,线程A才进入wait方法,导致线程A一直阻塞住。由于线程A不是后台/精灵线程,所以整个程序无法退出。

Locksupport去掉Thread.sleep()调用后:

我们会发现:不管你执行多少次,这段代码都能正常打印结果并退出。LockSupport支持主线程先调用unpark后,线程A再调用park而不被阻塞。这就是LockSupport最大的灵活所在。

6、总结

LockSupport比Object的wait/notify有两大优势:

  • LockSupport不需要在同步代码块里,所以线程间也不需要维护一个共享的同步对象,实现了线程间的解耦。
  • unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序,park和unpark的使用不会出现死锁的情况。
  • park和unpark可以实现类似wait和notify的功能,但是并不和wait和notify交叉,也就是说unpark不会对wait起作用,notify也不会对park起作用。
0

评论区