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

作者:六层楼 时间:2023-05-14 17:36:46 

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

来源:https://www.cnblogs.com/rever/p/14768553.html

标签:java,线程,执行,顺序
0
投稿

猜你喜欢

  • spring boot starter actuator(健康监控)配置和使用教程

    2021-07-29 06:24:18
  • Java实现将PDF转为PDF/A

    2023-03-16 00:54:22
  • 简单的观察者模式示例分享

    2023-02-11 12:52:12
  • Android平台预置GMS包后关机闹钟失效问题及解决方法

    2022-12-31 05:52:33
  • Java 8 中 Map 骚操作之 merge() 的使用方法

    2022-03-31 01:19:27
  • spring boot 静态资源处理方法

    2022-07-14 22:12:42
  • JAVA利用递归删除文件代码实例

    2022-12-10 23:59:53
  • C#多线程之Thread中Thread.Join()函数用法分析

    2022-01-20 14:47:58
  • Android XListView下拉刷新和上拉加载更多

    2022-11-01 19:07:45
  • Android Bluetooth蓝牙技术使用流程详解

    2022-07-07 02:41:16
  • SpringMVC 数据校验方法(必看篇)

    2023-11-14 21:44:05
  • Java中计算时间差的方法

    2023-11-15 10:35:44
  • Spring Boot Actuator监控端点小结

    2023-02-15 05:04:23
  • 如何在C#中使用指针

    2022-07-02 16:09:47
  • 使用maven开发springboot项目时pom.xml常用配置(推荐)

    2022-09-19 23:33:48
  • Android中系统自带锁WalkLock与KeyguardLock用法实例详解

    2023-11-26 01:50:49
  • Java实现简单LRU缓存机制的方法

    2023-08-11 03:54:49
  • 解决javaWEB中前后台中文乱码问题的3种方法

    2023-03-22 22:39:26
  • Java实现马踏棋盘算法

    2023-03-05 04:30:46
  • C#串口编程System.IO.Ports.SerialPort类

    2023-06-07 17:48:28
  • asp之家 软件编程 m.aspxhome.com