Java中保证线程顺序执行的操作代码

 更新时间:2021年05月15日 09:21:00   作者:六层楼  
本文给大家分享一篇教程关于java线程顺序执行问题,如何保证线程的顺序执行呢?今天通过实例代码给大家详细讲解下,感兴趣的朋友跟随小编一起看看吧

只要了解过多线程,我们就知道线程开始的顺序跟执行的顺序是不一样的。如果只是创建三个线程然后执行,最后的执行顺序是不可预期的。这是因为在创建完线程之后,线程执行的开始时间取决于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

4. 使用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这样的顺序。

参考文章

java 多线程 实现多个线程的顺序执行 - Hoonick - 博客园 (cnblogs.com)
Java lock锁的一些细节_笔记小屋-CSDN博客
VolatileCallSite (Java Platform SE 8 ) (oracle.com)
java保证多线程的执行顺序 - james.yj - 博客园 (cnblogs.com)

以上就是Java中保证线程顺序执行的详细内容,更多关于java线程执行顺序的资料请关注脚本之家其它相关文章!

相关文章

  • 解决IDEA使用maven创建Web项目,出现500错误的问题

    解决IDEA使用maven创建Web项目,出现500错误的问题

    本文主要介绍了在使用Maven创建项目并导入依赖写完测试代码后运行出现500错误的解决步骤,这种问题的根本原因是Tomcat启动后缺少某些支持的jar包,导致运行出错,解决方法是在项目结构中找到Artifacts,点击要编辑的项目
    2024-10-10
  • JAVA集成本地部署的DeepSeek的图文教程

    JAVA集成本地部署的DeepSeek的图文教程

    本文主要介绍了JAVA集成本地部署的DeepSeek的图文教程,包含配置环境变量及下载DeepSeek-R1模型并启动,具有一定的参考价值,感兴趣的可以了解一下
    2025-03-03
  • Java Optional用法面试题精讲

    Java Optional用法面试题精讲

    这篇文章主要为大家介绍了Java Optional用法面试题精讲,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-09-09
  • spring boot集成pagehelper(两种方式)

    spring boot集成pagehelper(两种方式)

    这篇文章主要介绍了spring boot集成pagehelper(两种方式),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-01-01
  • 使用Springboot打成jar包thymeleaf的问题

    使用Springboot打成jar包thymeleaf的问题

    这篇文章主要介绍了使用Springboot打成jar包thymeleaf的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • Java中的ReentrantReadWriteLock实现原理详解

    Java中的ReentrantReadWriteLock实现原理详解

    这篇文章主要介绍了Java中的ReentrantReadWriteLock实现原理详解,读写锁实现了接口ReadWriteLock,适合于读多写少的情况,支持公平锁和非公平锁,支持可冲入(进入读锁后可再进入读锁,进入写锁后可再进入写锁和读锁),需要的朋友可以参考下
    2024-01-01
  • java书店系统毕业设计 用户模块(2)

    java书店系统毕业设计 用户模块(2)

    这篇文章主要介绍了java书店系统毕业设计,第二步系统总体设计,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-10-10
  • 图片验证码概述及实现步骤

    图片验证码概述及实现步骤

    本文主要介绍了图片验证码概述及实现步骤。具有一定的参考价值,下面跟着小编一起来看下吧
    2017-01-01
  • java实现图片上传至本地实例详解

    java实现图片上传至本地实例详解

    我们给大家分享了关于java实现图片上传至本地的实例以及相关代码,有需要的朋友参考下。
    2018-08-08
  • JAVA中的FileWriter流解析

    JAVA中的FileWriter流解析

    这篇文章主要介绍了JAVA中的FileWriter流解析,FileWriter类提供了多种写入字符的方法,包括写入单个字符、写入字符数组和写入字符串等,它还提供了一些其他的方法,如刷新缓冲区、关闭文件等,需要的朋友可以参考下
    2023-10-10

最新评论