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

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

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


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

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

问题来了

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

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

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

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

所以

今天的主角是:阿鸡


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

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

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

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

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


不多逼逼,上才艺

先来个入门的:join

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

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

public class JoinTest {
   // 用来记录啊鸡学习时间
   static double year;

   public static void main(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();
   }
}

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

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

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

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

通过volatile

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

public class VolatileTest {
   //定义一个共享变量用来线程间通信,注意用volatile修饰,保证它内存可见
   static volatile boolean flag = false;
   static double year;

   public static void main(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下面有说明

public class SynchronizedTest {
   static double year;

   public static void main(String[] args) {
       SynchronizedTest sync= new SynchronizedTest();
       sync.execute();
   }

   public void execute() {
       //线程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里面?

public class ReentrantLockTest {
   static double year;

   public static void main(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

public class CountDownLatchTest {
   static double year;

   public static void main(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我话说完

上一篇:Command line is too long. Shorten command line for AppWebStarter or...(已解决)


下一篇:Spring Security 实战干货: 简单的认识 OAuth2.0 协议