《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)(二)

简介: 《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)

3.10 wait notify 的正确姿势

3.10.0 sleep(long n) 和 wait(long n) 的区别


sleep 是 Thread 方法,而 wait 是 Object 的方法

sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用

sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁

它们状态 都是 TIMED_WAITING (相同点)

当不满足条件等待,最好使用wait.因为sleep不会释放锁。sleep一般是让CPU休息…:

/**
 * 演示sleep和wait睡眠后,是否释放锁
 */
@Slf4j(topic = "c.Test19")
public class Test19 {
    static final Object lock = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lock) {
                log.debug("获得锁");
                try {
//                    Thread.sleep(20000);
                    lock.wait(20000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();
        Sleeper.sleep(1);
        synchronized (lock) {
            log.debug("获得锁");
        }
    }
}

3.10.1 问题描述

小南只有叼有烟的时候才干活,其他人只要有时间片就干活,用代码模拟这个过程

3.10.2 step 1

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep1 {
    static final Object room = new Object();//建议:引用用final修饰,保证锁的对象就不可变。
    static boolean hasCigarette = false; // 有没有烟
    static boolean hasTakeout = false;
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    sleep(2);
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                }
            }
        }, "小南").start();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
        }
        sleep(1);
        new Thread(() -> {
            // 这里能不能加 synchronized (room)?
            //synchronized (room) {
                hasCigarette = true;
                log.debug("烟到了噢!");
           // }
        }, "送烟的").start();
    }
}

输出1:

17:17:31.927 c.TestCorrectPosture [小南] - 有烟没?[false]
17:17:31.935 c.TestCorrectPosture [小南] - 没烟,先歇会!
17:17:32.929 c.TestCorrectPosture [送烟的] - 烟到了噢!
17:17:33.940 c.TestCorrectPosture [小南] - 有烟没?[true]
17:17:33.940 c.TestCorrectPosture [小南] - 可以开始干活了
17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了
17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了
17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了
17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了
17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了

输出2:(当加上synchronized (room)

17:25:44.996 c.TestCorrectPosture [小南] - 有烟没?[false]
17:25:45.004 c.TestCorrectPosture [小南] - 没烟,先歇会!
17:25:47.007 c.TestCorrectPosture [小南] - 有烟没?[false]
17:25:47.007 c.TestCorrectPosture [送烟的] - 烟到了噢!
17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了
17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了
17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了
17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了
17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了

分析:


其它干活的线程,都要一直阻塞,效率太低

小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来

main加了synchronized (room)后,就好比小南在里面反锁了门睡觉,烟根本没法送进门。没加

synchronized 就好像 main 线程是翻窗户进来的

解决方法,使用 wait - notify 机制


3.10.3 step 2

使用wait-notify改进上面代码

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep2 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait(2000);
                    } 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();
        }
        sleep(1);
        new Thread(() -> {
            synchronized (room) {
                hasCigarette = true;
                log.debug("烟到了噢!");
                room.notify();
            }
        }, "送烟的").start();
    }
}

输出:

17:42:24.719 c.TestCorrectPosture [小南] - 有烟没?[false]
17:42:24.727 c.TestCorrectPosture [小南] - 没烟,先歇会!
17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了
17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了
17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了
17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了
17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了
17:42:25.719 c.TestCorrectPosture [送烟的] - 烟到了噢!
17:42:25.719 c.TestCorrectPosture [小南] - 有烟没?[true]
17:42:25.719 c.TestCorrectPosture [小南] - 可以开始干活了

分析:

  • 此改进可以让其他线程同时运行,不会占用锁,并发效率大大提升
  • 思考:如果有其他线程也在等待,那么会不会错误的叫醒了其他线程?


3.10.4 step 3-4

加入另外一个线程 小女线程时,当外卖送到,小女可开始工作。思考并分析运行结果


@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep3 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    // 虚假唤醒
    public static void main(String[] args) {
        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();
        sleep(1);
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");
                room.notify();
                // room.notifyAll();
            }
        }, "送外卖的").start();
    }
}

输出1:(使用notify

image.png

  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线
    程,称之为【虚假唤醒
  • 解决方法,改为 notifyAll

输出2:(使用notifyAll

26eab13029b41aa93ccbc0c3b0c7bfaf.png


用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新

判断的机会了(比如小南被错误唤醒后,就不能重新判断了)

解决方法,用 while + wait,当条件不成立,再次 wait


3.10.5 step 5

if 改为 while ,防止虚假唤醒

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep5 {
   static final Object room = new Object();
   static boolean hasCigarette = false;
   static boolean hasTakeout = false;
   public static void main(String[] args) {
       new Thread(() -> {
           synchronized (room) {
               log.debug("有烟没?[{}]", hasCigarette);
               while (!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);
               while (!hasTakeout) {
                   log.debug("没外卖,先歇会!");
                   try {
                       room.wait();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
               log.debug("外卖送到没?[{}]", hasTakeout);
               if (hasTakeout) {
                   log.debug("可以开始干活了");
               } else {
                   log.debug("没干成活...");
               }
           }
       }, "小女").start();
       sleep(1);
       new Thread(() -> {
           synchronized (room) {
               hasTakeout = true;
               log.debug("外卖到了噢!");
               room.notifyAll();
           }
       }, "送外卖的").start();
   }
}

输出:

df151315d50a6b81c2c0c872ffe5befb.png

*结论:**此方法满足需求。即提高了并发效率问题,又不会出现虚假唤醒问题。


思考:while中使用wait()相比wait(long num)会不会浪费CPU呢?


只有线程被唤醒才会接着while,不唤醒就是wait,所以不会有cpu空转


3.10.6 使用wait和notify的正确姿势总结

synchronized(lock) {
  while(条件不成立) {//while防止虚假唤醒
    lock.wait();
  }
  // 干活
}
//另一个线程
synchronized(lock) {
  lock.notifyAll();//notifyAll唤醒所有等待序列中国的线程
}

3.11 Park & Unpark

3.11.1 基本使用

它们是 LockSupport 类中的方法

// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

先 park 再 unpark

@Slf4j(topic = "c.TestParkUnpark")
public class TestParkUnpark {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            Sleeper.sleep(1);
            log.debug("park...");
            LockSupport.park();
            log.debug("resume...");
        }, "t1");
        t1.start();
        Sleeper.sleep(2);
        log.debug("unpark...");
        LockSupport.unpark(t1);
    }
}

f1ed469ad7989ab98d17a5fcf938fbe5.png


先 unpark 再 park:交换main和t1线程sleep的时间


1c4a509058d7c97bc85f8fa9e5908336.png


3.11.2 特点


与 Object 的 wait & notify 相比


wait,notify 和 notifyAll 必须配合 Object Monitor(锁) 一起使用,而 park,unpark 不必

park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】


  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify (先notify的代码会被忽略)

3.11.3 原理之 park & unpark

3.12 重新理解线程状态转换

871f5cb8aa706b67c261a593fd814519.png


NEW:初始状态;创建了Java线程对象,还没有与操作系统的线程相关联。

情况 1: NEW --> RUNNABLE

当调用 t.start() 方法时,由 NEW --> RUNNABLE

情况 2: RUNNABLE <--> WAITING

t 线程用 synchronized(obj) 获取了对象锁后


调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING

调用 obj.notify() , obj.notifyAll() , t.interrupt() 时

竞争锁成功,t 线程从 WAITTING --> RUNNABLE

竞争锁失败,t 线程依旧是 WAITTING --> BLOCKED

**注意:**第二步中,调用 obj.notify() , obj.notifyAll() , t.interrupt() 但未释放锁,t 线程会先进入EntryList 等待竞争锁,此时为BLOCKED状态。持锁线程释放锁后 EntryList中的线程会进行竞争。然后 根据竞争结果,t 线程会处于不同的状态。此过程可在后续Debug分析中清晰看到…


测试代码

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
    final static Object obj = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t1").start();
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t2").start();
        // 主线程两秒后执行
        sleep(2);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
           // obj.notify(); // 唤醒obj上一个线程
            obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }
}

Debug分析:


afb66f7b11b6ade613d705888e4182e4.png


情况 3: RUNNABLE <--> WAITING

当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING

注意是当前线程在t 线程对象的监视器上等待

t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

情况 4 RUNNABLE <--> WAITING

当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING

调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE

情况 5 RUNNABLE <--> TIMED_WAITING

t 线程用 synchronized(obj) 获取了对象锁后


调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING

t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时

竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE

竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

情况 6 RUNNABLE <--> TIMED_WAITING

当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING

注意是当前线程在t 线程对象的监视器上等待

当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从

TIMED_WAITING --> RUNNABLE

情况 7 RUNNABLE <–> TIMED_WAITING

当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING

当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

情况 8 RUNNABLE <--> TIMED_WAITING

当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线

程从 RUNNABLE --> TIMED_WAITING

调用 LockSupport.unpark(目标线程) 或调用了线程 的interrupt(),或是等待超时,会让目标线程从

TIMED_WAITING--> RUNNABLE

情况 9 RUNNABLE <--> BLOCKED

t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED

持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争

成功,从BLOCKED --> RUNNABLE,其它失败的线程仍然 BLOCKED

情况 10 RUNNABLE <--> TERMINATED

当前线程所有代码运行完毕,进入TERMINATED



相关文章
|
7天前
|
Java 程序员
从菜鸟到大神:JAVA多线程通信的wait()、notify()、notifyAll()之旅
【6月更文挑战第21天】Java多线程核心在于wait(), notify(), notifyAll(),它们用于线程间通信与同步,确保数据一致性。wait()让线程释放锁并等待,notify()唤醒一个等待线程,notifyAll()唤醒所有线程。这些方法在解决生产者-消费者问题等场景中扮演关键角色,是程序员从新手到专家进阶的必经之路。通过学习和实践,每个程序员都能在多线程编程的挑战中成长。
|
2天前
|
Java 机器人 程序员
Java中的线程通信:wait、notify与Condition详解
Java中的线程通信:wait、notify与Condition详解
|
7天前
|
安全 Java
JAVA多线程通信新解:wait()、notify()、notifyAll()的实用技巧
【6月更文挑战第20天】Java多线程中,`wait()`, `notify()`和`notifyAll()`用于线程通信。在生产者-消费者模型示例中,它们确保线程同步。`synchronized`保证安全,`wait()`在循环内防止虚假唤醒,`notifyAll()`避免唤醒单一线程问题。关键技巧包括:循环内调用`wait()`,优先使用`notifyAll()`以保证可靠性,以及确保线程安全和正确处理`InterruptedException`。
|
7天前
|
安全 Java
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
【6月更文挑战第20天】JAVA多线程中,wait(), notify(), notifyAll()是Object类的关键同步机制。wait()让线程等待并释放锁,直到被notify()或notifyAll()唤醒或超时。它们必须在同步块中使用,持有锁的线程调用。notify()唤醒一个等待线程,notifyAll()唤醒所有。最佳实践包括:与synchronized结合,循环检查条件,避免循环内notify(),通常优先使用notifyAll()。
|
6天前
|
Oracle Java 关系型数据库
面试知识点:notify是随机唤醒线程吗(唤醒线程顺序)?
面试知识点:notify是随机唤醒线程吗(唤醒线程顺序)?
11 0
|
7天前
|
Java
当JAVA多线程遇上wait()和notify():一场奇妙的邂逅
【6月更文挑战第20天】JAVA多线程中,wait()和notify()是线程通信的关键。wait()让线程释放锁进入等待,直到被notify()或notifyAll()唤醒。它们用于协调如生产者-消费者问题中的线程协作,确保在同步块内调用,并伴随条件检查以防止虚假唤醒。示例代码展示了一个简单的共享队列,其中生产和消费使用wait/notify实现同步。
|
1天前
|
数据采集 Java Unix
10-多线程、多进程和线程池编程(2)
10-多线程、多进程和线程池编程
|
1天前
|
安全 Java 调度
10-多线程、多进程和线程池编程(1)
10-多线程、多进程和线程池编程
|
6天前
|
存储 Linux C语言
c++进阶篇——初窥多线程(二) 基于C语言实现的多线程编写
本文介绍了C++中使用C语言的pthread库实现多线程编程。`pthread_create`用于创建新线程,`pthread_self`返回当前线程ID。示例展示了如何创建线程并打印线程ID,强调了线程同步的重要性,如使用`sleep`防止主线程提前结束导致子线程未执行完。`pthread_exit`用于线程退出,`pthread_join`用来等待并回收子线程,`pthread_detach`则分离线程。文中还提到了线程取消功能,通过`pthread_cancel`实现。这些基本操作是理解和使用C/C++多线程的关键。
|
9天前
|
安全 Java
【极客档案】Java 线程:解锁生命周期的秘密,成为多线程世界的主宰者!
【6月更文挑战第19天】Java多线程编程中,掌握线程生命周期是关键。创建线程可通过继承`Thread`或实现`Runnable`,调用`start()`使线程进入就绪状态。利用`synchronized`保证线程安全,处理阻塞状态,注意资源管理,如使用线程池优化。通过实践与总结,成为多线程编程的专家。

热门文章

最新文章

相关实验场景

更多