IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 【多线程进阶】如何保证唱跳rap打篮球的顺序 -> 正文阅读

[Java知识库]【多线程进阶】如何保证唱跳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();
    }
}

在这里插入图片描述

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

如果不break,那自然是等threadA执行完了threadB才开始执行
在这里插入图片描述

通过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我话说完

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-12-18 15:50:19  更:2021-12-18 15:50:29 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/19 7:36:20-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码