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

  1. public class FIFOThreadExample {
  2. public synchronized static void foo(String name) {
  3. System.out.print(name);
  4. }
  5. public static void main(String[] args) {
  6. Thread thread1 = new Thread(() -> foo("A"));
  7. Thread thread2 = new Thread(() -> foo("B"));
  8. Thread thread3 = new Thread(() -> foo("C"));
  9. thread1.start();
  10. thread2.start();
  11. thread3.start();
  12. }
  13. }

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

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

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

1. 使用Thread.join()实现

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

  1. public class FIFOThreadExample {
  2. public static void foo(String name) {
  3. System.out.print(name);
  4. }
  5. public static void main(String[] args) throws InterruptedException{
  6. Thread thread1 = new Thread(() -> foo("A"));
  7. Thread thread2 = new Thread(() -> foo("B"));
  8. Thread thread3 = new Thread(() -> foo("C"));
  9. thread1.start();
  10. thread1.join();
  11. thread2.start();
  12. thread2.join();
  13. thread3.start();
  14. }
  15. }

输出结果:ABC

2. 使用单线程线程池来实现

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

  1. import java.util.concurrent.ExecutorService;
  2. import java.util.concurrent.Executors;
  3. public class FIFOThreadExample {
  4. public static void foo(String name) {
  5. System.out.print(name);
  6. }
  7. public static void main(String[] args) throws InterruptedException{
  8. Thread thread1 = new Thread(() -> foo("A"));
  9. Thread thread2 = new Thread(() -> foo("B"));
  10. Thread thread3 = new Thread(() -> foo("C"));
  11. ExecutorService executor = Executors.newSingleThreadExecutor();
  12. executor.submit(thread1);
  13. executor.submit(thread2);
  14. executor.submit(thread3);
  15. executor.shutdown();
  16. }
  17. }

输出结果:ABC

3. 使用volatile关键字修饰的信号量实现

上面两种的思路都是让保证线程的执行顺序,让线程按一定的顺序执行。这里介绍第三种思路,那就是线程可以无序运行,但是执行结果按顺序执行。

你应该可以想到,三个线程都被创建并start(),这时候三个线程随时都可能执行run()方法。因此为了保证run()执行的顺序性,我们肯定需要一个信号量来让线程知道在任意时刻能不能执行逻辑代码。

另外,因为三个线程是独立的,这个信号量的变化肯定需要对其他线程透明,因此volatile关键字也是必须要的。

  1. public class TicketExample2 {
  2. //信号量
  3. static volatile int ticket = 1;
  4. //线程休眠时间
  5. public final static int SLEEP_TIME = 1;
  6. public static void foo(int name){
  7. //因为线程的执行顺序是不可预期的,因此需要每个线程自旋
  8. while (true) {
  9. if (ticket == name) {
  10. try {
  11. Thread.sleep(SLEEP_TIME);
  12. //每个线程循环打印3次
  13. for (int i = 0; i < 3; i++) {
  14. System.out.println(name + " " + i);
  15. }
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. //信号量变更
  20. ticket = name%3+1;
  21. return;
  22. }
  23. }
  24. }
  25. public static void main(String[] args) throws InterruptedException {
  26. Thread thread1 = new Thread(() -> foo(1));
  27. Thread thread2 = new Thread(() -> foo(2));
  28. Thread thread3 = new Thread(() -> foo(3));
  29. thread1.start();
  30. thread2.start();
  31. thread3.start();
  32. }
  33. }

执行结果:

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就可以实现顺序的执行。

  1. public class TicketExample3 {
  2. //信号量
  3. AtomicInteger ticket = new AtomicInteger(1);
  4. public Lock lock = new ReentrantLock();
  5. private Condition condition1 = lock.newCondition();
  6. private Condition condition2 = lock.newCondition();
  7. private Condition condition3 = lock.newCondition();
  8. private Condition[] conditions = {condition1, condition2, condition3};
  9. public void foo(int name) {
  10. try {
  11. lock.lock();
  12. //因为线程的执行顺序是不可预期的,因此需要每个线程自旋
  13. System.out.println("线程" + name + " 开始执行");
  14. if(ticket.get() != name) {
  15. try {
  16. System.out.println("当前标识位为" + ticket.get() + ",线程" + name + " 开始等待");
  17. //开始等待被唤醒
  18. conditions[name - 1].await();
  19. System.out.println("线程" + name + " 被唤醒");
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. System.out.println(name);
  25. ticket.getAndIncrement();
  26. if (ticket.get() > 3) {
  27. ticket.set(1);
  28. }
  29. //执行完毕,唤醒下一次。1唤醒2,2唤醒3
  30. conditions[name % 3].signal();
  31. } finally {
  32. //一定要释放锁
  33. lock.unlock();
  34. }
  35. }
  36. public static void main(String[] args) throws InterruptedException {
  37. TicketExample3 example = new TicketExample3();
  38. Thread t1 = new Thread(() -> {
  39. example.foo(1);
  40. });
  41. Thread t2 = new Thread(() -> {
  42. example.foo(2);
  43. });
  44. Thread t3 = new Thread(() -> {
  45. example.foo(3);
  46. });
  47. t1.start();
  48. t2.start();
  49. t3.start();
  50. }
  51. }

输出结果:

线程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中如何保证线程顺序执行的更多相关文章

  1. java中等待所有线程都执行结束(转)

    转自:http://blog.csdn.net/liweisnake/article/details/12966761 今天看到一篇文章,是关于java中如何等待所有线程都执行结束,文章总结得很好,原 ...

  2. java中等待所有线程都执行结束

    转自:http://blog.csdn.net/liweisnake/article/details/12966761 今天看到一篇文章,是关于java中如何等待所有线程都执行结束,文章总结得很好,原 ...

  3. JAVA中等待所有线程都执行结束(转2)

    场景: package com.java4all.mypoint; import java.util.concurrent.CountDownLatch; public class ThreadTes ...

  4. Android中让多个线程顺序执行探究

    线程调度是指按照特定机制为多个线程分配CPU的使用权. 有两种调度模型:分时调度模型和抢占式调度模型. 分时调度模型:是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片. ...

  5. java中子类继承父类程序执行顺序

    java中子类继承父类程序执行顺序 FatherTest.java public class FatherTest { private String name; public FatherTest() ...

  6. Java中的成员初始化顺序和内存分配过程

    Java中的成员初始化顺序和内存分配过程 原帖是这样描述的: http://java.dzone.com/articles/java-object-initialization?utm_source= ...

  7. Java中的守护线程 & 非守护线程(简介)

    Java中的守护线程 & 非守护线程 守护线程 (Daemon Thread) 非守护线程,又称用户线程(User Thread) 用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守 ...

  8. T1,T2,T3 三个线程顺序执行

    T1,T2,T3 三个线程顺序执行 现在有 T1.T2.T3 三个线程,你怎样保证 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行?(T1->T2->T3) 这个线程问题通常会 ...

  9. 详解线程池的作用及Java中如何使用线程池

    服务端应用程序(如数据库和 Web 服务器)需要处理来自客户端的高并发.耗时较短的请求任务,所以频繁的创建处理这些请求的所需要的线程就是一个非常消耗资源的操作.常规的方法是针对一个新的请求创建一个新线 ...

随机推荐

  1. Vue.js 学习笔记之七:使用现有组件

    5.3 使用现有组件 在之前的五个实验中,我们所演示的基本都是如何构建自定义组件的方法,但在具体开发实践中,并非项目中所有的组件都是需要程序员们自己动手来创建的.毕竟在程序设计领域,"不要重 ...

  2. 生成元(JAVA语言)

    package 第三章; import java.util.Scanner; public class 生成元 { public static void main(String[] args) { / ...

  3. 错误提示:Access denied for user 'GC'@'localhost' (using password: YES)

    错误描述:使用的是C3P0连接池 Spring整合Mybatis时出现错误 java.sql.SQLException: Access denied for user 'GC'@'localhost' ...

  4. 思维导图趋势大分析(MindMaster与百度脑图)

    思维导图现在可以说是大流行期间,涉及学习.工作.生活方方面面的内容. 一.什么是思维导图 思维导图的英文名称是The Mind Map,也叫做心智导图,脑图,心智地图,脑力激荡图等.思维导图应用图文兼 ...

  5. Jmeter(四十) - 从入门到精通进阶篇 - Jmeter配置文件的刨根问底 - 中篇(详解教程)

    1.简介 为什么宏哥要对Jmeter的配置文件进行一下讲解了,因为有的童鞋或者小伙伴在测试中遇到一些需要修改配置文件的问题不是很清楚也不是很懂,就算修改了也是模模糊糊的.更有甚者觉得那是禁地神圣不可轻 ...

  6. PAT (Advanced Level) Practice 1031 Hello World for U (20 分) 凌宸1642

    PAT (Advanced Level) Practice 1031 Hello World for U (20 分) 凌宸1642 题目描述: Given any string of N (≥5) ...

  7. Istio安全-证书管理(实操一)

    Istio安全-证书管理 目录 Istio安全-证书管理 插入现有CA证书 插入现有证书和密钥 部署Istio 配置示例services 校验证书 卸载 Istio的DNS证书管理 DNS证书的提供和 ...

  8. leetcode 刷题(数组篇)11题 盛最多水的容器(双指针)

    题目描述 给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) .在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) .找出其 ...

  9. PAT B1038 统计同成绩学生

    读入N名同学的成绩,然后将获得某一给定分数的学生人数输出第一行给出不超过10^5的正整数,即学生总数:第二行给出N名学生的百分制的成绩,中间以空格分隔:第三行给出要查寻的分数个数K,随后是K个分数,中 ...

  10. Object o = new Object()占多少个字节?-对象的内存布局

    一.先上答案 这个问题有坑,有两种回答 第一种解释: object实例对象,占16个字节. 第二种解释: Object o:普通对象指针(ordinary object pointer),占4个字节. ...