李锋镝的博客

  • 首页
  • 时间轴
  • 说说
  • 系列文章
  • AI工具集
  • 论坛
  • 左邻右舍
    • 左邻右舍
    • 朋友圈
    • 游客中心
  • 留言
    • 留言
    • 走心评论
  • 关于
    • 关于本站
    • Now
    • 每日心情
    • 网站地图
    • 网站统计
    • 赞助
  • 星球穿梭
  • 🚇开往
白雪成红
惟坚韧者始能遂其志
  1. 首页
  2. 转载
  3. 技术
  4. 正文

Java并发编程之如何保证线程顺序执行

2021年5月18日 约 1,490 字5 分钟 201 0 0
本文最后更新于 2021年5月18日,距今已 1977 天,其中的信息可能已经发生变化,请注意甄别。

背景

只要了解过多线程,我们就知道线程开始的顺序跟执行的顺序是不一样的。如果只是创建三个线程然后执行,最后的执行顺序是不可预期的。这是因为在创建完线程之后,线程执行的开始时间取决于CPU何时分配时间片,线程可以看成是相对于的主线程的一个异步操作。

public class FIFOThreadExample {
    public synchronized static void foo(String name) {
        System.out.print(name);
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> foo("A"));
        Thread thread2 = new Thread(() -> foo("B"));
        Thread thread3 = new Thread(() -> foo("C"));
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

输出结果:ACB/ABC/CBA...

那么我们该如何保证线程的顺序执行呢?

如何保证线程的顺序执行?

  1. 使用Thread.join()实现
    Thread.join()的作用是让父线程等待子线程结束之后才能继续运行。以上述例子为例,main()方法所在的线程是父线程,在其中我们创建了3个子线程A,B,C,子线程的执行相对父线程是异步的,不能保证顺序性。而对子线程使用Thread.join()方法之后就可以让父线程等待子线程运行结束后,再开始执行父线程,这样子线程执行被强行变成了同步的,我们用Thread.join()方法就能保证线程执行的顺序性。

    public class FIFOThreadExample {
    
    public static void foo(String name) {
        System.out.print(name);
    }
    
    public static void main(String[] args) throws InterruptedException{
        Thread thread1 = new Thread(() -> foo("A"));
        Thread thread2 = new Thread(() -> foo("B"));
        Thread thread3 = new Thread(() -> foo("C"));
        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
        thread3.start();
    }
    }

    输出结果:ABC

  2. 使用单线程线程池来实现
    另一种保证线程顺序执行的方法是使用一个单线程的线程池,这种线程池中只有一个线程,相应的,内部的线程会按加入的顺序来执行。

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    public class FIFOThreadExample {
    
    public static void foo(String name) {
        System.out.print(name);
    }
    
    public static void main(String[] args) throws InterruptedException{
        Thread thread1 = new Thread(() -> foo("A"));
        Thread thread2 = new Thread(() -> foo("B"));
        Thread thread3 = new Thread(() -> foo("C"));
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.submit(thread1);
        executor.submit(thread2);
        executor.submit(thread3);
        executor.shutdown();
    }
    }

    输出结果:ABC

  3. 使用volatile关键字修饰的信号量实现
    上面两种的思路都是让保证线程的执行顺序,让线程按一定的顺序执行。这里介绍第三种思路,那就是线程可以无序运行,但是执行结果按顺序执行。
    你应该可以想到,三个线程都被创建并start(),这时候三个线程随时都可能执行run()方法。因此为了保证run()执行的顺序性,我们肯定需要一个信号量来让线程知道在任意时刻能不能执行逻辑代码。
    另外,因为三个线程是独立的,这个信号量的变化肯定需要对其他线程透明,因此volatile关键字也是必须要的。

public class TicketExample2 {

    //信号量
    static volatile int ticket = 1;
    //线程休眠时间
    public final static int SLEEP_TIME = 1;

    public static void foo(int name){
        //因为线程的执行顺序是不可预期的,因此需要每个线程自旋
        while (true) {
            if (ticket == name) {
                try {
                    Thread.sleep(SLEEP_TIME);
                    //每个线程循环打印3次
                    for (int i = 0; i < 3; i++) {
                        System.out.println(name + " " + i);
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //信号量变更
                ticket = name%3+1;
                return;

            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> foo(1));
        Thread thread2 = new Thread(() -> foo(2));
        Thread thread3 = new Thread(() -> foo(3));
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

执行结果:
1 0
1 1
1 2
2 0
2 1
2 2
3 0
3 1
3 2

  1. 使用Lock和信号量实现
    此种方法的思想跟第三种方法是一样的,都是不考虑线程执行的顺序而是考虑用一些方法控制线程执行业务逻辑的顺序。这里我们同样用一个原子类型信号量ticket,当然你可以不用原子类型,这里我只是为了保证自增操作的线程安全。然后我们用了一个可重入锁ReentrantLock。用来给方法加锁,当一个线程拿到锁并且标识位正确的时候开始执行业务逻辑,执行完毕后唤醒下一个线程。
    这里我们不需要使用while进行自旋操作了,因为Lock可以让我们唤醒指定的线程,所以改成if就可以实现顺序的执行。

    public class TicketExample3 {
    //信号量
    AtomicInteger ticket = new AtomicInteger(1);
    public Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private Condition[] conditions = {condition1, condition2, condition3};
    
    public void foo(int name) {
        try {
            lock.lock();
            //因为线程的执行顺序是不可预期的,因此需要每个线程自旋
            System.out.println("线程" + name + " 开始执行");
            if(ticket.get() != name) {
                try {
                    System.out.println("当前标识位为" + ticket.get() + ",线程" + name + " 开始等待");
                    //开始等待被唤醒
                    conditions[name - 1].await();
                    System.out.println("线程" + name + " 被唤醒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(name);
            ticket.getAndIncrement();
            if (ticket.get() > 3) {
                ticket.set(1);
            }
            //执行完毕,唤醒下一次。1唤醒2,2唤醒3
            conditions[name % 3].signal();
        } finally {
            //一定要释放锁
            lock.unlock();
        }
    
    }
    
    public static void main(String[] args) throws InterruptedException {
        TicketExample3 example = new TicketExample3();
        Thread t1 = new Thread(() -> {
            example.foo(1);
        });
        Thread t2 = new Thread(() -> {
            example.foo(2);
        });
        Thread t3 = new Thread(() -> {
            example.foo(3);
        });
        t1.start();
        t2.start();
        t3.start();
    }
    }

    输出结果:
    线程2 开始执行
    当前标识位为1,线程2 开始等待
    线程1 开始执行
    1
    线程3 开始执行
    当前标识位为2,线程3 开始等待
    线程2 被唤醒
    2
    线程3 被唤醒
    3

上述的执行结果并非唯一,但可以保证打印的顺序一定是123这样的顺序。

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
分享到

Java并发编程之如何保证线程顺序执行

也可使用浏览器菜单中的「分享」功能

微信扫一扫分享

标签: JAVA 多线程
最后更新:2021年5月18日

岁月同一天 10 月 17 日

回望过去的今天,你在写什么

  • AI
    1 年前 2025年10月17日
    从技术本质到生态关联,一文吃透 AIGC、RAG、Agent、Function Call、MCP

    当ChatGPT帮你写周报、MidJourney生成海报、AI助手自动规划旅行时,你可能没意识到:这些看似连贯的智能体验…

  • 中间件
    1 年前 2025年10月17日
    10 个MQ高频业务场景深度解析

    在分布式系统开发中,很多开发者都会疑惑:“明明直接调用接口就能实现功能,为什么还要引入MQ这个‘中间商’?”其实,当系统…

  • 7 年前 2019年10月17日
    从零开始入门 K8s | Kubernetes 网络概念及策略控制

    一、Kubernetes 基本网络模型 本文来介绍一下 Kubernetes 对网络模型的一些想法。大家知道 Kuber…

相关文章
  • jmap命令(jdk1.8)2020年9月27日
  • ZGC介绍2025年2月15日
  • 了解一下Spring中用了哪些设计模式2019年10月20日
  • SpringBoot定时任务 - 经典定时任务设计:时间轮(Timing Wheel)案例和原理2022年8月4日
  • 别再背线程池的七大参数了,现在面试官都这么问2025年5月15日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >
1234567891112131415161718192021222324252627282930313233343536373839404142434446474849505152535455575859606162636465666769727476777879808182858687909293949596979899
取消回复
…

文章评论

还没有评论,快来抢沙发吧~

苟利国家生死以,岂因祸福避趋之。

听点儿音乐吧 朋友~
文章目录
最新 热点 随机
最新 热点 随机
Markdown 的前世今生 讨论下XXL-JOB GLUE模式的安全问题 心气没了,干什么都不得劲 Kratos-plus v1.1.26版本更新说明 我觉得是时候公布我的导航网站了 代码是 AI 写的,生产事故谁背锅?
给主题增加了游客中心功能心气没了,干什么都不得劲我觉得是时候公布我的导航网站了游本昌去世关于主题加载速度优化的一点儿小演进给主题增加了Now、每日心情、年度回顾、岁月同一天、随机漫步等功能
TransmittableThreadLocal介绍与使用 从零搭建Spring Cloud Gateway网关(二)—— 打印请求响应日志 本来想把主题上传到WordPress官方主题商店的,结果懵逼了…… UUID太长怎么办?快来试试NanoId 因在公司上不正经网站,我没过试用期… 今天七夕节呀~~
最近评论
李锋镝 发布于 13 小时前(10月16日) 其实语法很好记,而且也有很多markdown的文本编辑器
李锋镝 发布于 13 小时前(10月16日) Typecho就是太轻量了,导致很多功能都没有
沐阳 发布于 13 小时前(10月16日) 换了Markdown,好多代码都要重新记
不凡 发布于 16 小时前(10月16日) 我搭建临时站点看了下,是有个预览,然后发现是主题对markdown编辑器做了“二开”加了一些自定义按...
不凡 发布于 16 小时前(10月16日) 我开发的插件替代了原生附件管理,在附件管理显示最近上传的文件,点击缩略图就直接插入了,不过插入的仍然...
标签聚合
Spring 分布式 数据库 Theme JVM AI AI编程 Claude ElasticSearch K8s MySQL SQL Redis 日常 WordPress 多线程 SpringBoot IDEA JAVA 架构
友情链接
  • 懋和道人
  • 刘永博客
  • 韩情脉脉
  • 韩小韩博客
  • 瓦匠个人小站
  • 彬红茶日记
  • 哥斯拉
  • 林羽凡
  • 皮皮社
  • 志文工作室
  • Honesty
  • 搬砖日记
  • Mr.Sun的博客
  • 九仞之行
  • 老张博客
  • Serendipity
  • sssr7844的博客
  • lijie blog
  • 知向前端

COPYRIGHT © 2016-2026 lifengdi.com. ALL RIGHTS RESERVED.

lifengdi.com 十年之约虫洞

Domain age badge for lifengdi.com

Theme Kratos-plus By Dylan Li

津ICP备2024022503号-3

京公网安备11011502039375号