由于blog各种垃圾评论太多,而且本人审核评论周期较长,所以懒得管理评论了,就把评论功能关闭,有问题可以直接qq骚扰我

LockSupport和线程等待唤醒机制

JAVA 西门飞冰 1152℃
[隐藏]

1.LockSupport是什么

Lock Support从字面意思直接翻译过来是锁的支撑类,是一个改进类。下面就看一下这个技术为什么会出现,解决了那些老技术不方便不灵活的问题。

官方解释:用于创建锁和其他同步类的基本线程阻塞原语。

核心就是park()unpark()方法

  • park()方法是阻塞线程
  • unpark()方法是解除阻塞线程

LockSupport就是对线程等待和唤醒机制的另外一种机制和提升

2.线程等待唤醒机制

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

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

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

3、LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

这三种方法是从1到2在到3的关系,在看Lock Support牛在什么地方之前,先看一下方式一和方式二的实现以及优缺点,在看Lock Support为什么会被提出来,并且被重度使用。

2.1.方式一实现

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

正常代码:

public class LockSupportDemo
{
    public static void main(String[] args)
    {
        Object objectLock = new Object();	 // 同一把锁,类似资源类

        new Thread(() -> {
            synchronized (objectLock) {
                System.out.println(Thread.currentThread().getName()+"\t ---- come in");
                try {
                    objectLock.wait();//----------------------这里先让他等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒了");
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            synchronized (objectLock) {
                objectLock.notify();//-------------------------再唤醒它
                System.out.println(Thread.currentThread().getName()+"\t ---发出通知");
            }
        },"t2").start();
    }
}

异常1:去掉synchronized锁,看看会有什么问题出现

public class LockSupportDemo
{
    public static void main(String[] args)
    {
        Object objectLock = new Object();

        new Thread(() -> {
            //    synchronized (objectLock) {
            System.out.println(Thread.currentThread().getName()+"\t ---- come in");
            try {
                objectLock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //      }
            System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒了");
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }

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

执行结果:出现了监视器不对的异常

t1	 ---- come in
Exception in thread "t1" java.lang.IllegalMonitorStateException: current thread is not owner
	at java.base/java.lang.Object.wait(Native Method)
	at java.base/java.lang.Object.wait(Object.java:338)
	at com.fblinux.JUC.LockSupportDemo.lambda$main$0(LockSupportDemo.java:14)
	at java.base/java.lang.Thread.run(Thread.java:833)
Exception in thread "t2" java.lang.IllegalMonitorStateException: current thread is not owner
	at java.base/java.lang.Object.notify(Native Method)
	at com.fblinux.JUC.LockSupportDemo.lambda$main$1(LockSupportDemo.java:26)
	at java.base/java.lang.Thread.run(Thread.java:833)

我们得到第一个结论,要使用wait和notify必须将它放在synchronized锁块中,也就是必须先持有锁了才能使用。

异常2:把notify和wait的执行顺序对换

public class LockSupportDemo
{
    public static void main(String[] args)
    {
        Object objectLock = new Object();

        new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            synchronized (objectLock) {
                System.out.println(Thread.currentThread().getName()+"\t ---- come in");
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
          
            System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒了");
        },"t1").start();



        new Thread(() -> {
            synchronized (objectLock) {
                objectLock.notify();//这个先执行了
                System.out.println(Thread.currentThread().getName()+"\t ---发出通知");
            }
        },"t2").start();
    }
}

执行结果: 程序一直卡住,并没有正常的退出和后续的执行

t2	 ---发出通知
t1	 ---- come in

我们得到的第二个结论就是wait和notify的顺序是必须要遵守的。

2.2.方式二实现

Condition接口中的await后signal方法实现线程的等待和唤醒

正常代码:

public class LockSupportDemo
{
    public static void main(String[] args)
    {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println(Thread.currentThread().getName()+"\t-----come in");
                condition.await();
                System.out.println(Thread.currentThread().getName()+"\t -----被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            lock.lock();
            try
            {
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"我要进行唤醒");
        },"t2").start();

    }
}

执行结果:

t1	-----come in
t2	我要进行唤醒
t1	 -----被唤醒
  • 异常1原理同上:仍然返回`IllegalMonitorStateException`
  • 异常2 原理同上:仍然在不停的循环

结论:`await`和`notify`类似于上面`wait`和`notify`

  • Condition中的线程等待和唤醒方法,需要先获取锁
  • 一定要先await后signal,不能反了

3.LockSupport实现

后面的技术一定比前面的技术更优秀,它可以搞定前面两个突破不了的限制

3.1.是什么

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

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

  • LockSupport类使用了一种名为Permit(许可) 的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit),
  • permit(许可)只有两个值1和0,默认是0。0 是阻塞,1是唤醒
  • 可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1。

3.2.主要方法

阻塞:park()/park(Object blocker) 阻塞当前线程/阻塞传入的具体线程

permit许可证默认没有不能放行,所以一开始调park()方法当前线程就会阻塞,直到别的线程给当前线程发放permi通行证t,park方法才会被唤醒。

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

调用unpark(thread)方法后,就会将thread线程的许可证permit发放,会自动唤醒park线程,即之前阻塞中的Lock Support.park()方法会立即返回。

3.3.代码

正常代码:可以看到代码比之前的两种实现方式要简洁很多,且无锁块要求

public class LockSupportDemo
{
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t----------come in");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName()+"\t----------被唤醒了");
        },"t1");
        t1.start();

        new Thread(()->{
            LockSupport.unpark(t1);	  // 参数中传递发放通行证的线程名称
            System.out.println(Thread.currentThread().getName()+"\t-----发出通知,去唤醒t1");
        },"t2").start();
    }
}

执行结果:

t1	----------come in
t2	-----发出通知,去唤醒t1
t1	----------被唤醒了

之前错误的先唤醒后等待,LockSupport照样支持

public class LockSupportDemo
{
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
            System.out.println(Thread.currentThread().getName()+"\t----------come in"+"\t"+System.currentTimeMillis());
            LockSupport.park();
            System.out.println(Thread.currentThread().getName()+"\t----------被唤醒了"+"\t"+System.currentTimeMillis());
        },"t1");
        t1.start();

        new Thread(()->{
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName()+"\t-----发出通知,去唤醒t1");
        },"t2").start();
    }
}

执行结果:

t2	-----发出通知,去唤醒t1
t1	----------come in	1675220291150
t1	----------被唤醒了	1675220291151

sleep方法3秒后醒来,执行park无效,没有阻塞效果,解释如下

先执行了unpark(t1)导致上面的park方法形同虚设无效,时间一样

4.LockSupport总结

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

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

LockSupport提供park()和unpack()方法实现阻塞线程和解除线程阻塞的过程

LockSupport和每个使用它的线程都有一个许可(permit)关联

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

形象的理解

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

当调用park方法时

如果有凭证,则会直接消耗掉这个凭证然后正常退出;

如果无凭证,就必须阻塞等待凭证可用;

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

为什么可以突破wait/notify的原有调用顺序?

因为unpark获得了一个凭证,之后在调用park方法,就可以名正言顺的凭证消费,故不会阻塞。先发放了凭证后续可以畅通无阻。

 

转载请注明:西门飞冰的博客 » LockSupport和线程等待唤醒机制

喜欢 (0)or分享 (0)