【多线程进阶】如何保证唱跳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
到底如何保证线程安全,总结得太好了。。
一、线程安全等级 之前的博客中已有所提及“线程安全”问题,一般我们常说某某类是线程安全的,某某是非线程安全的。其实线程安全并不是一个“非黑即白”单项选择题。
999 0
到底如何保证线程安全,总结得太好了。。
|
3月前
|
Java 程序员 编译器
【多线程-从零开始-叁】线程的核心操作
【多线程-从零开始-叁】线程的核心操作
29 0
|
5月前
|
安全 Java 调度
震撼揭秘!手撕并发编程迷雾,Semaphore与CountDownLatch携手AQS共享模式,让你秒懂并发神器背后的惊天秘密!
【8月更文挑战第4天】在Java并发编程中,AbstractQueuedSynchronizer (AQS) 是核心框架,支持独占锁与共享锁的实现。本文以Semaphore与CountDownLatch为例,深入解析AQS共享模式的工作原理。Semaphore通过AQS管理许可数量,控制资源的并发访问;而CountDownLatch则利用共享计数器实现线程间同步。两者均依赖AQS提供的tryAcquireShared和tryReleaseShared方法进行状态管理和线程调度,展示了AQS的强大功能和灵活性。
48 0
|
8月前
|
存储 缓存 Java
剑指JUC原理-10.并发编程大师的原子累加器底层优化原理(与人类的优秀灵魂对话)
剑指JUC原理-10.并发编程大师的原子累加器底层优化原理(与人类的优秀灵魂对话)
53 1
|
8月前
|
设计模式 前端开发 JavaScript
一秒开挂!工厂模式让你告别重复代码!
欢迎来到前端入门之旅!这个专栏是为那些对Web开发感兴趣、刚刚开始学习前端的读者们打造的。无论你是初学者还是有一些基础的开发者,我们都会在这里为你提供一个系统而又亲切的学习平台。我们以问答形式更新,为大家呈现精选的前端知识点和最佳实践。通过深入浅出的解释概念,并提供实际案例和练习,让你逐步建立起一个扎实的基础。无论是HTML、CSS、JavaScript还是最新的前端框架和工具,我们都将为你提供丰富的内容和实用技巧,帮助你更好地理解并运用前端开发中的各种技术。
|
8月前
|
Go
一图胜千言,帮你搞懂Go面试中常问的channel问题!
一图胜千言,帮你搞懂Go面试中常问的channel问题!
126 0
|
数据采集 算法 Java
库调多了,都忘了最基础的概念-《线程池篇》
库调多了,都忘了最基础的概念-《线程池篇》
139 0
库调多了,都忘了最基础的概念-《线程池篇》
|
监控
【多线程:犹豫模式】
【多线程:犹豫模式】
139 0
多线程:(充分利用定义任务后,开启多线程实现任务的理解)题目:模拟三个老师同时给50个小朋友发苹果,每个老师相当于一个线程。
多线程:(充分利用定义任务后,开启多线程实现任务的理解)题目:模拟三个老师同时给50个小朋友发苹果,每个老师相当于一个线程。
722 0
多线程:(充分利用定义任务后,开启多线程实现任务的理解)题目:模拟三个老师同时给50个小朋友发苹果,每个老师相当于一个线程。