wait使用场景
当Owner线程发现条件不满足,就会进入wait状态,进入Monitor中的WaitSet变成WATING状态。在WATING的县城和BLOCKED一样都是阻塞状态,不会占有CPU的时间片。但BLOCKED状态的线程会在Owner线程释放锁时唤醒。而WAITING线程会在Owner调用notify或notifyAll时唤醒,唤醒后也不意味着马上就会获得锁,而是重新进入EntryList进行竞争。
wait/notify
- wait():使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。
- wait(long):超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。
- wait(long,int):对于超时时间更细力度的控制,单位为纳秒。
- notify():随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知一个线程。
- notifyAll():使所有正在等待队列中等待同一共享资源的全部线程退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现。
- 被wait的前提是,你获得了Owner,也就是你竞争到了锁。
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (lock){
System.out.println("执行t1");
try {
//lock.wait();
lock.wait(1000);//这里表示,如果等待一秒后还是没有被notify叫醒,那他就会继续执行下去
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1:执行其他代码");
}
},"t1").start();
new Thread(()->{
synchronized (lock){
System.out.println("执行t2");
try {
lock.wait();//不加参数的话它会一直等待下去,直到notify唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2:执行其他代码");
}
},"t2").start();
TimeUnit.SECONDS.sleep(2);//让主线程休息两秒再执行
System.out.println("唤醒lock上的其它线程");
synchronized (lock) {
//lock.notify(); // 唤醒obj上一个线程
lock.notifyAll(); // 唤醒obj上所有等待线程
}
}
wait和sleep的比较
对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
从使用角度看,sleep是Thread线程类的方法,而wait是Object顶级类的方法。
sleep可以在任何地方使用,而wait只能在同步方法或者同步块中使用。
CPU及资源锁释放
sleep,wait调用后都会暂停当前线程并让出cpu的执行时间,但不同的是sleep不会释放当前持有的对象的锁资源,到时间后会继续执行,而wait会放弃所有锁并需要notify/notifyAll后重新获取到对象锁资源后才能继续执行。
sleep和wait的区别:
- 1、sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
- 2、sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
- 3、它们都可以被interrupted方法中断。
具体来说:
Thread.Sleep(1000) 意思是在未来的1000毫秒内本线程不参与CPU竞争,1000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束,即使这个时候恰巧轮到操作系统进行CPU 分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去。另外值得一提的是Thread.Sleep(0)的作用,就是触发操作系统立刻重新进行一次CPU竞争,竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。
wait(1000)表示将锁释放1000毫秒,到时间后如果锁没有被其他线程占用,则再次得到锁,然后wait方法结束,执行后面的代码,如果锁被其他线程占用,则等待其他线程释放锁。注意,设置了超时时间的wait方法一旦过了超时时间,并不需要其他线程执行notify也能自动解除阻塞,但是如果没设置超时时间的wait方法必须等待其他线程执行notify。
wait/notify的使用姿势
step1
@Slf4j(topic = "c.Test01")
public class Test01 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
// 这里能不能加 synchronized (room)?
hasCigarette = true;
log.debug("烟到了噢!");
}, "送烟的").start();
}
送烟的线程能不能加synchronized (room)?
不能,因为此时小南占据了room,它只是调用了sleep。并不会放弃锁。如果送烟线程也用了锁,那送烟线程得等到小南线程执行结束以后才可以获得锁并送烟。
其实这个线程使用sleep也不好,烟1秒后送到,小南却睡了两秒,中间有一秒钟的资源浪费。小南锁住了room,其它人不需要烟也可以干活,但是因为没有锁,导致不能够执行。这样的效率是非常低的。
改进方法:小南线程里的sleep换成wait,送烟线程加上synchronized锁住room并调用notify方法。
step3
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notify();
}
}, "送外卖的").start();
}
运行结果:
notify这时候就出现问题了,因为notify是随机叫醒一个线程,此时外卖线程里调用了notify,但是却叫醒了小南线程,产生了虚假唤醒的现象。
解决方法就是直接用notifyAll。
其实面对小南被无辜唤醒的事,可以通过把小南线程里的if判断换成while循环来解决。
保护性暂停
保护性暂停,即Guarded Suspension,用一个线程等待另一个线程的执行结果
要点:
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
- JDK中,join的实现、Futrue的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
@Slf4j(topic = "c.GuardedSuspensionTest")
public class GuardedSuspensionTest {
public static void main(String[] args) {
//模拟线程1等待线程2的结果
GuardedObject guardedObject = new GuardedObject();
new Thread(()->{
//等待结果
try {
log.info("线程一等待结果中.....");
LinkedList list = (LinkedList) guardedObject.get();
log.debug("结果返回了{}",list.get(0));
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
//等待结果
log.info("线程二执行业务逻辑中.....");
List<String> list = new LinkedList<>();
for (int i = 65; i < 65+26; i++) {
list.add(String.valueOf((char) i));
}
guardedObject.complete(list);
},"t2").start();
}
}
class GuardedObject{
//结果
private Object response;
//获取结果的方法
public synchronized Object get() throws InterruptedException {
while (response == null){
//表示还没有结果,一直等待下去。
this.wait();
}
//跳出循环后就可以返回不为空的response了
return this.response;
}
//产生结果
public synchronized void complete(Object response){
//给response赋值
this.response = response;
this.notifyAll();
}
}
保护性暂停模式可以避免使用join,并且可以避免使用全局变量。
扩展:增加超时
线程一只等待一个最大时间,超时的话立马结束while循环。
@Slf4j(topic = "c.GuardedSuspensionTest")
public class GuardedSuspensionTest {
public static void main(String[] args) {
//模拟线程1等待线程2的结果
GuardedObject guardedObject = new GuardedObject();
new Thread(()->{
//等待结果
try {
log.info("线程一等待结果中.....");
LinkedList list = (LinkedList) guardedObject.get(2000);
log.debug("结果返回了{}",list.get(0));
} catch (Exception e) {
//e.printStackTrace();
log.debug("俺不等了!!!");
}
},"t1").start();
new Thread(()->{
//等待结果
log.info("线程二执行业务逻辑中.....");
List<String> list = new LinkedList<>();
for (int i = 65; i < 65+26; i++) {
list.add(String.valueOf((char) i));
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
guardedObject.complete(list);
},"t2").start();
}
}
class GuardedObject{
//结果
private Object response;
//获取结果的方法
public synchronized Object get(long timeout) throws InterruptedException {
//开始时间
long begin = System.currentTimeMillis();
//已经过去的时间
long passMillis = 0;
while (response == null){
//破除循环
if(passMillis >= timeout){
break;
}
//表示还没有结果,一直等待下去。
this.wait(timeout-passMillis); //为了防止虚假唤醒,所以每次等待的时间应该是timeout-passMillis
//在这里记录一个已经过去的时间,如果已经过去了,那就不让这个循环继续下去。
passMillis = System.currentTimeMillis() - begin;
}
//跳出循环后就可以返回不为空的response了
return this.response;
}
//产生结果
public synchronized void complete(Object response){
//给response赋值
this.response = response;
this.notifyAll();
}
}
为什么要wait(timeout-passMillis)呢,这是因为如果第一次循环,并不满足条件,那wait实际上第一次是wait了2s,假设1s后进入了第二次循环,这时不满足破除循环的条件,但wait还需要再wait两秒吗?显然是不需要的,而是只需要wait1s即可,我们已经记录了已经过去的时间,那只要求出还需要最多等多久的时间就够了,所以要wait的参数要填wait(timeout-passMillis)。
Join原理
下面是Join的源码
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
join的底层也是使用wait来实现的,wait方法也是调用了wait(0)来实现。wait(0)就是一直让线程等待下去,join方法本质就是利用上面的线程实例作为对象锁的原理,当线程终止时,会调用线程自身的notifyAll()方法,通知所有等待在该线程对象上的线程的特征。
如果参数大于0的话,也就相当于刚才写的代码。通过记录开始时间和经过时间,来确定需要wait多久来保证wait时间是符合参数的。也就是保护性暂停模式。
扩展2:解耦
多任务版 GuardedObject图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理。和生产者消费者模式的区别就是:这个生产者和消费者之间是一一对应的关系,但是生产者消费者模式并不是。rpc框架的调用中就使用到了这种模式。
@Slf4j(topic = "c.GuardedSuspensionTest")
public class GuardedSuspensionTest {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new People().start();
}
TimeUnit.SECONDS.sleep(1);
for (Integer i: MailBoxes.getIds()) {
new PostMan(i,"恭喜收到信,内容ID:"+i).start();
}
}
}
//因为是做的多线程,所以直接用继承Thread类的方式来实现。
@Slf4j(topic = "c.People")
class People extends Thread{
//重写run方法。居民等待信件到来。
@Override
public void run() {
GuardedObject guardedObject = MailBoxes.createGuardedObject();
log.debug("开始收信........");
try {
Object mail = guardedObject.get(5000);
log.debug("收到id为{}的信成功,结果是{}",guardedObject.getId(),mail.toString());
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("收信失败了........");
}
}
}
@Slf4j(topic = "c.PostMan")
class PostMan extends Thread{
//因为邮递员比较特殊,他是生产者,所以他需要两个信息,邮箱ID和信件内容(笑嘻了
private int mailId;
private String mail;
public PostMan(int id,String mail){
this.mailId = id;
this.mail = mail+mailId;
}
@Override
public void run() {
GuardedObject guardedObject = MailBoxes.getGuardedObject(mailId);
log.debug("送信咯:{}",mail);
guardedObject.complete(mail);
}
}
class MailBoxes{
private static Map<Integer,GuardedObject> boxes = new Hashtable<>();//这里暂时使用hashtable
private static int id = 1;
//给GuardedObject产生一个唯一ID
private static synchronized int generatorId(){
//毕竟是多线程操作所以必须加上synchronized,而且要记住synchronized加在静态方法下就是给MailBoxes类对象加锁!!!
return id++;
}
public static GuardedObject createGuardedObject(){
GuardedObject go = new GuardedObject(generatorId());
boxes.put(go.getId(),go);
return go;
}
public static Set<Integer> getIds(){
return boxes.keySet();
}
public static GuardedObject getGuardedObject(int id){
//从优雅性上考虑,反正居民不会收到第二次信,那我们就在获取到实例后直接把它从Map中删除。
return boxes.remove(id);
}
}
class GuardedObject{
//用来区分其它实例
private int id;
//结果
private Object response;
public GuardedObject(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
//获取结果的方法
public synchronized Object get(long timeout) throws InterruptedException {
//开始时间
long begin = System.currentTimeMillis();
//已经过去的时间
long passMillis = 0;
while (response == null){
//破除循环
if(passMillis >= timeout){
break;
}
//表示还没有结果,一直等待下去。
this.wait(timeout-passMillis); //为了防止虚假唤醒,所以每次等待的时间应该是timeout-passMillis
//在这里记录一个已经过去的时间,如果已经过去了,那就不让这个循环继续下去。
passMillis = System.currentTimeMillis() - begin;
}
//跳出循环后就可以返回不为空的response了
return this.response;
}
//产生结果
public synchronized void complete(Object response){
//给response赋值
this.response = response;
this.notifyAll();
}
}
算是一次应用吧。
异步模式之生产者消费者
要点
- 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
- JDK 中各种阻塞队列,采用的就是这种模式
“异步”的意思就是生产者产生消息之后消息没有被立刻消费,而“同步模式”中,消息在产生之后被立刻消费了。
我们写一个线程间通信的消息队列,要注意区别,像rabbit mq等消息框架是进程间通信的。
@Slf4j(topic = "c.Test02")
public class Test02 {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue(2);
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
try {
queue.put(new Message(id, "balabala" + id));
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "生产者" + i).start();
}
new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者").start();
}
}
//消息队列类,仅用来线程通信
@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
//消息的队列集合
private LinkedList<Message> channel = new LinkedList<>();
//容器容量
private int capCity;
public MessageQueue(int capCity) {
this.capCity = capCity;
}
//获取消息的方法
public Message take() throws InterruptedException {
//检查队列是否为空,为空的话消费者就进入等待。
synchronized (channel) {
while (channel.isEmpty()) {
log.debug("队列为空");
channel.wait();
}
//返回第一条消息并将消息从队列中剔除。
Message message = channel.removeFirst();
log.debug("取出消息:{}", message.getValue());
//通知生产者线程可以接着放入了
channel.notifyAll();
return message;
}
}
//存入消息的方法
public void put(Message message) throws InterruptedException {
synchronized (channel) {
//检查队列是否满了,满了的话则进入等待
while (channel.size() == capCity) {
log.debug("队列已满");
channel.wait();
}
//将消息加入队列的尾部
channel.addLast(message);
log.debug("生产消息:{}", message.getValue());
//通知消费者线程可以消费了
channel.notifyAll();
}
}
}
//因为消息的唯一性很重要,所以我们自己建一个Message类
final class Message {
private int id;
private Object value;
public Message(int id, Object value) {
this.id = id;
this.value = value;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Object getValue() {
return value;
}
}