【多线程进阶】如何保证唱跳rap打篮球的顺序

简介: 一个线程负责学习唱跳rap,一个线程负责学习打篮球,而这两个线程的调度是随机的,顺序由操作系统决定,我们怎么保证阿鸡 学会唱跳rap后,再学打篮球

前言阿巴,阿巴阿巴阿巴阿巴阿巴,阿巴,阿巴阿巴

怕你们学不会,又花了几根头发想出这个demo


最近有这么个需求(真实场景)

我开了10个线程同时给用户发送消息,10分钟发一次,这其中有9个业务线程负责发消息,而剩下一个打杂线程用来获取最新消息策略(所谓策略就是指定给哪些用户发;通过哪些途径发,微信、邮件、短信等等),每次都要获取的原因是策略可能随时有变动,我设计成每次获取最新的,就能实现不重启项目灵活更改策略(策略存在数据库中)

问题来了

每个业务线程发消息都要用到最新策略,所以必须让打杂线程先执行完,而线程的调度是随机的,执行顺序由操作系统决定,我怎么让业务线程在打杂线程执行完了再执行?

这就要牵涉到线程间的通讯了,举这个栗子目的是为了让大家对线程间通讯在实际项目中的运用有个大致了解

这个场景真实存在,不过没看懂没关系

熟悉我的朋友都知道,博主暖男嘛,举的栗子当然不会这么枯燥乏味

所以

今天的主角是:阿鸡


对不起了阿鸡,我不得不这么做

故事背景:阿鸡需要先学会唱跳rap,然后开始学打篮球

翻译成多线程:一个线程负责学习唱跳rap,一个线程负责学习打篮球,而这两个线程的调度是随机的,顺序由操作系统决定,我们怎么保证阿鸡 学会唱跳rap后,再学打篮球

方法有很多,我这里列举几个常用的,足够应付所有场景了

  • 基于join
  • 基于volatile
  • 基于synchronized
  • 基于reentrantLock
  • 基于countDownLatch


不多逼逼,上才艺

先来个入门的:join

join是Thread类的方法,底层基于wait+notify,你可以把这个方法理解成插队,谁调用谁插队,但有局限性

适用于线程较少的场景,如果线程多了会造成无限套娃,有点麻烦,不够优雅

publicclassJoinTest {
   // 用来记录啊鸡学习时间
   staticdouble year;

   publicstaticvoidmain(String[] args) {
       //线程A,练习唱跳rap
       Thread threadA = new Thread(() -> {
           for (year = 0.5; year <= 5; year += 0.5) {
               System.out.println("开始练习唱跳rap:已练习" + year + "年");
               try {
                   Thread.sleep(288);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               //众所周知,练习两年半即可出道
               if (year == 2.5) {
                   System.out.println("===========================>练习时长两年半,出道!!!");
                   //留意下这个break,想想如果不break会怎样
                   break;
               }
           }
       });
       //线程B,练习打篮球
       Thread threadB = new Thread(() -> {
           try {
            // 让threadA线程插队,threadB执行到这儿时会被阻塞,直到threadA执行完
               threadA.join();
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           System.out.println("开始练习打篮球");
       });
       // 启动线程
       threadA.start();
       threadB.start();
   }
}

微信截图_20211223114915.png

不管运行多少次,结果都一样,今天就是耶稣来了也一样,我说的

如果不break,那自然是等threadA执行完了threadB才开始执行

微信截图_20211223114927.png

通过volatile

这种实现比较简单,也很好理解,但是性能不咋地,会抢占很多cpu资源,如非必要,不要用

publicclassVolatileTest {
   //定义一个共享变量用来线程间通信,注意用volatile修饰,保证它内存可见
   staticvolatileboolean flag = false;
   staticdouble year;

   publicstaticvoidmain(String[] args) {
       //线程A,练习唱跳rap
       Thread threadA = new Thread(() -> {
           while (true) {
               if (!flag) {
                   for (year = 0.5; year <= 5; year += 0.5) {
                       System.out.println("开始练习唱跳rap:已练习" + year + "年");
                       try {
                           Thread.sleep(288);
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
                       //众所周知,练习两年半即可出道
                       if (year == 2.5) {
                           System.out.println("===========================>练习时长两年半,出道!!!");
                           // 通知threadB你可以执行了
                           flag = true;
                           //同样留意这个break
                           break;
                       }
                   }
                   break;
               }
           }
       });
       //线程B,练习打篮球
       Thread threadB = new Thread(() -> {
           while (true) {
            // 监听flag
               if (flag) {
                   System.out.println("开始练习打篮球");
                   break;
               }
           }
       });
       threadA.start();
       threadB.start();
   }
}

结果与上面第一个一样,就不展示了 关于break,我这里先不说,你先猜猜结果,再复制demo去跑跑,一定要动手

synchronized、wait()、notify()三件套

wait() 和 notify()都是Object类的通讯方法,注意一点,wait和 notify需搭配synchronized使用,注意,notify不会释放锁,至于不会释放锁体现在哪儿,这个demo下面有说明

publicclassSynchronizedTest {
   staticdouble year;

   publicstaticvoidmain(String[] args) {
       SynchronizedTest sync= new SynchronizedTest();
       sync.execute();
   }

   publicvoidexecute() {
       //线程A,练习唱跳rap
       Thread threadA = new Thread(() -> {
           synchronized (this) {
               for (year = 0.5; year <= 5; year += 0.5) {
                   try {
                       System.out.println("开始练习唱跳rap:已练习" + year + "年");
                       Thread.sleep(288);
                       if (year == 2.5) {
                           System.out.println("===========================>练习时长两年半,出道!!!");
                           //唤醒等待中的threadB,但threadB不会立马执行,而是等待threadA执行完,因为notify不会释放锁
                           notify();
                           break;
                       }
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
           }
       });
       //线程B,练习打篮球
       Thread threadB = new Thread(() -> {
           synchronized (this) {
               try {
                   wait();
                   System.out.println("开始练习打篮球");
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       });
       //注意,一定要先启动B,不然会导致B永远阻塞
       threadB.start();
       threadA.start();
   }
}

这个threadA里面的break一定要多想想,跑一跑你就知道啥叫不会释放锁 如果没有break,threadA在唤醒threadB后,会继续执行自己的逻辑,等自己执行完了才会释放锁,这时候threadB才开始执行

基于ReentrantLock

ReentrantLock是juc包下的并发工具,也能实现,但相对复杂,需结合Condition的await和signal,底层原理有点像上面的wait和notify

这里留个课后作业:思考一下为什么unlock要放在finally里面?

publicclassReentrantLockTest {
   staticdouble year;

   publicstaticvoidmain(String[] args) {
    //实例化一个锁和Condition
       ReentrantLock lock = new ReentrantLock();
       Condition condition = lock.newCondition();
       //线程A,练习唱跳rap
       Thread threadA = new Thread(() -> {
           lock.lock();
           try {
               for (year = 0.5; year <= 5; year += 0.5) {
                   System.out.println("开始练习唱跳rap:已练习" + year + "年");
                   Thread.sleep(288);
                   //众所周知,练习两年半即可出道
                   if (year == 2.5) {
                       System.out.println("===========================>练习时长两年半,出道!!!");
                       //唤醒等待中的线程
                       condition.signal();
                       //这里的break也是个彩蛋,去掉它触发隐藏关卡
                       break;
                   }
               }
           } catch (InterruptedException e) {
               e.printStackTrace();
           } finally {
               //解锁
               lock.unlock();
           }
       });
       //线程B,练习打篮球
       Thread threadB = new Thread(() -> {
           lock.lock();
           try {
               //让当前线程等待
               condition.await();
               System.out.println("开始练习打篮球");
           } catch (Exception e) {
               e.printStackTrace();
           } finally {
               lock.unlock();
           }
       });
       //必须保证B先拿到锁,不然会导致A永远阻塞
       threadB.start();
       threadA.start();
   }
}

基于CountDownLatch

这也是juc包下的并发工具,主要有两个常用方法,countDown和await 简单说下原理:CountDownLatch底层维护了一个计数器count,在实例化的时候设置,当调用countDown方法时,count减一,如果count在减一前已经为0,那么什么都不会发生,如果减一后变成0,则唤醒所有等待的线程;await方法会使当前线程等待,直到count为0

publicclassCountDownLatchTest {
   staticdouble year;

   publicstaticvoidmain(String[] args) {
    //实例化一个CountDownLatch,count设置为1,也就是说,只要调用一次countDown方法就会唤醒线程
       CountDownLatch latch = new CountDownLatch(1);
       //线程A,练习唱跳rap
       Thread threadA = new Thread(() -> {
           for (year = 0.5; year <= 5; year += 0.5) {
               System.out.println("开始练习唱跳rap:已练习" + year + "年");
               try {
                   Thread.sleep(288);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               //众所周知,练习两年半即可出道
               if (year == 2.5) {
                   System.out.println("===========================>练习时长两年半,出道!!!");
                   //计数器减一
                   latch.countDown();
                   //老规矩,去掉break触发隐藏关卡
                   break;
               }
           }
       });
       //线程B,练习打篮球
       Thread threadB = new Thread(() -> {
           try {
               //阻塞当前线程,计数器为0时被唤醒
               latch.await();
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           System.out.println("开始练习打篮球");
       });
       threadA.start();
       threadB.start();
   }
}


打完收工


上面五个demo要是都看懂了的话,你对多线程这块也算是比较熟了,恭喜!!!

demo的threadA 中都有break,这是我专门设计的,多观察下有break和没有break的运行结果,相信你会很有收获


ok我话说完

相关文章
|
存储 Java API
写给大忙人看的 - Java中从MinIO服务器中下载文件(3)
前面两章介绍了 MinIO 文件服务器的环境搭建,以及在 Java 中上传文件至 MinIO 文件服务器中,现在,一起来看下如何从 MinIO 文件服务器中下载文件吧
2940 0
|
XML 存储 安全
深入理解HttpSecurity的设计
介绍了基于配置文件的使用方式以及实现细节,如下:
353 0
|
9月前
|
自然语言处理 开发工具 Android开发
百宝箱开放平台 ✖️ 友盟+ SDK 接入准备
开发者可通过集成SDK,将百宝箱智能体接入友盟App,实现智能答疑与数据分析。本文详述在友盟创建App、获取Appkey,及在百宝箱创建智能体、获取TboxAgentID的完整流程,并提供iOS与Android平台集成指引,助力提升应用智能化服务能力。(239字)
418 0
百宝箱开放平台 ✖️ 友盟+ SDK 接入准备
|
10月前
|
JSON 监控 API
小红书:对接苹果支付API满足iOS用户习惯,提升转化率
小红书集成Apple Pay可显著提升iOS用户支付体验,简化流程、增强安全、提高转化率。本文详解从开发配置、代码实现到后端验证与优化策略的全流程,助力高效落地,推动业务增长。(238字)
|
5月前
|
存储 机器学习/深度学习 人工智能
阿里云服务器第九代计算型c9i实例解析:至强6处理器,AI加速40%,安全升级
阿里云服务器计算型c9i实例,搭载英特尔®至强®6处理器,单核算力提升20%,采用CIPU架构降低网络延迟、提升存储IOPS,实现高性能与高能效的平衡,适用于在线游戏、视频编解码等场景。c9i实例在计算、网络、存储性能上全方位提升。目前,c9i实例2核4G、4核8G、8核16G配置有优惠,用户还可领取额外优惠券享受减免。
|
前端开发 JavaScript vr&ar
WebGL
WebGL(Web Graphics Library)是一种用于在网页上渲染交互式 3D 和 2D 图形的 JavaScript API,它基于 OpenGL ES 2.0 标准,为开发者提供了一种在浏览器中创建高性能图形应用的途径
|
AI芯片
AIGC视频生成/编辑技术调研报告
随着图像生成领域的研究飞速发展,基于diffusion的生成式模型取得效果上的大突破。在图像生成/编辑产品大爆发的今天,视频生成/编辑技术也引起了学术界和产业界的高度关注。该分享主要介绍视频生成/编辑的研究现状,包括不同技术路线的优劣势,以及该领域当下面临的核心问题与挑战。
2338 3
AIGC视频生成/编辑技术调研报告
|
人工智能 小程序 开发者
【一步步开发AI运动APP】六、运动计时计数能调用
本文章介绍了如何通过【一步步开发AI运动APP】系列博文,利用uniAPP插件开发高性能的AI运动应用。文中详细说明了创建运动分析器、进行运动分析、监听计数变化以及停止/重置分析等功能实现步骤。插件内置多种常见运动(如跳绳、俯卧撑等),支持自定义扩展,满足健身、体测等场景需求。示例代码展示了人体检测、运动计时计数及UI更新的完整流程,帮助开发者快速上手并深耕AI运动领域。
|
运维 负载均衡 监控
【软件设计师备考 专题 】网络性能分析
【软件设计师备考 专题 】网络性能分析
512 0