JUC 七. LockSupport 线程的阻塞与唤醒

一. LockSupport 基础

  1. LockSupport是什么: 是用来创建锁和其它同步类的基本线程阻塞原语,LockSupport中有两个方法park() 和unpark()

park(): 阻塞线程
unpark(): 解除阻塞线程

  1. 简单来说: LockSupport 类,可以阻塞当前线程以及唤醒指定被阻塞的线程
    在这里插入图片描述

二. 阻塞与唤醒线程的几种方式

wait() 与 notify() 方式

  1. wait() 与 notify() 方法讲解
  • wati() 与 notify() 是 Object 中提供的方法,
  • wati() 与 notify() 使用前提是在 synchronized 中
  • wati() 表示休眠并释放锁资源, notify() 表示唤醒当前对象监视器上(同一锁下的)其它休眠线程
  1. wait() 与 join() 的区别: wait() 用在 synchronized 中, 被 wait() 的线程需要唤醒, 在一个方法中调用了 join(),表示当前线程进入阻塞状态,并且在其它线程执行完毕后才会自动唤醒执行,没有锁的概念
  2. wait() 与 sleep() 的区别: sleep 指定休眠时间,时间过后自动唤醒,并且sleep不会释放锁
  3. notify() 与 yield()区别: yield() 使当前线程重新回到可执行状态,但是并不会马上执行,没有锁的概念,notify() 是唤醒被 wait() 的线程,与 wait() 配合使用的
  4. 示例代码: testWaitNotify() 方法中存在两个线程t1与t2, t1执行时会阻塞, t2会唤醒同一个监视器上的其它阻塞线程
	//wait与notify是监视器的(锁),声明锁对象
	private Object syLock = new Object();

    public void testWaitNotify() {
        //1.t1线程中会阻塞
        new Thread(() -> {
            //3.注意点: t2线程中唤醒当前t1线程的阻塞,假设t2唤醒时t1线程还没阻塞,会出现什么情况
            try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
            synchronized (syLock) {
                try {
                    System.out.println(Thread.currentThread().getName() + "线程阻塞执行");
                    syLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "线程被t2中唤醒");
            }
        }, "t1").start();

        //2.t2.线程中会唤醒同一个监视器上的其它阻塞线程
        new Thread(() -> {
            synchronized (syLock) {
                syLock.notify();
                System.out.println(Thread.currentThread().getName() + "唤醒t1线程");
            }
        }, "t2").start();
    }

    public static void main(String[] args) {
        LockSupportDemo demo = new LockSupportDemo();
        demo.testWaitNotify();
    }
  1. 根据示例代码总结: wait() 与 notify() 方式 中存在的问题
  1. wait() 与 notify() 是基于同一个监视器的也就是同一把锁,在唤醒时会唤醒同一个监视器上的其它被阻塞线程,所以如果使用该方式必须用synchronized
  2. wait() 阻塞线程, notify() 唤醒线程, 假设notify()在唤醒时,没有线程阻塞,这样可能就会造成,唤醒执行后,线程阻塞了,会一直阻塞, 也就是说: wait() 与 notify() 必须成对出现,并且阻塞唤醒顺序不能错,否则会阻塞

JUC包下 Condition 中的 await() 与 signal() 方式

  1. 使用api

await(): 表示让线程阻塞
signal(): 表示唤醒线程

  1. 示例代码: testCondition()方法中开启了两个线程t1与t2,在t1中会阻塞线程, t2中会唤醒t1线程
	//基于lock, Condition 方式实现阻塞唤醒线程
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void testCondition() {
        //1.t1线程中会阻塞
        new Thread(() -> {
            //3.t2线程中唤醒当前t1线程的阻塞,假设t2唤醒时t1线程还没阻塞,会出现什么情况
            //try { TimeUnit.MILLISECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "线程阻塞执行");
                condition.await();
                System.out.println(Thread.currentThread().getName() + "线程被t2中唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }

        }, "t1").start();

        //2.t2.线程中会唤醒同一个监视器上的其它阻塞线程
        new Thread(() -> {
            lock.lock();
            try {
                condition.signal();
                System.out.println(Thread.currentThread().getName() + "唤醒t1线程");
            }finally {
                lock.unlock();
            }

        }, "t2").start();
    }

    public static void main(String[] args) {
        LockSupportDemo demo = new LockSupportDemo();
        demo.testCondition();
    }
  1. 根据上方使用await()与signal()阻塞唤醒线程示例了解可能存在的问题
  1. 必须有锁否则会报错
  2. 假设signal()唤醒时,其它线程还为执行await()阻塞,会出现问题,进而了解到await()与signal()要成对出现,并且是有顺序的

LockSupport 使用示例与底层原理

  1. 在上面了解到不管 wait() 与 notify() 方式还是await() 与 signal() 方式实现线程的阻塞与唤醒都存在两个问题
  1. 线程要先获取并持有锁,必须在锁(synchronized或Lock)中
  2. 必须要先等待后唤醒,线程才能正常唤醒,程序才能正常执行
  1. LockSupport 类使用一种名为Permit许可证方式来实现线程的阻塞唤醒功能,每个线程都有一个permit许可证, 可以把许可证permit看成是一种信号量Semaphore,但是与信号量不同的是许可的累加上限是1,每个许可证都只有两个值0和1,默认0(底层通过Unsafe实现)
  2. 源码分析: 底层通过调用UNSAFE的park()与unpark(Thrade thread)阻塞唤醒线程,默认permit许可证为0,所以第一次调park()方法时会阻塞线程,调unpark()传递指定线程会将传递线程的permit设置为1,阻塞线程被唤醒执行,并且阻塞线程会再次将permit设置为0
	public static void park() {
        UNSAFE.park(false, 0L);
    }

	public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }
  1. 示例,在testLockSupport()方法中开启了两个线程t1, t1中会执行LockSupport.park()阻塞当前线程执行,t2中会执行LockSupport.unpark(t1) 传递t1的线程对象,唤醒t1线程
	public void testLockSupport() {
        //1.t1线程中会阻塞
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "线程阻塞执行");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "线程被t2中唤醒");
        }, "t1");

        t1.start();

        //2.t2.线程中会唤醒同一个监视器上的其它阻塞线程
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "线程使用LockSupport唤醒t1线程");
            LockSupport.unpark(t1);
        }, "t2").start();
    }

    public static void main(String[] args) {
        LockSupportDemo demo = new LockSupportDemo();
        //demo.testWaitNotify();
        //demo.testCondition();
        demo.testLockSupport();
    }
  1. 根据上面的示例代码,假设在t1线程执行阻塞前手动休眠5秒,先然t2线程执行,会不会出现因为阻塞唤醒的顺序不对造成程序一直阻塞?答案是不会,先执行t2,在t2中设置t1的permit为1,允许通行,然后t1执行发现当前的permit为1,已经被唤醒过了不在阻塞,并将permit设置回0,可以理解为两个抵消掉了

多次阻塞唤醒案例

  1. 上面讲到在使用 LockSupport 设置线程阻塞时,permit 上限是1,那么如果实际案例就是需要某个线程多次阻塞怎么办,需要多次阻塞的线程多次执行park(), 然后通过多个其它线程多次执行 unpark(Thread 需要唤醒的线程)
	public void testLockSupport() {
        //1.t1线程中会阻塞
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "线程阻塞执行");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "线程被唤醒");
            System.out.println(Thread.currentThread().getName() + "线程再次阻塞");
            LockSupport.park();

        }, "t1");

        t1.start();

        //2.t2线程中会唤醒同一个监视器上的其它阻塞线程
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "线程使用LockSupport唤醒t1线程");
            LockSupport.unpark(t1);
        }, "t2").start();

        //3.t3线程再次执行unpark()唤醒t1线程
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "线程使用LockSupport唤醒t1线程");
            LockSupport.unpark(t1);
        }, "t3").start();
    }

三. 总结 LockSupport 的使用

  1. 如何阻塞唤醒线程
  1. wait() 与 notify()方式: Object中提供的方法: 需要在synchronized中使用, wati() 表示休眠并释放锁资源, notify() 表示唤醒当前此对象监视器上(同一锁下的)其它休眠线程
  2. JUC包下 Condition 中的 await() 与 signal() 方式
  1. 以上两种方式的缺点:
  1. wait() 与 notify()方式 需要使用synchronized,
  2. Condition 中的 await() 与 signal() 方式 需要使用 ReentrantLock
  3. 必须成对出现,并且要先阻塞后唤醒是有顺序的
  1. 还有几个基础问题
  1. wait() 与 join() 的区别: wait() 用在 synchronized 中, 被 wait() 的线程需要唤醒, 在一个方法中调用了 join(),表示当前线程进入阻塞状态,并且在其它线程执行完毕后才会自动唤醒执行,没有锁的概念
  2. wait() 与 sleep() 的区别: sleep 指定休眠时间,时间过后自动唤醒,并且sleep不会释放锁
  3. notify() 与 yield()区别: yield() 使当前线程重新回到可执行状态,但是并不会马上执行,没有锁的概念,notify() 是唤醒被 wait() 的线程,与 wait() 配合使用的
  1. 进而提出了阻塞唤醒线程第三种方式: LockSupport 中的 park() 与 unpark(Thread t), 以Permit许可证方式来实现线程的阻塞唤醒功能,底层基于UNSAFE的pack()与UNSAFE的unpark()实现,每个线程都有自己的permit标识,不用非要保证先执行阻塞,后执行唤醒的顺序,线程在执行时会自己进行判断,如果先执行的唤醒,会将被指定唤醒的线程中的permit设置为1,后执行阻塞时发现permit已经为1了,就不回再次阻塞,相当于两次抵消掉了,并且permit许可上限为1,多次累加不变
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值