java juc LockSupport详解

LockSupport是什么

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

LockSupport就是线程等待唤醒机制 wait/notify 的改良加强版

那么问题来了,为什么要加强等待唤醒机制,原来使用的等待唤醒机制有什么缺点

使用synchornized和Lock的等待唤醒机制

1. 使用Object中的 wait() ,notify() 方法等待,唤醒线程

问题1.使用wait和notify的时候 必须包裹在synchronized关键字中

使用wait和notify的时候,添加了关键字 sychronized代码块

public class SyncTest {
    static Object obj = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            //wait方法被包裹在synchronized关键字里面了
            synchronized (obj) {
                try {
                    System.out.println(Thread.currentThread().getName() + "开始执行");
                    obj.wait();
                    System.out.println(Thread.currentThread().getName() + "开始执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }, "AA").start();

        new Thread(() -> {
            // notifyAll方法被包裹在synchronized关键字里面了
            synchronized (obj) {
                System.out.println("AA线程被通知");
                obj.notifyAll();
            }
        }, "BB").start();
    }
}

执行结果如下
在这里插入图片描述

那么问题来了,如果不使用synchronized关键字呢, 将上述synchronized关键字给去掉,可以看到,刚好在执行wait和执行notifyAll方法的时候,出现了 IllegalMonitorStateException异常
在这里插入图片描述

问题2. 在程序执行的时候,如果先执行notify方法,然后执行wait方法,会发生什么呢
public class SyncTest {
    static Object obj = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            //wait方法被包裹在synchronized关键字里面了
          	synchronized (obj) {
                try {
                    System.out.println(Thread.currentThread().getName() + "开始执行");
                    obj.wait();
                    System.out.println(Thread.currentThread().getName() + "开始执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        new Thread(() -> {
            // notifyAll方法被包裹在synchronized关键字里面了
            synchronized (obj) {
                System.out.println("AA线程被通知");
                obj.notifyAll();
            }
        }, "BB").start();
    }
}

执行结果如下,线程进入阻塞状态,一直不能将锁释放, AA线程一直不能被唤醒
在这里插入图片描述

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

问题1 Condition在使用的时候,和synchronized一样也需要被锁住,不然会出错
public class LockTest {

    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() + "开始执行");
                condition.await();
                System.out.println(Thread.currentThread().getName() + "开始执行");

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }, "AA").start();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "执行通知操作");
                condition.signalAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "BB").start();
    }
}

使用锁包裹住的运行结果
在这里插入图片描述
不使用锁包裹住的运行结果
在这里插入图片描述

问题2. 先执行唤醒操作,后执行等待操作
public class LockTest {

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

    public static void main(String[] args) {

        new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "开始执行");
                condition.await();
                System.out.println(Thread.currentThread().getName() + "开始执行");

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }, "AA").start();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "执行通知操作");
                condition.signalAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "BB").start();
    }
}

执行结果如下
在这里插入图片描述

synchornized和Lock的缺点

  1. 必须在同步代码块中完成
  2. 如果先执行唤醒操作,后执行等待操作,会出现死锁现象

LockSupport解决了这写问题

使用LockSupport类中的 park() 方法 让线程阻塞,unpark(Thread thread)方法 唤醒指定被阻塞的线程

public class LockSupportTest {

    public static void main(String[] args) {
        Thread aa = new Thread(() -> {
            //try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            try {
                System.out.println(Thread.currentThread().getName() + "开始执行");
                LockSupport.park();
                System.out.println(Thread.currentThread().getName() + " park后 开始执行");
            } catch (Exception e) {
                e.printStackTrace();
            }

        }, "AA");
        aa.start();

        Thread bb = new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "执行通知操作");
                LockSupport.unpark(aa);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "BB");
        bb.start();
    }
}

不设置等待时间的操作结果,可以看到 LockSupport操作可以直接调用,而不需要在同步代码块中执行
在这里插入图片描述

设置等待时间的操作结果,可以看到,程序并没有出现死锁现象,而是 在程序运行的时候 相当于是把 LockSupport.park(); 这一行命令忽略掉了一样
在这里插入图片描述
可以看到 LockSupport解决了 synchronized和Lock的缺点

LockSupport原理

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

permit 只有两个值 0 和 1,默认值为0

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

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

LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码

LockSupport阻塞线程,解除阻塞线程的过程

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

调用一次unpart就加1 变成1

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

如果再次调用 park 会变成阻塞 ( 因为permit为 0 了 会阻塞在这里,直到permit变成 1 ),这时调用unpark 会吧 permit 置为 1

每一个线程都有一个相关的permit, permit最多只有一个,重复调用 unpark也不会积累凭证

形象理解

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

当调用park方法时

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

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

使用生活实例理解:

park、unpark操作相当于是使用密码解锁不同的门,unpark是重置门的密码 重置了之后不管输入什么都可以开门( 不管重置几次,设置了1次之后 这个门的密码就永久改变了,只能再次重置才可以开锁 ),park就相当于是设置一扇未知密码的门,现在这个门锁 就相当于是一个凭证 permit

举个例子:

例子1

先执行 unpark操作后执行park操作 为什么线程不会阻塞,线程A,线程B执行的时候,先调用 unpark操作 相当于是把门锁的密码给重置了,那么 我park操作的时候,不管怎么样都可以通过了

public class LockSupportTest {

    public static void main(String[] args) {
        Thread aa = new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            try {
                System.out.println(Thread.currentThread().getName() + "开始执行, " + System.currentTimeMillis());
                // 如果先执行的 unpark操作,后执行park操作,这一行就相当于是没有执行一样, 可以根据时间来验证
                LockSupport.park();
                System.out.println(Thread.currentThread().getName() + " park后 开始执行," + System.currentTimeMillis());
            } catch (Exception e) {
                e.printStackTrace();
            }

        }, "AA");
        aa.start();

        Thread bb = new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "执行通知操作");
                LockSupport.unpark(aa);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "BB");
        bb.start();
    }
}

在这里插入图片描述

例子2:连续执行2次park操作,2次unpark操作

A线程开始执行,发现门锁的密码没有被重置,就只能在外面等着

B线程开始执行,首先执行了一次 unpark操作,把门的密码重置了 还没等A线程通过呢;又执行了一次unpark操作又把这一扇门的unpark的密码重置了

A线程现在不管输入什么 都可以通过第一次的那个unpark了

但是问题来了 还有一扇门,没有人可以给我重置密码了,那么我就只能在外面等着了,直到有人给我重置密码,或者有人来把这扇门给我炸了( 程序终止等操作 )

public class LockSupportTest {
    public static void main(String[] args) {
        Thread aa = new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "开始执行, " + System.currentTimeMillis());
                // 如果先执行的 unpark操作,后执行park操作,这一行就相当于是没有执行一样, 可以根据时间来验证
                LockSupport.park();
                LockSupport.park();
                System.out.println(Thread.currentThread().getName() + " park后 开始执行," + System.currentTimeMillis());
            } catch (Exception e) {
                e.printStackTrace();
            }

        }, "AA");
        aa.start();

        Thread bb = new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "执行通知操作");
                LockSupport.unpark(aa);
//                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                LockSupport.unpark(aa);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "BB");
        bb.start();
    }
}

BB线程不等待时的运行结果,程序不会终止
在这里插入图片描述

BB线程停顿1s中的执行结果
在这里插入图片描述

例子3:执行2次park操作,2次unpark操作,在执行unpark操作的时候,在中间停顿1s钟

A线程开始执行,发现门锁的密码没有被重置,就只能在外面等着

B线程开始执行,首先执行了一次 unpark操作,把门的密码重置了,B线程停顿了1S中,那么A线程开始执行

A线程发现 诶第一扇门我顺利通过了,然后发现还有第二扇门,执行等待,B线程又开始执行

B线程停顿了1s种后,再一次调用unpark操作,把第二扇门的密码给重置了

A 美滋滋,第一扇门,第二扇门的密码都给我重置了,就可以顺利通过了

面试题

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

先唤醒线程相当于是发放了一个凭证,那么我调用park的时候 有这个凭证 就可以直接使用了 而不需要进入阻塞状态

为什么唤醒2次后 阻塞2次,但最终结果还是阻塞状态

这个还是要分情况讨论的,如果 unpark是2次连续执行,就会进入阻塞状态,如果中间停顿了1s,就不会进入阻塞状态

原因使用中的实例了来理解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值