• 线程间等待与唤醒机制、单例模式、阻塞队列、定时器


    目录

    线程间等待与唤醒机制

    线程等待wait

    唤醒方法notify

    面试题:wait方法和sleep方法的区别

    练习

    单例模式 

    饿汉式单例

    懒汉式单例

    解决懒汉式的线程安全问题

    阻塞式队列

    JDK中的阻塞队列BlockingQueue

    定时器——类比现实生活中的闹钟


    线程间等待与唤醒机制

    线程间等待与唤醒机制。wait和notify是Object类的方法,用于线程的等待与唤醒。无论是wait还是notify方法,都需要搭配synchronized锁来使用(等待和唤醒,也是需要对象)。

    线程等待wait

    多线程并发的场景下,有时需要某些线程先执行,这些线程执行结束后其他线程再继续执行。

    死等,线程进入阻塞态(WAITING)直到有其他线程调用notify方法唤醒。

    等待一段时间,若在该时间内线程被唤醒,则继续执行;

    若超过相应时间还没其他线程唤醒此线程,此线程就不再等待,恢复执行。

    唤醒方法notify

    唤醒方法
    notify():随机唤醒一个处在等待状态的线程
    notifyAll():唤醒所有处在等待状态的线程

    1. private static class notifyTask implements Runnable {
    2. private Object lock;
    3. public notifyTask(Object lock) {
    4. this.lock = lock;
    5. }
    6. @Override
    7. public void run() {
    8. synchronized (lock) {
    9. System.out.println("准备唤醒");
    10. // 唤醒所有处在等待状态的线程
    11. lock.notifyAll();
    12. System.out.println("唤醒结束");
    13. }
    14. }
    15. }
    1. public class waitDemo1 {
    2. private static class waitTask implements Runnable {
    3. private Object lock;
    4. public waitTask(Object lock) {
    5. this.lock = lock;
    6. }
    7. @Override
    8. public void run() {
    9. synchronized (lock) {
    10. System.out.println(Thread.currentThread().getName() + "准备进入等待状态");
    11. try {
    12. lock.wait();
    13. } catch (InterruptedException e) {
    14. throw new RuntimeException(e);
    15. }
    16. System.out.println("等待结束,本线程继续执行");
    17. }
    18. }
    19. }
    20. private static class notifyTask implements Runnable {
    21. private Object lock;
    22. public notifyTask(Object lock) {
    23. this.lock = lock;
    24. }
    25. @Override
    26. public void run() {
    27. synchronized (lock) {
    28. System.out.println("准备唤醒");
    29. lock.notify();
    30. System.out.println("唤醒结束");
    31. }
    32. }
    33. }
    34. public static void main(String[] args) throws InterruptedException {
    35. Object lock = new Object();
    36. Thread t1 = new Thread(new waitTask(lock), "t1");
    37. Thread t2 = new Thread(new waitTask(lock), "t2");
    38. Thread t3 = new Thread(new waitTask(lock), "t3");
    39. Thread notify = new Thread(new notifyTask(lock), "notify线程");
    40. t1.start();
    41. t2.start();
    42. t3.start();
    43. Thread.sleep(100);
    44. notify.start();
    45. }
    46. }

    对于wait和notify方法,其实有一个阻塞队列和一个等待队列
    阻塞队列表示同一时间只有一个线程能获取到锁,其他线程进入阻塞队列。

    等待队列:表示线程调用wait(首先此线程要获取到锁,才能进入等待队列,最后释放锁),

    调用wait方法的线程就会进入Waiting状态,等待被其他线程唤醒(lock.notify())。

    面试题:wait方法和sleep方法的区别

    a. 若这两个方法有联系,就先答共性,再答区别;
    b. 若这两方法毫无关系,就分别介绍即可。

    作答:
    1. wait方法是Object类提供的方法,需要搭配synchronized锁来使用,调用wait方法会释放锁,线程进入WAITING状态,等待被其他线程唤醒或者超时自动唤醒,唤醒之后的线程需要再次竞争synchronized锁才能继续执行。
    2. sleep方法是Thread类提供的方法,调用sleep方法的线程进入TIMED_WAITING状态,不会释放锁,时间到自动唤醒

    练习

    求输出

    1. public class waitDemo1 {
    2. private static class waitTask implements Runnable {
    3. private Object lock;
    4. public waitTask(Object lock) {
    5. this.lock = lock;
    6. }
    7. @Override
    8. public void run() {
    9. synchronized (lock) {
    10. System.out.println(Thread.currentThread().getName() + "准备进入等待状态");
    11. // 此线程在等待lock对象的notify方法唤醒
    12. try {
    13. lock.wait();
    14. Thread.sleep(1000);
    15. } catch (InterruptedException e) {
    16. throw new RuntimeException(e);
    17. }
    18. System.out.println(Thread.currentThread().getName() + "等待结束,本线程继续执行");
    19. }
    20. }
    21. }
    22. private static class notifyTask implements Runnable {
    23. private Object lock;
    24. public notifyTask(Object lock) {
    25. this.lock = lock;
    26. }
    27. @Override
    28. public void run() {
    29. synchronized (lock) {
    30. System.out.println("准备唤醒");
    31. lock.notifyAll();
    32. System.out.println("唤醒结束");
    33. }
    34. }
    35. }
    36. public static void main(String[] args) throws InterruptedException {
    37. Object lock = new Object();
    38. Object lock2 = new Object();
    39. Thread t1 = new Thread(new waitTask(lock), "t1");
    40. Thread t2 = new Thread(new waitTask(lock2), "t2");
    41. Thread t3 = new Thread(new waitTask(lock2), "t3");
    42. Thread notify = new Thread(new notifyTask(lock2), "notify线程");
    43. t1.start();
    44. t2.start();
    45. t3.start();
    46. Thread.sleep(100);
    47. notify.start();
    48. }
    49. }

    单例模式 

    单例模式:校招中考察频率非常高的一个设计模式(共23种设计模式–编程思想,不同场景下该如何设计和实现代码的固定套路)
    所谓的单例模式保证某个类在程序中有且只有一个对象。
    现实生活中的单例:一个类只有一个对象,地球类-只有地球这一个对象,太阳类-只有太阳这一个对象。


    如何控制某个类只有一个对象呢?

    1. 要创建类的对象,通过构造方法产生对象;

    2. 构造方法若是public权限,对于类的外部,随意创建对象,无法控制对象的个数;
    3. 将构造方法私有化,类的外部彻底没法产生对象,一个对象都没有。

    1. public class SingleTon {
    2. private SingleTon() {
    3. }
    4. }

    系统默认的无参没了,对于SingleTon的外部就彻底没法产生SingleTon的对象了。


    构造方法私有化之后,对于类的外部而言就一个对象都没有。
    如何构造这唯一的对象(私有化的构造方法只能在类的内部调用),只调用一次构造方法即可。

    1. public class SingleTon {
    2. private SingleTon singleTon = new SingleTon();
    3. private SingleTon() {
    4. }
    5. }

    问题:此时这个唯一变量使用成员变量是否可行?

    X,类中的成员变量必须通过对象访问,对于SingleTon的外部压根就没对象,无法通过对象访问。

    类的外部就是要获取这个唯一的对象,才能访问,现在外部没有对象,没办法通过对象访问。

    在SingleTon类的外部访问这个唯一的对象
    直接通过getSingleTon方法获取这个唯一的对象

    1. public class SingleTon {
    2. // 唯一的这一个对象
    3. private static SingleTon SINGLETON = new SingleTon();
    4. private SingleTon() {
    5. }
    6. // 调用此方法时,singleTon已经产生了
    7. public static SingleTon getSingleTon() {
    8. return singleTon;
    9. }
    10. }

    单例模式三步走
    1. 构造私有化(保证对象的产生个数);
    2. 单例类的内部提供这个唯一的对象(static);
    3. 单例类提供返回这个唯一的对象的静态方法供外部使用。

    饿汉式单例

    天然线程安全
    系统初始化JVM加载类的过程中就创建了这个唯一的对象

    1. /**
    2. * 饿汉式单例。(类加载就产生这个唯一对象)饥不择食,这个类一加载就把唯一的这个对象产生了。
    3. * 我也不管外部到底用不用这个对象,只要这个类加载到JVM,唯一对象就会产生
    4. */
    5. public class SingleTon {
    6. private static SingleTon singleTon = new SingleTon();
    7. private SingleTon() {
    8. }
    9. public static SingleTon getSingleTon() {
    10. return singleTon;
    11. }
    12. }
    13. package thread.single;
    14. public class Main {
    15. public static void main(String[] args) {
    16. SingleTon s1 = SingleTon.getSingleTon();
    17. SingleTon s2 = SingleTon.getSingleTon();
    18. SingleTon s3 = SingleTon.getSingleTon();
    19. System.out.println(s1 == s1);
    20. System.out.println(s1 == s3);
    21. }
    22. }

    懒汉式单例

    只有第一次调用getSingleTon方法,表示外部需要获取这个单例对象时才产生对象

    未优化代码

    1. /**
    2. * 懒汉式单例
    3. */
    4. public class LazySingleTon {
    5. private static LazySingleTon singleTon;
    6. private LazySingleTon() {
    7. }
    8. // 第一次调用获取单例对象方法时才实例化对象
    9. public static LazySingleTon getSingleTon() {
    10. if (singleTon == null) {
    11. singleTon = new LazySingleTon();
    12. }
    13. return singleTon;
    14. }
    15. }

    系统初始化时,外部不需要这个单例对象,就先不产生,只有当外部需要此对象才实例化对象。这种操作称之为懒加载

    例如:HashMap中

    懒加载,只有需要给map中添加元素时,表示此时需要table数组,才初始化数组大小为16。


    问题:多线程场景下是否能确保只有一个对象产生了?

    答:饿汉模式可以,懒汉模式下不能。

    饿汉

     懒汉

    解决懒汉式的线程安全问题

    1. 最简单粗暴的方式,直接在静态方法上加锁


    优化刚才的方法锁

    当t1先进入同步代码块之后,t2和t3卡在获取锁的位置,t1产生对象后,锁释放;
    t2和t3还是从获取锁的位置继续执行,t2和t3就会再次new对象


    double-check优化

    在同步代码块内部需要再次检查singleTon是否为空,防止其他线程恢复执行后多次创建单例对象。

    问题:不使用double-check,直接把if写道synchronized里面不应该更简单一点吗?

    答:这个单例只是最核心的代码,单例模式还有很多其他操作,为了保证其他操作尽可能的并发执行。

    双重加锁,使用volatile关键字保证单例对象的初始化不被中断

    1. /**
    2. * 懒汉式单例
    3. */
    4. public class LazySingleTon {
    5. private static volatile LazySingleTon singleTon;
    6. private LazySingleTon() {
    7. int x = 10;
    8. int y = 20;
    9. int z = 30;
    10. }
    11. public static LazySingleTon getSingleTon() {
    12. if (singleTon == null) {
    13. synchronized (LazySingleTon.class) {
    14. if (singleTon == null) {
    15. singleTon = new LazySingleTon();
    16. }
    17. }
    18. }
    19. return singleTon;
    20. }
    21. }

    请写出单例模式
    1. 如果你对double - check的理解到位了,直接写懒汉式的double - check。

    2. 如果感觉稍微有点慌,就写个饿汉。

    阻塞式队列

    和普通队列最大的区别在于入队和出队会阻塞:
    入队时,若队列已满,则入队操作会"阻塞",直到有其他线程从队列中取出元素;

    出队时,若队列为空,则出队操作会"阻塞",直到有其他线程向队列中添加元素。

    生产者消费者模型

    例如拍卖、秒杀场景——流量削峰

    10w个用户等待拍卖,此时将支付请求交给队列,服务器不直接处理支付逻辑,有专门处理支付逻辑的程序从队列中取出请求依次处理。

    JDK中的阻塞队列BlockingQueue

    入队方法put()阻塞式入队方法,出队方法take()阻塞式的出队。

    通过 锁 + wait 和notify 机制实现阻塞队列。

    常用子类
    ArrayBlockingQueue

    LinkedBlockingQueue

    1. public class Test {
    2. public static void main(String[] args) throws InterruptedException {
    3. BlockingQueue<Integer> blockingQueue = new LinkedBlockingDeque<>();
    4. blockingQueue.put(1);
    5. System.out.println(blockingQueue.take());
    6. }
    7. }

    1. public class Test {
    2. public static void main(String[] args) throws InterruptedException {
    3. BlockingQueue<Integer> blockingQueue = new LinkedBlockingDeque<>();
    4. // 当阻塞队列为空,take方法就会阻塞
    5. System.out.println(blockingQueue.take());
    6. blockingQueue.put(1);
    7. }
    8. }

    take方法一直处在阻塞中。


    1. import java.util.Random;
    2. import java.util.concurrent.BlockingQueue;
    3. import java.util.concurrent.LinkedBlockingDeque;
    4. public class Test2 {
    5. public static void main(String[] args) throws InterruptedException {
    6. BlockingQueue<Integer> blockingQueue = new LinkedBlockingDeque<>(3);
    7. Thread customer = new Thread(() -> {
    8. while (true) {
    9. try {
    10. // 当阻塞队列为空,take方法就会阻塞
    11. int val = blockingQueue.take();
    12. System.out.println("消费元素:" + val);
    13. } catch (InterruptedException e) {
    14. throw new RuntimeException(e);
    15. }
    16. }
    17. }, "消费者");
    18. Random random = new Random();
    19. Thread producer = new Thread(() -> {
    20. while (true) {
    21. try {
    22. int val = random.nextInt(100);
    23. // 当队列已满,put方法就会阻塞
    24. blockingQueue.put( val);
    25. System.out.println("生产元素:" + val);
    26. Thread.sleep(100);
    27. } catch (InterruptedException e) {
    28. throw new RuntimeException(e);
    29. }
    30. }
    31. }, "生产者");
    32. customer.start();
    33. producer.start();
    34. }
    35. }


    阻塞队列的大小一般通过构造方法传入,没有参数就是无界队列

    1. import java.util.Random;
    2. import java.util.concurrent.BlockingQueue;
    3. import java.util.concurrent.LinkedBlockingDeque;
    4. public class Test2 {
    5. public static void main(String[] args) throws InterruptedException {
    6. BlockingQueue<Integer> blockingQueue = new LinkedBlockingDeque<>(3);
    7. Thread customer = new Thread(() -> {
    8. while (true) {
    9. try {
    10. // 当阻塞队列为空,take方法就会阻塞
    11. int val = blockingQueue.take();
    12. System.out.println("消费元素:" + val);
    13. } catch (InterruptedException e) {
    14. throw new RuntimeException(e);
    15. }
    16. }
    17. }, "消费者");
    18. Random random = new Random();
    19. Thread producer = new Thread(() -> {
    20. while (true) {
    21. try {
    22. int val = random.nextInt(100);
    23. // 当队列已满,put方法就会阻塞
    24. blockingQueue.put( val);
    25. System.out.println("生产元素:" + val);
    26. Thread.sleep(100);
    27. } catch (InterruptedException e) {
    28. throw new RuntimeException(e);
    29. }
    30. }
    31. }, "生产者");
    32. // customer.start();
    33. producer.start();
    34. }
    35. }

    定时器——类比现实生活中的闹钟

    设定一个时间以及一个相应的任务

    如:三分钟后播放电影

    在web编程部分,检测客户端的连接,500ms之后没有收到数据,断开连接;
    LRU缓存希望某个键值对3s之后就过期(删除)。


    JDK中使用Timer类描述定时器

    核心方法就是schedule方法,两个参数(指定时间到了要执行的任务,等待时间- ms)。

    1. 延迟3s之后执行TimerTask任务

    1. public class TimerTest {
    2. public static void main(String[] args) {
    3. Timer timer = new Timer();
    4. // 3s之后执行此任务
    5. timer.schedule(new TimerTask() {
    6. @Override
    7. public void run() {
    8. System.out.println("hello");
    9. }
    10. },3000);
    11. }
    12. }

     

    2. 延迟3s之后开始执行任务,该任务启动之后每隔1s就会再次执行

    1. public class TimerTest {
    2. public static void main(String[] args) {
    3. Timer timer = new Timer();
    4. // 3s之后执行此任务
    5. timer.schedule(new TimerTask() {
    6. @Override
    7. public void run() {
    8. System.out.println("hello");
    9. }
    10. },3000,1000);
    11. }
    12. }

    单位都是ms,参数:(要执行的任务,延迟多久开始执行,每隔多久执行一次)。

  • 相关阅读:
    移动终端数据业务高安全通信方案研究
    OAuth2授权服务器Id Server一键生成配置原理
    经济型EtherCAT运动控制器(七):运动缓冲
    智慧城市与智慧乡村:共创城乡一体化新局面
    SpringBoot限制接口访问频率 - 这些错误千万不能犯
    LCR 002. 二进制求和
    将list中的对象转换为另一个类型的对象
    VUE3学习小记(2)- ref 与 reactive
    LeetCode-1030. 距离顺序排列矩阵单元格_Python
    英国公派访问学者带家属签证经验分享
  • 原文地址:https://blog.csdn.net/XHT117/article/details/125455382