《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



相关文章
|
2月前
|
Java
【多线程面试题十六】、谈谈ReentrantLock的实现原理
这篇文章解释了`ReentrantLock`的实现原理,它基于Java中的`AbstractQueuedSynchronizer`(AQS)构建,通过重写AQS的`tryAcquire`和`tryRelease`方法来实现锁的获取与释放,并详细描述了AQS内部的同步队列和条件队列以及独占模式的工作原理。
【多线程面试题十六】、谈谈ReentrantLock的实现原理
|
1月前
|
存储 Java 程序员
优化Java多线程应用:是创建Thread对象直接调用start()方法?还是用个变量调用?
这篇文章探讨了Java中两种创建和启动线程的方法,并分析了它们的区别。作者建议直接调用 `Thread` 对象的 `start()` 方法,而非保持强引用,以避免内存泄漏、简化线程生命周期管理,并减少不必要的线程控制。文章详细解释了这种方法在使用 `ThreadLocal` 时的优势,并提供了代码示例。作者洛小豆,文章来源于稀土掘金。
|
2月前
|
Java 开发者
Java多线程教程:使用ReentrantLock实现高级锁功能
Java多线程教程:使用ReentrantLock实现高级锁功能
34 1
|
2月前
|
安全 Java 调度
【多线程面试题十】、说一说notify()、notifyAll()的区别
notify()唤醒单个等待对象锁的线程,而notifyAll()唤醒所有等待该对象锁的线程,使它们进入就绪队列竞争锁。
|
23天前
|
安全 Java
LinkedBlockingQueue 是线程安全的,为什么会有两个线程都take()到同一个对象了?
LinkedBlockingQueue 是线程安全的,为什么会有两个线程都take()到同一个对象了?
29 0
|
2月前
|
Java C++
【Java 并发秘籍】synchronized vs ReentrantLock:揭秘线程同步神器的对决!
【8月更文挑战第24天】本文详细对比了Java并发编程中`synchronized`关键字与`ReentrantLock`的不同之处。`synchronized`作为内置关键字,提供自动锁管理但不支持中断或公平锁;`ReentrantLock`则通过显式调用方法控制锁,具备更多高级功能如可中断、公平锁及条件变量。文章通过两个计数器类实例展示了两种机制的具体应用,帮助读者理解其差异及适用场景。掌握这两者对于提升多线程程序设计能力至关重要。
43 0
|
2月前
|
安全 Java C#
Spring创建的单例对象,存在线程安全问题吗?
Spring框架提供了多种Bean作用域,包括单例(Singleton)、原型(Prototype)、请求(Request)、会话(Session)、全局会话(GlobalSession)等。单例是默认作用域,保证每个Spring容器中只有一个Bean实例;原型作用域则每次请求都会创建一个新的Bean实例;请求和会话作用域分别与HTTP请求和会话绑定,在Web应用中有效。 单例Bean在多线程环境中可能面临线程安全问题,Spring容器虽然确保Bean的创建过程是线程安全的,但Bean的使用安全性需开发者自行保证。保持Bean无状态是最简单的线程安全策略;
|
2月前
|
缓存 Java 容器
多线程环境中的虚假共享是什么?
【8月更文挑战第21天】
25 0
【多线程面试题九】、说一说sleep()和wait()的区别
sleep()和wait()的主要区别在于sleep()是Thread类的静态方法,可以在任何地方使用且不会释放锁;而wait()是Object类的方法,只能在同步方法或同步代码块中使用,并会释放锁直到相应线程通过notify()/notifyAll()重新获取锁。