为什么需要wait方法
- 一个线程调用sleep方法会陷入休眠状态,虽然释放了cpu但是不会释放锁,这样就会造成浪费,因为陷入sleep状态的线程白白占着共享资源而没有使用它,使得其他线程不得不陷入等待状态。
- 而wait方法虽然也会使线程陷入暂停状态,与sleep不同的是它会将cpu和锁资源全部释放。
- 如下 线程1占用锁后休眠了100秒,在线程1休眠期间,线程2无法获得锁,因此无法往下执行,这样白白浪费了cpu资源。
new Thread(() -> {
synchronized (Code.class){
System.out.println("t1获得锁");
try {
Thread.sleep(100*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(1000);
new Thread(()->{
System.out.println("t2尝试获得锁");
synchronized (Code.class){
System.out.println("t2成功获得锁");
}
}).start();
此时可以使用wait方法将线程1休眠,这样线程1会释放锁,就不会耽误线程2的执行。
new Thread(() -> {
synchronized (Code.class){
System.out.println("t1获得锁");
try {
Code.class.wait(100*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(1000);
new Thread(()->{
System.out.println("t2尝试获得锁");
synchronized (Code.class){
System.out.println("t2成功获得锁");
}
}).start();
wait方法的原理
- wait方法的底层也会使用到monitor对象,如图所示。当一个线程申请到锁之后,就会让monitor对象的owner属性指向自己,但是当它发现虽然自己得到了锁但是因为其他条件不满足,无法执行,就会调用wait方法。
- 调用wait方法之后的线程会进入waitset队列,线程变为waiting状态。与entrylist队列线程不同的是,entrylist队列线程是申请不到锁资源而陷入等待的状态,而waitset队列线程是申请锁资源后由于其他条件不满足而必须调用wait方法重新释放锁的线程。
- 陷入waiting状态的线程会在owner线程调用notify或notifyAll时唤醒,唤醒后的线程不会立刻获得锁,而是重新进入entrylist队列重新竞争锁。
- 因此entrylist可以称为阻塞队列,waitset可以称为条件队列。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/1f94e5423beed95e9f2c8ddf3f4d9632.png)
相关API
- obj.wait() 让获得obj对象锁的线程到waitset等待。
- obj.notify() 在obj对象关联的monitor对象上正在waitset等待的线程中挑一个唤醒。
- obj.notifyALL() 在obj对象关联的monitor对象上正在等待的线程全部唤醒。
- 他们都属于object对象的方法,必须获得此对象的锁(成为owner线程后),才能调用这几个方法。
wait/notify方法的基本使用
- wait方法必须在线程获得锁之后才能,调用,如果没有获得锁就调用wait方法会报错,即只有owner线程才有权限唤醒waitset中等待的线程。
public class Demo {
public static final Object lock = new Object();
public static final Logger log = LoggerFactory.getLogger(Test.class);
public static void main(String[] args) throws InterruptedException {
lock.wait();
}
}
public class Demo {
public static final Object lock = new Object();
public static final Logger log = LoggerFactory.getLogger(Test.class);
public static void main(String[] args) throws InterruptedException {
synchronized (lock){
lock.wait();
}
}
}
- notify方法会在lock对象上面挑一个waiting线程唤醒
public class Demo {
public static final Object lock = new Object();
public static final Logger log = LoggerFactory.getLogger(Test.class);
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
log.info("线程0开始执行");
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("线程0被唤醒继续执行");
}).start();
new Thread(()->{
log.info("线程1开始执行");
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("线程1被唤醒继续执行");
}).start();
Thread.sleep(2000);
synchronized (lock){
log.info("主线程唤醒一个waiting状态的线程");
lock.notify();
}
}
}
- notifyall方法会唤醒lock对象上面所有waiting线程
public class Demo {
public static final Object lock = new Object();
public static final Logger log = LoggerFactory.getLogger(Test.class);
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
log.info("线程0开始执行");
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("线程0被唤醒继续执行");
}).start();
new Thread(()->{
log.info("线程1开始执行");
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("线程1被唤醒继续执行");
}).start();
Thread.sleep(2000);
synchronized (lock){
log.info("主线程唤醒一个waiting状态的线程");
lock.notifyAll();
}
}
}
- wait方法可以加参数表示有限时的等待,比如wait(3000)会让当前线程等待三秒,如果三秒后还没有其他线程唤醒它,该线程会自动唤醒,如果三秒内它被其他线程唤醒它就不用等三秒唤醒。
一个送烟的例子
- 小明和其他三个人要干活,他们依次在一个房间干活,该房间只能允许一个人进入。小明烟瘾很大,如果没有烟就无法干活,刚开始小明进入房间,他发现没烟就会休息5秒,而在2秒后有服务员过来送烟,到5秒的时候小明发现有烟了开始干活,干完之后其他人在依次干活。
- 在这个例子中烟就是小明线程运行的必要条件。
public class Demo {
public static final Object room = new Object();
public static volatile boolean flag = false;
public static final Logger log = LoggerFactory.getLogger(Test.class);
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (room){
log.info("有烟没--"+flag);
if(flag==false){
log.info("没烟 先睡5秒");
try {
Thread.sleep(5000);
} catch (InterruptedException e) { e.printStackTrace(); }
}
log.info("有烟没--"+flag);
if(flag==true){
log.info("有烟了 可以开始干活了");
}else{
log.info("没干成活");
}
}
},"小明").start();
for(int i=1;i<4;i++){
new Thread(()->{
synchronized (room){
log.info("开始干活了");
}
},"线程"+i).start();
}
Thread.sleep(2000);
new Thread(()->{
log.info("两秒后开始送烟");
flag = true;
},"送烟线程").start();
}
}
- 可以发现刚开始小明占着房间没有干活,直到5秒后才开始干活,那么相当于房间使用权白白浪费了5秒,而且其他人必须要等待小明干完活才能干活,这5秒房间时间使用权完全可以交给没有烟瘾的其他人用。
- 而且需要注意,送烟线程不能加synchronized ,如果送烟线程加了synchronized ,因为小明已经占了房间,所以送烟线程进不了房间和其他干活的人一样也会阻塞。
- 使用wait/notify来优化送烟例子
public class Demo {
public static final Object room = new Object();
public static boolean flag = false;
public static final Logger log = LoggerFactory.getLogger(Test.class);
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (room){
log.info("有烟没--"+flag);
if(flag==false){
log.info("没烟 先睡5秒");
try {
room.wait(5000);
} catch (InterruptedException e) { e.printStackTrace(); }
}
log.info("有烟没--"+flag);
if(flag==true){
log.info("有烟了 可以开始干活了");
}else{
log.info("没干成活");
}
}
},"小明").start();
for(int i=1;i<4;i++){
new Thread(()->{
synchronized (room){
log.info("开始干活了");
}
},"线程"+i).start();
}
Thread.sleep(2000);
new Thread(()->{
synchronized (room){
log.info("两秒后开始送烟 并叫醒小明线程");
flag = true;
room.notify();
}
},"送烟线程").start();
}
}
- 可以看到小明在发现没有烟后不会占着房间休息,而是让出房间使用权,此时后面其他人依然可以使用房间干活,当送烟线程送到烟后再去休息室将小明叫醒,小明可以继续干活。
- 但是如果条件队列中阻塞的不止小明一个人,还有小红也在休息,那么送烟线程可能错误唤醒小红而没有把小明唤醒。如下所示,小红没有干成活,小明依然在休息室休息。这种错误唤醒的情况也称为虚假唤醒。
public class Demo {
public static final Object room = new Object();
public static boolean flag = false;
public static boolean flag2 = false;
public static final Logger log = LoggerFactory.getLogger(Test.class);
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (room){
log.info("外卖到了没--"+flag2);
if(flag2==false){
log.info("没有外卖 进入休息室休息");
try {
room.wait();
} catch (InterruptedException e) { e.printStackTrace(); }
}
log.info("外卖到了没--"+flag2);
if(flag2==true){
log.info("有外卖了 可以开始干活了");
}else{
log.info("没干成活");
}
}
},"小红").start();
new Thread(()->{
synchronized (room){
log.info("有烟没--"+flag);
if(flag==false){
log.info("没烟 进入休息室休息");
try {
room.wait();
} catch (InterruptedException e) { e.printStackTrace(); }
}
log.info("有烟没--"+flag);
if(flag==true){
log.info("有烟了 可以开始干活了");
}else{
log.info("没干成活");
}
}
},"小明").start();
for(int i=1;i<4;i++){
new Thread(()->{
synchronized (room){
log.info("开始干活了");
}
},"线程"+i).start();
}
Thread.sleep(2000);
new Thread(()->{
synchronized (room){
log.info("两秒后开始送烟 并叫醒小明线程");
flag = true;
room.notify();
}
},"送烟线程").start();
}
}
- 这时候送烟线程就不能使用notify随机唤醒一个线程,而应该使用notifyall将休息室线程全部唤醒,此时小明小红都被唤醒,小明看到有烟了开始干活,小红发现没有外卖不会干活。
new Thread(()->{
synchronized (room){
log.info("两秒后开始送烟 并叫醒所有线程");
flag = true;
room.notifyAll();
}
},"送烟线程").start();
- 但是此时还有一个问题,小红被送烟线程唤醒了却没有干成活,等送外卖线程来了小红已经不在休息室了,小红就永远不会干活了,此时需要进行改进,当小红被叫醒后发现外卖并没有送到,她应该再次进入休息室等待,直到外卖真的送到了。
public class Demo {
public static final Object room = new Object();
public static boolean flag = false;
public static boolean flag2 = false;
public static final Logger log = LoggerFactory.getLogger(Test.class);
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (room){
while(flag2==false){
log.info("外卖到了没--"+flag2);
log.info("没有外卖 进入休息室休息");
try {
room.wait();
} catch (InterruptedException e) { e.printStackTrace(); }
}
if(flag2==true){
log.info("外卖到了 可以干活了");
}
}
},"小红").start();
new Thread(()->{
synchronized (room){
while(flag==false){
log.info("有烟没--"+flag);
log.info("没烟 进入休息室休息");
try {
room.wait();
} catch (InterruptedException e) { e.printStackTrace(); }
}
if(flag==true){
log.info("烟到了 可以干活了");
}
}
},"小明").start();
for(int i=1;i<4;i++){
new Thread(()->{
synchronized (room){
log.info("开始干活了");
}
},"线程"+i).start();
}
Thread.sleep(2000);
new Thread(()->{
synchronized (room){
log.info("两秒后开始送烟 并叫醒所有线程");
flag = true;
room.notifyAll();
}
},"送烟线程").start();
}
}
- 另外可以使用ReentrantLock设置多个条件队列,彻底解决错误唤醒的问题。