• Java中Thread类的常用API以及使用示例


    场景

    Java语言是支持多线程的,一个正在运行的Java程序可以称之为一个进程(process),在每个进程里面包含多个线程,线程是进程中单一的顺序控制流,CPU在执行计算机指令的时候都是按顺序执行,但是由于其执行速度很快,可以把时间分成很细小的时间片,交替执行,线程和进程的区别在于:

    创建进程的开销大于创建线程的开销,进程之间的通信比线程间要难
    线程不能独立存在,依托于进程而存在,线程也可以看作轻量级的进程
    多进程的稳定性高于多线程,一个进程的运行不会影响其他进程,但线程崩溃往往会引起程序的崩溃
    Thread类位于java.lang包,JDK1.0引入。在HotSpot虚拟机中,线程使用的是基于操作系统的1 : 1的内核实现模型来创建线程,线程的创建、调度、执行、销毁等由内核进行控制,调度过程通过抢占式策略进行调度。

    下面记录Thread类的常用API。

    注:

    博客:
    霸道流氓气质的博客_CSDN博客-C#,架构之路,SpringBoot领域博主
    关注公众号
    霸道的程序猿
    获取编程相关电子书、教程推送与免费下载。

    实现

    1、sleep线程休眠

    sleep方法会使当前线程进入指定毫秒数的休眠,暂停执行,虽然给定一个休眠时间,但是最终要以系统的定时器和调度器的精度为准。

    1. public class ApiDemo {
    2.     public static void main(String[] args) throws InterruptedException {
    3.         //1、线程sleep
    4.         //sleep方法会使当前线程进入指定毫秒数的休眠,暂停执行,虽然给定一个休眠时间,但是最终要以系统的定时器和调度器的精度为准
    5.           new Thread(()->{
    6.                long startTime = System.currentTimeMillis();
    7.                sleep(2000L);
    8.                long endTime = System.currentTimeMillis();
    9.                System.out.println(String.format("总共花费时间:%d ms",(endTime-startTime)));
    10.            }).start();
    11.            long startTime = System.currentTimeMillis();
    12.            sleep(3000l);
    13.            long endTime = System.currentTimeMillis();
    14.            System.out.println(String.format("主线程总共花费时间:%d ms",(endTime-startTime)));
    15.         //输出结果
    16.        //总共花费时间:2012 ms
    17.        // 主线程总共花费时间:3006 ms
    18.        //分别在自定义的线程和主线程中进行了休眠,每个线程的休眠互不影响。
    19.     }

    2、使用TimeUnit代替Thread.sleep

    在JDK1.5以后,引入了枚举TimeUnit,其对sleep进行了很好的封装,使用它可以省去时间单位的换算步骤

    1.         //休眠3000毫秒
    2.         //TimeUnit.MILLISECONDS.sleep(3000);
    3.         //休眠4
    4.         //TimeUnit.SECONDS.sleep(4);
    5.         //休眠1分钟
    6.         //TimeUnit.MINUTES.sleep(1);
    7.         //休眠1小时
    8.         //TimeUnit.HOURS.sleep(1);

    3、yield方法

    会提醒调度器自愿放弃当前的CPU资源,如果CPU资源不紧张,则会忽略这种提醒。

    1. package com.ruoyi.demo.thread;
    2. import java.util.concurrent.TimeUnit;
    3. import java.util.stream.IntStream;
    4. import static jodd.util.ThreadUtil.sleep;
    5. public class ApiDemo {
    6.     public static void main(String[] args) throws InterruptedException {
    7.         IntStream.range(0,2).mapToObj(ApiDemo::create).forEach(Thread::start);
    8.     }
    9.     private static Thread create(int index){
    10.         return new Thread(()->{
    11.             //如果不加index0调用yield方法,输出结果有时候0在前,有时候1在前
    12.             //当调用了yield方法,顺序始终是0,1,因为index0的线程如果最先获取了CPU资源会高速CPU调度器放弃了原本属于自己的资源
    13.             //但是yield只是一个提示,不能保证每次都能满足
    14.             if(index == 0){
    15.                 Thread.yield();
    16.             }
    17.             System.out.println(index);
    18.         });
    19.     }
    20. }

    4、setPriority设置线程优先级

    理论上优先级较高的线程会获取优先被CPU调度的机会,但是也只是一个提示作用,不能保证

    每次都是这样。

    1.         Thread t1 = new Thread(()->{
    2.            while(true)
    3.            {
    4.                System.out.println("t1");
    5.            }
    6.         });
    7.         t1.setPriority(3);
    8.         Thread t2 = new Thread(()->{
    9.             while(true)
    10.             {
    11.                 System.out.println("t2");
    12.             }
    13.         });
    14.         t2.setPriority(1);
    15.         t1.start();
    16.         t2.start();

    通过设置t1的优先级高,所以t1的输出频率要高于t2。

    除了设置优先级,还有getPriority()获取线程的优先级。

    5、获取当前线程currentThread以及获取线程ID

    获取当前线程currentThread,getId()获取线程的唯一ID。

    1.         //4、获取线程ID
    2.         new Thread(() -> {
    3.             System.out.println(Thread.currentThread().getId());
    4.         }).start();
    5.         //5、获取当前线程以及名字
    6.         new Thread(() -> {
    7.             System.out.println(Thread.currentThread().getName());
    8.         }).start();

    6、interrupt打断堵塞

    调用wait、sleep、join等方法时会使线程进入堵塞状态,而调用interrupt方法就可以打断堵塞。

    一旦在堵塞的情况下被打断,都会抛出一个InterruptedException的异常。

    1.         Thread thread = new Thread(() -> {
    2.             try {
    3.                 TimeUnit.MINUTES.sleep(2);
    4.             } catch (InterruptedException e) {
    5.                 System.out.println("被interrupted");
    6.             }
    7.         });
    8.         thread.start();
    9.         TimeUnit.SECONDS.sleep(2);
    10.         thread.interrupt();

    新建一个线程并企图休眠2分钟,但是主线程在2秒后调用interrupt将其打断。

    7、join方法

    join某个线程A,会使当前线程进入等待,直到线程A结束生命周期,或者到达给定的时间

    下面创建两个线程,分别启动,并且调用了每个线程的join方法,join方法是被主线程调用的,会发现线程1和线程2交替地输出直到他们结束生命周期,main线程的循环才会开始运行。

    1. public class ApiDemo {
    2.     public static void main(String[] args) throws InterruptedException {
    3.         List<Thread> threads = IntStream.range(1, 3).mapToObj(ApiDemo::create).collect(Collectors.toList());
    4.         //启动线程
    5.         threads.forEach(Thread::start);
    6.         //执行这两个线程的join方法
    7.         for(Thread thread : threads){
    8.             thread.join();
    9.         }
    10.         //main线程循环输出
    11.         for (int i = 0; i < 10; i++) {
    12.             System.out.println(Thread.currentThread().getName()+"#"+i);
    13.             shortSleep();
    14.         }
    15.     }
    16.     //创建线程,每个线程只做循环输出
    17.     private static Thread create(int index){
    18.         return new Thread(()->{
    19.             for (int i = 0; i < 10; i++) {
    20.                 System.out.println(Thread.currentThread().getName() +"#"+i);
    21.                 shortSleep();
    22.             }
    23.         });
    24.     }
    25.     //休眠1
    26.     private static void shortSleep(){
    27.         try {
    28.             TimeUnit.SECONDS.sleep(1);
    29.         } catch (InterruptedException e) {
    30.             e.printStackTrace();
    31.         }
    32.     }
    33. }

    输出结果

    Thread-1#0
    Thread-0#0
    Thread-0#1
    Thread-1#1
    Thread-1#2
    Thread-0#2
    Thread-1#3
    Thread-0#3
    Thread-0#4
    Thread-1#4
    Thread-0#5
    Thread-1#5
    Thread-0#6
    Thread-1#6
    Thread-1#7
    Thread-0#7
    Thread-1#8
    Thread-0#8
    Thread-1#9
    Thread-0#9
    main#0
    main#1
    main#2
    main#3
    main#4
    main#5
    main#6
    main#7
    main#8
    main#9

    Process finished with exit code 0

    应用场景:

    app调用后台服务查询航班信息,后台需要到各大航空公司的接口获取信息,最后统一整理加工返回

    到app端。除了使用CountDownLatch等,也可以用join方法。将每一个航空公司的查询交给一个线程去工作,

    然后在他们结束之后统一对数据进行整理。

    8、join方法结合实战(查询多个航空公司api返回航班列表汇总)

    每个航空公司的接口不一样,查询速度也存在差异,如果跟航空公司进行串行化交互(逐个查询),

    客户端需要等待很长的时间,如果将每一个航空公司的查询都交给一个线程去工作,然后在他们结束工作之后

    统一对数据进行整理,这样就可以节省时间。

    定义查询接口FightQuery,并提供一个返回方法,不管是Thread的run方法还是Runable接口,都是void返回类型,如果

    想通过某个线程的运行得到结果,就需要自己定义一个返回的接口。

    1. package com.ruoyi.demo.thread;
    2. import java.util.List;
    3. public interface FightQuery {
    4.     //FightQuery提供了一个返回方法,不管是Thread的run方法,还是Runable方法,都是void返回类型,如果想通过某个线程的运行得到结果,就需要自己定义一个返回的结果
    5.     List get();
    6. }

    查询Fight的task就是一个线程的子类,主要用于到各大航空公司获取数据

    1. package com.ruoyi.demo.thread;
    2. import java.util.ArrayList;
    3. import java.util.List;
    4. import java.util.concurrent.ThreadLocalRandom;
    5. import java.util.concurrent.TimeUnit;
    6. public class FightQueryTask extends Thread implements FightQuery{
    7.     //起点
    8.     private final String origin;
    9.     //终点
    10.     private final String destination;
    11.     //航班列表
    12.     private final List<String> flightList = new ArrayList<>();
    13.     public FightQueryTask(String airline,String origin,String destination){
    14.         //调用父类Thread的构造方法,传递航班名参数作为线程name
    15.         super("["+airline+"]");
    16.         this.origin  = origin;
    17.         this.destination = destination;
    18.     }
    19.     @Override
    20.     public void run() {
    21.         //getName()是调用父类Thread的getName()方法
    22.         System.out.printf("%s-query from %s to %s \n",getName(),origin,destination);
    23.         //ThreadLocalRandom 线程安全随机数获取
    24.         int randomVal = ThreadLocalRandom.current().nextInt(10);
    25.         try {
    26.             TimeUnit.SECONDS.sleep(randomVal);
    27.             this.flightList.add(getName()+"-"+randomVal);
    28.             System.out.printf("The Fight:%s list query successful \n",getName());
    29.         } catch (InterruptedException e) {
    30.             e.printStackTrace();
    31.         }
    32.     }
    33.     @Override
    34.     public List<String> get() {
    35.         return this.flightList;
    36.     }
    37. }

    注意上面的构造方法中调用了super,这代表着调用了父类Thread的构造方法,传递航班名参数作为线程name

    以及getName也是调用了父类Thread的方法。

    然后实现app模拟发起航班查询

    1. package com.ruoyi.demo.thread;
    2. import java.util.ArrayList;
    3. import java.util.Arrays;
    4. import java.util.List;
    5. import java.util.stream.Collectors;
    6. public class FightQueryDemo {
    7.     //定义合作的各大航空公司
    8.     private static List<String> fightCompany = Arrays.asList("Company1","Company2","Company3");
    9.     public static void main(String[] args) {
    10.         //模拟发出搜索机票请求,传递参数起点和终点
    11.         List<String> results = search("QD","BJ");
    12.         //遍历输出查询结果
    13.         results.forEach(System.out::println);
    14.     }
    15.     private static List<String> search(String original,String dest)
    16.     {
    17.         final List<String> result = new ArrayList<>();
    18.         //创建查询航班信息的线程列表
    19.         //这里有三家航工公司,遍历这三家航空公司,调用创建线程的方法传递航空公司的名字和起点以及终点
    20.         List<FightQueryTask> tasks = fightCompany.stream().map(f->createSearchTask(f,original,dest)).collect(Collectors.toList());
    21.         //遍历启动这几个线程
    22.         tasks.forEach(Thread::start);
    23.         //分别调用每一个线程的join方法,获取每个查询线程的结果,并且将其加入到result中
    24.         tasks.forEach(t->{
    25.             try {
    26.                 t.join();
    27.             } catch (InterruptedException e) {
    28.                 e.printStackTrace();
    29.             }
    30.         });
    31.         //在此之前,当前线程会堵塞住,获取每个查询线程的结果,并且加入到result中
    32.         //调用接口的get方法获取每个公司的航班列表,并将每个公司的航班列表addAll到result中
    33.         tasks.stream().map(FightQuery::get).forEach(result::addAll);
    34.         return result;
    35.     }
    36.     private static FightQueryTask createSearchTask(String fight,String original,String dest){
    37.         //通过传参的构造方法传递参数
    38.         return  new FightQueryTask(fight,original,dest);
    39.     }
    40. }

     

    总结:

    主线程收到了seach请求之后,交给了若干个查询线程分别进行工作,最后将每一个线程获取的航班进行统一的汇总。

    由于每个航空公司的查询时间不一样,所以使用随机值来模拟不同的查询速度。

    运行结果

     

    以上代码和示例参考《Java高并发编程详解》,建议阅读原书。

  • 相关阅读:
    微服务项目:尚融宝(40)(核心业务流程:申请借款额度(3))
    在非IOS系统打开HEIC格式图片
    保洁实业如何使用虚拟机器人提高安全性
    【Spring Boot 3】【Camel 4】静态路由
    前后端分离项目使用宝塔部署
    Go语言 Map教程
    基于动态数据体积的网络入侵问题
    MacOS lldb 使用记录
    PX4模块设计之十八:Logger模块
    【华为机试真题 JAVA】数字反转打印-100
  • 原文地址:https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/126596884