【JUC】线程通信与等待唤醒机制

文章目录

    • 1. 线程通信
    • 2. Object类中的wait和notify方法实现等待和唤醒
    • 3. Condition接口中的await和signal方法实现等待和唤醒
    • 4. LockSupport实现等待和唤醒
      • 4.1 优点

1. 线程通信

多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同,于是这些线程之间就存在通信问题,称为线程间通信。

比如:生产者消费者问题。

当多个线程间存在通信问题时,我们希望它们能有规律地执行,因此就需要一些协调手段,其中,等待唤醒机制就是协调线程间通信的一种有效手段。

2. Object类中的wait和notify方法实现等待和唤醒

  • wait和notify方法必须在同步块或者方法里面使用,且成对出现
  • 必须先wait后notify才OK
public static void main(String[] args) {
    Object monitor = new Object();
    new Thread(() -> {
        synchronized (monitor) {
            System.out.println("线程1执行");
            try {
                monitor.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }).start();
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (Exception e) {
        e.printStackTrace();
    }

    new Thread(() -> {
        System.out.println("线程2执行");
        synchronized (monitor) {
            monitor.notify();
        }
    }).start();
}

3. Condition接口中的await和signal方法实现等待和唤醒

  • Condition中的线程等待和唤醒方法,需要先获取锁
  • 一定要先await后signal
public static void main(String[] args) {
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    new Thread(() -> {
        System.out.println("线程1执行");
        lock.lock();
        try{
            condition.await();
            System.out.println("线程1被唤醒");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }).start();
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (Exception e) {
        e.printStackTrace();
    }

    new Thread(() -> {
        System.out.println("线程2执行");
        lock.lock();
        try{
            condition.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }).start();
}

4. LockSupport实现等待和唤醒

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

  • park():permit许可证默认没有,所以一开始调用park()方法当前线程就会阻塞,直到别的线程给该线程发放permit,该线程才会被唤醒
  • unpark(thread):发放permit许可证给对应线程thread
  • 满足 正常阻塞唤醒要求,无锁块要求;且支持先唤醒后等待
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        System.out.println("线程1执行");
        LockSupport.park();
        System.out.println("线程1被唤醒");
    });
    t1.start();
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (Exception e) {
        e.printStackTrace();
    }

    new Thread(() -> {
        System.out.println("线程2执行");
        LockSupport.unpark(t1);
    }).start();
}

4.1 优点

为什么推荐使用LockSupport来做线程的阻塞与唤醒(线程间协同工作),因为它具备如下优点

  • 以线程为操作对象更符合阻塞线程的直观语义
  • 操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程)
  • 无需竞争锁对象(以线程作为操作对象),不会因竞争锁对象产生死锁问题
  • unparkpark没有严格的执行顺序,不会因执行顺序引起死锁问题,比如「Thread.suspendThread.resume」没按照严格顺序执行,就会产生死锁

另外LockSupport还提供了park的重载函数,提升灵活性

  • void parkNanos(long nanos):增加了超时机制
  • void parkUntil(long deadline):加入超时机制(指定到某个时间点,1970年到指定时间点的毫秒数)
  • void park(Object blocker):设置blocker对象,当线程没有许可证被阻塞时,该对象会被记录到该线程的内部,方便后续使用诊断工具进行问题排查
  • void parkNanos(Object blocker, long nanos):设置blocker对象,加入超时机制
  • void parkUntil(Object blocker, long deadline):设置blocker对象,加入超时机制(指定到某个时间点,1970年到指定时间点的毫秒数)

建议使用时,传入blocker对象,至于超时根据业务场景选择

你可能感兴趣的:(#,03,JUC,java)