等待与唤醒机制

255 阅读5分钟

什么是等待、唤醒机制

当多个线程在竞争获取同一把锁时,此时某个线程1已经获取到了锁但是线程1需要在某些特定的条件下才能运行,那么锁会调用wait方法使得线程1进入_waitset队列中,此时的线程1不会浪费cpu的资源(不会去竞争锁)并且线程1处于WAITING状态。后继的某个线程运行完程序会调用锁的notify方法唤醒处于_waitset 队列中的一个或多个线程。综上等待与唤醒是锁在多线程环境中的一种协调资源的一种机制。

工作原理

image.png

  • OWner 线程发现条件不满足,调用wait方法,即线程进入WaitSet 状态变为 WAITING状态
  • BLOKED 和 WAITING 的线程都处于阻塞状态,不占用CPU时间片
  • BLOKED 线程会在OWner 线程释放锁时被唤醒
  • WAITING 线程会在OWner线程调用notify 或者 notifyAll 时被唤醒,但唤醒后并不意味着立刻获得到锁,仍需进入EntryList 中参与锁的竞争。

Java三种等待/唤醒实现方式

  • wait()/notify()
package org.chou.wn;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * @author Axel
 * @version 1.0
 * @className Wait_Notify
 * @description 线程间的等待与唤醒机制
 * @date 2022/5/1 22:03
 */

@Slf4j(topic = "Wait_Notify")
public class Wait_Notify {
    static Object monitor = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (monitor) {
                System.out.println("线程1为满足业务条件调用 wait(),进入等待状态....");
                try {
                    monitor.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程1被唤醒执行业务逻辑......");
            }
        }, "t1").start();
        try {
            TimeUnit.MILLISECONDS.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            synchronized (monitor) {
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程2执行业务逻辑准备数据,唤醒其他线程....");
                monitor.notify();
            }
        }, "t2").start();
    }

}
  • await()/signal()
package org.chou.wn;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Axel
 * @version 1.0
 * @className Wait_Notify
 * @description 线程间的等待与唤醒机制
 * @date 2022/5/1 22:03
 */

@Slf4j(topic = "Wait_Notify")
public class Wait_Notify {
    static Object monitor = new Object();
    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    public static void main(String[] args) {
//        Wn();
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("线程1为满足业务条件调用 wait(),进入等待状态....");
                condition.await();
                System.out.println("线程1被唤醒执行业务逻辑......");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }, "t1").start();

        new Thread(() -> {
            lock.lock();
            try {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程2执行业务逻辑准备数据,唤醒其他线程....");
                condition.signal();
            } catch (Exception e) {
                log.error(e.getMessage());
            } finally {
                lock.unlock();
            }

        }).start();

    }

}
  • park()/unPark()
package org.chou.wn;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Axel
 * @version 1.0
 * @className Wait_Notify
 * @description 线程间的等待与唤醒机制
 * @date 2022/5/1 22:03
 */

@Slf4j(topic = "Wait_Notify")
public class Wait_Notify {
    static Object monitor = new Object();
    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    public static void main(String[] args) {
        // Wn();
        //lockUnlock();
        Thread t1 = new Thread(() -> {
            System.out.println("线程" + Thread.currentThread().getName() + "开始执行业务逻辑");
            // 让t2先唤醒,线程t1 在等待
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.park();
            System.out.println("线程" + Thread.currentThread().getName() + "线程被挂起后执行业务逻辑");
        }, "t1");
        t1.start();

        try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(()->{
            System.out.println("线程"+Thread.currentThread().getName()+"执行业务逻辑并唤醒线程t1");
            // 在线程2中唤醒
            LockSupport.unpark(t1);
            System.out.println("线程"+Thread.currentThread().getName()+"结束...");

        },"t2").start();

    }
}

总结以上三种等待与唤醒机制峰使用方法及注意事项

  • wait()/notify()

    1、需要结合 synchronized 关键字来使用,必须要在同步语句块中进行wait()/notify()方法的调用

    2、两个方法来自顶级父类Object中实现线程的等待和唤醒

    3、两个方法有先后顺序必须是先线程调用了wait(),之后才能调用notify()方法

  • awati()和signal()

    1、两个方法需要结合 Lock 对象来一起使用,即需要获取到锁,必须在(syncchonized/lock)中

    2、需要通过 Lock 获取得到对应的Condition对象使用(AQS?)。

    3、需要先执行await()之后,再调用signal()。

  • Park()/unPark()

    1、来源于LockSupport类中(是用来创建锁和其他同步类的基本线程阻塞原语)。该类使用了一种名为许可(Permit)的概念来做到阻塞和唤醒线程的功能,给每个线程都会分配一个permit(许可)。

    2、没有顺序的区分(使用permit限制)。

等待与唤醒机制的应用

在日常开发中使用不到三种机制,根据这些特性去思考在多线程环境中有哪些应用场景。下面来介绍下多线程环境下使用三种机制的 同步模式顺序控制,交替输出

1、多线程环境中固定循序运行

存在两个线程t1、t2。其中t1线程运行打印数字 1,t2线程运行打印数字 2。要求程序运行时先打印数字 2 再打印数字1。

代码

package org.chou.wn;

import lombok.extern.slf4j.Slf4j;

/**
* @author Axel
* @version 1.0
* @className WnPattern
* @description 多线程同步模式-交替输出循序输出
* @date 2022/5/29 10:17
*/

@Slf4j(topic = "WnPattern")
public class WnPattern {
   // 锁对象
   public final static Object monitor = new Object();
   // 标识其他线程是否运行过
   public static boolean t2Runed = false;

   public static void main(String[] args) {


       Thread t1 = new Thread(() -> {
           System.out.println(Thread.currentThread().getName()+"开始执行...");
           synchronized (monitor) {
               while (!t2Runed) {
                   try {
                       monitor.wait();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
               log.info("print number---> 1");
           }
       },"t1");

       Thread t2 = new Thread(() -> {
           System.out.println(Thread.currentThread().getName()+"开始执行...");
           synchronized (monitor) {
                   log.info("print number---> 2");
                   t2Runed = true;
                   monitor.notify();
           }
       },"t2");

       t1.start();
       t2.start();
   }

}

2、多线程环境中交替运行输出

程序中存在三个线程t1、t2、t3。要求每个线程分别各自输出 a 5次,b 3次, c 5 次。即输出abcabcabcabcabc。

代码

package org.chou.wn;

/**
 * @author Axel
 * @version 1.0
 * @className ReplacePrint
 * @description 多线程条件下同步模式交替输出
 * @date 2022/5/29 11:54
 */

public class ReplacePrint {
    public static void main(String[] args) {
        AsyncReplacePrint asyncReplacePrint = new AsyncReplacePrint(1, 5);
        new Thread(() -> {
            asyncReplacePrint.print("a", 1, 2);
        }, "t1").start();
        new Thread(() -> {
            asyncReplacePrint.print("b", 2, 3);
        }, "t2").start();
        new Thread(() -> {
            asyncReplacePrint.print("c", 3, 1);
        }, "t3").start();

    }
}

class AsyncReplacePrint {
    // 标识那个线程输出
    public int flag;
    // abc 一组数据输出数据格式
    public int loopCount;

    public AsyncReplacePrint(int flag, int loopCount) {
        this.flag = flag;
        this.loopCount = loopCount;
    }

    /**
     * 多线程环境中使用wait()/notify() 交替打印 abc
     * @param printStr  需要打印的字符
     * @param waitFlag  需要等待的线程标识
     * @param nextFlag   下一个需要打印的线程标识
     */
    public void print(String printStr, int waitFlag, int nextFlag) {
        for (int i = 0; i < loopCount; i++) {
            synchronized (this) {
                while (this.flag != waitFlag) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(printStr);
                flag = nextFlag;
                this.notifyAll();
            }
        }
    }
}

等待/唤醒机机制应用介绍完毕,以上两种应用也可以使用 await()/signal()和 park()/unpark() 来实现。有兴趣的小伙伴可以动手看看。