《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



相关文章
|
5天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
25 9
|
8天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
22 3
|
23天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
15 1
|
23天前
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
24 1
|
23天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
33 1
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
43 1
C++ 多线程之初识多线程
|
23天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
16 3
|
23天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
16 2
|
23天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
28 2
|
23天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
28 1