什么是等待、唤醒机制
当多个线程在竞争获取同一把锁时,此时某个线程1已经获取到了锁但是线程1需要在某些特定的条件下才能运行,那么锁会调用wait方法使得线程1进入_waitset队列中,此时的线程1不会浪费cpu的资源(不会去竞争锁)并且线程1处于WAITING状态。后继的某个线程运行完程序会调用锁的notify方法唤醒处于_waitset 队列中的一个或多个线程。综上等待与唤醒是锁在多线程环境中的一种协调资源的一种机制。
工作原理
- 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() 来实现。有兴趣的小伙伴可以动手看看。