详解LockSupport原理、使用场景及面试题

本文介绍了LockSupport类在Java并发编程中的关键作用,包括park()和unpark()方法的工作原理,以及它们与synchronized、wait/notify和Condition接口的对比。通过实例展示了如何避免常见问题,并强调了LockSupport在简化代码和提高性能方面的优势。

1、LockSupport是什么

  • LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
  • LockSupport中的park()和 unpark()的作用分别是阻塞线程和解除阻塞线程。
  • 3种让线程等待和唤醒的方式:

方式1:使用Object中的wait()方法让线程等待,使用object中的notify()方法唤醒线程
方式2:使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程(可以理解为方式1和方式2的加强版)

2、waitNotify和awaitSignal的限制

  • synchronized -> wait->notify实现线程之间的等待唤醒
import java.util.concurrent.locks.LockSupport;

//   synchronized -> wait->notify
//   lock -> await -> signal
public class LockSupportTest {
    private static Object objectLock = new Object();

    public static void main(String[] args) {

       new Thread(()->{
           synchronized (objectLock){
               System.out.println(Thread.currentThread().getName() + "\t" + "coming in ....");

               try {
                   objectLock.wait();
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
               System.out.println(Thread.currentThread().getName() + "\t" + "被唤醒了 ....");
           }
       },"thA").start();

       new Thread(()->{
           synchronized (objectLock){
               System.out.println(Thread.currentThread().getName() + "\t" + "唤醒其他线程 ....");
               objectLock.notify();
           }

       },"thB").start();

    }
}

输出结果:

thA	coming in ....
thB	唤醒其他线程 ....
thA	被唤醒了 ....

如果让thA暂停三秒钟,让thB先获取锁objectLock,notify方法用来唤醒其他线程(实则没有任何线程等待), 再让thA获取锁,wait方法让thA线程等待,而thA不被唤醒,会一直处于等待/阻塞状态,即"thA 被唤醒了 …”一直不会输出。

  • 调用顺序要先wait,在notify才能正常地实现线程之间的通知和唤醒。
    在这里插入图片描述
    还需要注意的是:
  • wait和notify方法必须要在同步块或者方法里面且成对出现使用,否则会抛出java.lang.IllegalMonitorStateException。
    在这里插入图片描述
  • 实现线程的等待和唤醒,Condition接口中的await和signal方法,同前面使用Object类中的wait和notify方法来实现,其效果大致相同。
  • 都必须要先等待(wait/await),再唤醒(notify/signal);等待和唤醒方法必须要在同步块或者方法里面且成对出现使用,否则会抛出java.lang.IllegalMonitorStateException
    private static Condition condition = lock.newCondition();
    private static Object objectLock = new Object();
    public static void main(String[] args) {

        new Thread(() -> {
            // 当前线程睡眠三秒钟,让thB先执行
            // try {
           //      TimeUnit.SECONDS.sleep(3);
           //  } catch (InterruptedException e) {
           //     throw new RuntimeException(e);
           //  }
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t" + "coming in ....");
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "\t" + "被唤醒了 ....");
            } catch (RuntimeException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }, "thA").start();

        new Thread(() -> {
            lock.lock();
            try {
                condition.signal();
                System.out.println(Thread.currentThread().getName() + "\t" + "唤醒其他线程 ....");
            } finally {
                lock.unlock();
            }

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

使用LockSupport可以解决如上两个问题,也会使代码更为简练。

3、LockSupport原理分析

  • LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法
  • LockSupport分别使用park、unpark实现等待和唤醒,见官网原话:

Basic thread blocking primitives for creating locks and other synchronization classes.
This class associates, with each thread that uses it, a permit (in the sense of the Semaphore class). A call to park will return immediately if the permit is available, consuming it in the process; otherwise it may block. A call to unpark makes the permit available, if it was not already available. (Unlike with Semaphores though, permits do not accumulate. There is at most one.) link

其解读如下:

  • LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
  • LockSupport类使用了Permit(许可证)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和零,默认是零
  • 许可看成是一种(0和1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1
  • 通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作,此外,park()+park(Object blocker) 可以用来阻塞当前线程阻塞传入的具体线程

LockSupport 源码摘要如下:

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

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }
 }
  • park和unpark方法都需要调用Unsafe类中native修饰的方法,底层交由C语言去实现
  • permit默认是0,调用park()方法,当前线程th1就会阻塞,直到别的线程th2调用unpark(Thread thread),将当前线程th1的permit设置为1时(表示当前线程获取到许可),th1会被th2唤醒,然后会将permit再次设置为0并返回。
  • 调用unpark(thread)方法后,就会将th1线程的许可permit设置成1(注意多次调用unpark方法,不会累加,pemit值还是1),使得th1线程被唤醒。

4、LockSupport使用案例

 public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            // 当前线程睡眠三秒钟,让thB先执行
            //try {
            //     TimeUnit.SECONDS.sleep(3);
            //} catch (InterruptedException e) {
            //     throw new RuntimeException(e);
            //}
            System.out.println(Thread.currentThread().getName() + "\t" + "coming in ....");
            LockSupport.park();
                /* 如果这里有两个LockSupport.park(),由于permit的值为1,
                上一行已经使用了permit,所以下一行被注释的打开会导致程序处于一直等待       的状态 * */
            //LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "\t" + "被唤醒了 ....");
        }, "thA");

        threadA.start();

        Thread threadB = new Thread(() -> {
            //try {
            //    TimeUnit.SECONDS.sleep(1);
            //} catch (InterruptedException e) {
            //    throw new RuntimeException(e);
            //}
               //如果有两个LockSupport.unpark(t1),由于permit的值最大为1,所以只能给park一个通行证
            //  LockSupport.unpark(threadA);
            LockSupport.unpark(threadA);
            System.out.println(Thread.currentThread().getName() + "\t" + "唤醒其他线程 ....");
        }, "thB");
        threadB.start();
   }

运行如上程序,即使不加锁,也不会出现异常 IllegalMonitorStateException,也不需要关心线程之间等待和唤醒的先后顺序。

5、总结

  • 没有使用LockSupport的等待唤醒通知机制必须synchronized里面执行wait和notify,在lock里面执行await和signal,这上面这两个都必须要持有锁才能干
  • LockSupport:俗称锁中断,LockSupport 解决了 synchronized 和 lock 的痛点
  • LockSupport不用持有锁块,不用加锁,程序性能好,无须注意唤醒和阻塞的先后顺序,不容易导致卡死

形象理解LockSupport:

  • 线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。
  • 当调用park方法时:
    如果有凭证,则会直接消耗掉这个凭证然后正常退出
    如果无凭证,就必须阻塞等待凭证可用
  • 而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无放。

6、LockSupport面试题

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

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

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

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值