JUC并发编程基础篇第六章之LockSupport[notify,signal之外的另一种唤醒方式]

发布时间 2023-04-07 15:40:47作者: 爱吃糖的靓仔

1、LockSupport有什么用

一般情况下,我们们有如下3种办法去唤醒一个线程

  1. 使用object方法的wait()方法,让线程等待;使用object的notify()方法进行唤醒
  2. 使用juc包中的condition的await()方法让线程等待,使用signal()方法唤醒线程
  3. LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

接下来将进行一一的讲解

2、使用wait和notify唤醒一个线程

2.1、正常情况

public class InterruputDemo1 {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        new Thread(() -> {
            synchronized (o) {
                System.out.println(Thread.currentThread().getName() + "----coming in");
                try {
                    o.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "好的,多谢通知 ,我不等待了,");
            }
        }, "t1").start();

        Thread.sleep(1000);

        new Thread(() -> {
            synchronized (o) {
                o.notify();
                System.out.println("解锁了,不用等待了");
            }
        },"t2").start();
    }
}

2.2、异常情况2 ,这里去掉了 synchronized (o) {} 代码块

public class InterruputDemo1 {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "----coming in");
                try {
                    o.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "好的,多谢通知 ,我不等待了,");
        }, "t1").start();

        Thread.sleep(1000);

        new Thread(() -> {
                o.notify();
                System.out.println("解锁了,不用等待了");
        },"t2").start();
    }
}

会出现如下的错误

t1----coming in
Exception in thread "t1" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at com.test.InterruputDemo1.lambda$main$0(InterruputDemo1.java:10)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "t2" java.lang.IllegalMonitorStateException
	at java.lang.Object.notify(Native Method)
	at com.test.InterruputDemo1.lambda$main$1(InterruputDemo1.java:21)
	at java.lang.Thread.run(Thread.java:748)

结论: wait和notify 必须要配合synchronized

2.3、异常情况3 先notify再wait

 public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "----coming in");
                try {
                    synchronized (o) {
                        o.wait();
                    }
                    System.out.println(Thread.currentThread().getName() + "好的,多谢通知 ,我不等待了,");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        }, "t1").start();

        Thread.sleep(1000);

        new Thread(() -> {
            synchronized (o) {
                o.notify();
                System.out.println("解锁了,不用等待了");
            }
        },"t2").start();
    }

最后输出的结果是

解锁了,不用等待了
t1----coming in
xxxxx  程序没有停止 ,仍在执行!!!!

总结: wait和notify必须放到同步代码块和方法里面,且成对出现, 先wait后notify才行

3、使用await和signal唤醒一个线程

3.1、正常情况

 public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(()->{
            lock.lock();
            System.out.println("我等待被唤醒");
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            System.out.println("被唤醒");
        },"t1").start();

        Thread.sleep(1000);

        new Thread(()->{
            lock.lock();
            condition.signal();
            System.out.println("你被释放了");
            lock.unlock();
        },"t2").start();
    }

3.2、异常情况: 如果去除锁块

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(()->{
//            lock.lock();
            System.out.println("我等待被唤醒");
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
//                lock.unlock();
            }
            System.out.println("被唤醒");
        },"t1").start();

        Thread.sleep(1000);

        new Thread(()->{
//            lock.lock();
            condition.signal();
            System.out.println("你被释放了");
//            lock.unlock();
        },"t2").start();
    }

输出结果为

我等待被唤醒
Exception in thread "t1" java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.fullyRelease(AbstractQueuedSynchronizer.java:1723)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2036)
	at com.test.InterruputDemo1.lambda$main$0(InterruputDemo1.java:17)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "t2" java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.signal(AbstractQueuedSynchronizer.java:1939)
	at com.test.InterruputDemo1.lambda$main$1(InterruputDemo1.java:30)
	at java.lang.Thread.run(Thread.java:748)

3.3、异常情况: 先执行signal后await

public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(()->{
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.lock();
            System.out.println("我等待被唤醒");
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            System.out.println("被唤醒");
        },"t1").start();

        Thread.sleep(1000);

        new Thread(()->{
            lock.lock();
            condition.signal();
            System.out.println("你被释放了");
            lock.unlock();
        },"t2").start();
    }

输出结果

你被释放了
我等待被唤醒
xxxxx  程序没有停止 ,仍在执行!!!!

总结: Condition中的线程等待和唤醒都需要先获取锁,一定要先await,然后signal,顺序不能反

4、LockSupport的park等待和unpark唤醒

LockSupport类使用了一种名叫Permit的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可,但是与Semaphore不同,许可的累加上限是1

4.1、正常代码

  public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "---->com in");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "---->被唤醒了");
        }, "t1");
        t1.start();

        Thread.sleep(1000);

        new Thread(()->{
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName()+"---->我来唤醒你");
        },"t2").start();
    }

输出结果

t1---->com in
t2---->我来唤醒你
t1---->被唤醒了

4.2、先执行unPark 后执行park

public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "---->com in");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "---->被唤醒了");
        }, "t1");
        t1.start();

        new Thread(()->{
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName()+"---->我来唤醒你");
        },"t2").start();
    }

输出结果

t2---->我来唤醒你
t1---->com in
t1---->被唤醒了

重要说明:LockSupportL的许可累加是1

public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "---->com in");
            LockSupport.park();
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "---->被唤醒了");
        }, "t1");
        t1.start();

        new Thread(()->{
            LockSupport.unpark(t1);
            LockSupport.unpark(t1);
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName()+"---->我来唤醒你");
        },"t2").start();
    }

输出结果

t2---->我来唤醒你
t1---->com in
xxxx 程序没有停止运行!!!

虽然unpark是一个生成一个许可证,但是他的上限只有1,只能通过1park,后面就无法通过了;

5、面试题

5.1、问题1: 为什么LockSupport可以突破wait和notify的原有的调用顺序

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

5.2、问题2: 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

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