Java八股文面试之多线程篇

简介: Java八股文面试之多线程篇

线程有哪几种状态。

(1)NEW

线程至今尚未启动

(2)RUNNABLE

线程正在 Java 虚拟机中执行

(3)BLOCKED

受阻塞并等待获得同步代码块的锁

(4)WAITING

无限期地等待另一个线程来执行某一特定操作

(5)TIMED_WAITING

在指定的时间内等待另一个线程来执行某一特定操作

(6)TERMINATED

线程已退出

注意:RUNNABLE 是否真的运行取决于调度器,由操作系统内核来决定,可以细分为Ready就绪和Running运行

创建多线程有几种方法?

通过继承Thread类或者实现Runnable接口、Callable接口(JDK>=1.5)都可以实现多线程,不过实现Runnable接口与实现Callable

接口的方式基本相同,只是Callable接口里定义的方法返回值,可以声明抛出异常而已。因此,将实现Runnable接口

和实现Callable接口可以归为一种方式。

(1)实现Runnable、Callable接口的方式创建线程的优缺点

①优点:线程类只是实现了Runnable接口或者Callable接口,还可以继承其他类。这种方式下,多个线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想。

②缺点:编程稍微复杂一些,如果需要访问当前线程,则必须使用Thread.currentThread()方法

(2)继承Thread类的方式创建线程的优缺点

①优点:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获取当前线程

②缺点:因为线程类已经继承了Thread类,java语言是单继承的,所以就不能再继承其他父类了。

如何停止一个正在运行的线程

(1)当线程退出后,会自动正常退出,也就是当run方法完成后线程终止

(2)使用stop方法强行终止,但是不推荐;因为stop方法是个废弃方法

(3)使用interrupt方法中断线程,但并没有真正的结束线程,所以一般用于中断正在休眠的线程

sleep()和wait() 有什么区别?

(1)sleep()方法属于Thread类,可以在任何地方调用;wait()方法属于Object类,需要和 synchronized 一起用。

(2)sleep()方法不会释放锁,线程会一直占用CPU资源。wait()方法会释放锁,让出CPU资源,并且线程进入等待状态,直到被其他线程唤醒。

(3)sleep()方法会自动苏醒,不需要其他线程唤醒。wait()方法需要被notify()或notifyAll()方法唤醒。

(4)sleep()方法通常用于让线程暂停执行一段时间,例如模拟延迟操作。wait()方法通常用于线程间通信和协作,例如生产者-消费者模型。

Thread 类中的start() 和 run() 方法有什么区别?

start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的

效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()

方法才会启动新线程。

Thread类中的yield方法有什么作用?

yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法

而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可

能在进入到暂停状态后马上又被执行。

乐观锁和悲观锁的理解以及实现

(1)**乐观锁:**每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断

一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。

(2)乐观锁的实现方式:

①使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可

以采取丢弃和再次尝试的策略。

②java中的Compare and Swap即CAS ,当多个线程尝试使用CAS同时更新同一个变量时,只有其

中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争

中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比

较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自

动将该位置值更新为新值B。否则处理器不做任何操作

**(3)悲观锁:**总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据

的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做

操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

(4)悲观锁的实现方式:synchronized和lock

①synchronized是Java关键字,而lock是一个类

②synchronized获取锁的时候,假设A线程获得锁,B等待,若A阻塞,B会自治等待;而lock在这种情况下

B会尝试去获取锁

③synchronized的锁的状态是无法判断的,而lock是可以判断的

④synchronized和lock都是对象锁,都支持重入锁

⑤lock可以实现synchronized不具备的特性,如响应中断,支持超时,公平锁和共享锁

(共享锁:在同一时刻,可以有多个线程用有锁;独占锁:synchronized,在任一时刻,只有一个线程可以拥有此锁)

有三个线程T1,T2,T3如何保证顺序执行

可以使用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的

顺序应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。先启动三个线程中哪一个都行,因

为在每个线程的run()方法中都用join方法限定了三个线程的执行顺序。

public class JoinTest2 {
// 1.现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行
public static void main(String[] args) {
  final Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
      System.out.println("t1");
  }
});
  final Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
      try {
    // 引用t1线程,等待t1线程执行完
        t1.join();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
        System.out.println("t2");
    }
  });
  Thread t3 = new Thread(new Runnable() { 
  @Override
  public void run() {
    try {
      // 引用t2线程,等待t2线程执行完
      t2.join();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("t3");
  }

线程池是什么?线程池ThreadPoolExecutor的参数分别表示什么?

https://editor.csdn.net/md/?articleId=128745907

常用的线程池有哪些?分别有什么特点?

(1)newSingleThreadExecutor:创建一个单线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行

(2)newFixedThreadPool:是一种固定线程数量的线程池

特点:核心线程和最大线程数量都是一个固定的值。如果任务比较多工作线程处理不过来,就会加入到阻塞队列

里面等待。

(3)newCachedThreadPool:是一种可以缓存的线程池,它可以用来处理大量短期的突发流量

特点:①最大线程数是Interger.MaxValue②线程存活时间是60秒③阻塞队列用的是SynchronousQueue

(4)newScheduledThreadPool:具有延迟执行功能的线程池,可以用来实现实时调度

(5)newWorkStealingPool:Java8里面新加入的一个线程池,它内部会创建一个ForkJoinPool,利用工作窃取的算法并行处理请求

线程池的拒绝策略

JDK 提供了 4 种内置的拒绝策略:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy 和

DiscardPolicy;

①AbortPolicy:这是线程池默认的拒绝策略,当任务不能再提交的时候抛出异常,让开发人员及时知道程序运行状态,这样能在系统不能承载更大的并发量时,及时通过异常信息发现;

②DiscardPolicy:当任务不能再提交的时候直接丢弃任务,不抛出异常,这样可能会使我们无法发现系统的异

常状态,对于一些无关紧要的业务可以采用此策略;

③DiscardOldestPolicy:当任务不能再提交的时候丢弃任务队列中靠最前的任务,然后重新提交被拒绝的任务,对于一些允许丢弃老任务的业务可以采用此策略;

④CallerRunsPolicy: 当任务不能再提交的时候交由任务的调用线程(提交任务的线程,如main线程)来执行当前任务;这种拒绝策略会让所有任务都能得到执行,适合大量计算类型的任务执行。

⑤除了上面的四种拒绝策略,还可以通过实现 RejectedExecutionHandler 接口,实现自定义的拒绝策略

线程池的工作流程

提交任务后:

(1)判断当前线程池里的核心线程是否达到最大。

①如果不是,创建一个新的工作线程来执行任务。

②否则,则进入下个流程。

(2)判断线程池任务队列是否已满。

①如果任务队列没有满,则将任务添加到任务队列中等待线程执行。

②如果任务队列已满,则进入下个流程。

(3)判断线程池里的线程是否达到最大线程数。

①如果没有,则创建非核心线程执行任务。

②如果已满,则执行饱和策略。

ThreadLocal

说说ThreadLocal原理?

(1)ThreadLocal相当于一个线程本地变量,他会在每个线程中都创建一个副本;在线程之间访问内部

副本变量,就可以做到线程之间互相隔离,相比于synchronized它是在用空间来换时间。

(2)ThreadLocal中有一个静态内部类ThreadLocalMap,ThreadLocalMap又存在一个Entry数组,Entry具备保存key、value键值对的能力。Entry是一个弱引用,他的key是指向ThreadLocal的弱引用,

(3)使用弱引用的目的是为了防止内存泄露,如果使用的是强引用的话,除非线程结束否则ThreadLocal对象始终无

法被回收,使用弱引用的话会则在下一次GC的时候被回收,但是这样还是会存在内存泄露的问题。假如key和

ThreadLocal对象被回收之后,entry中存在key为null,但是value有值的entry对象,就会造成永远没办法被

访问到,除非线程运行结束。但是只要我们合理使用,在每次使用完之后都调用remove方法删除

Entry对象,实际上是不会出现这个问题的。

Thread、ThreadLocal、ThreadLocalMap关系

https://editor.csdn.net/md/?articleId=137903828

ThreadLocal会造成内存泄漏吗?如何避免内存泄漏

(1)ThreadLocal会造成内存泄漏吗?

①ThreadLocalMap内部维护了一个Entry类来存储键值对的映射关系,Entry将ThreadLocal作为Key,值作为value保存,它继承自弱引用WeakReference。由于ThreadLocal对象是弱引用,如果外部没有强引用指向它,它就会被GC回收,导致Entry的Key为null,如果这时value外部也没有强引用指向它,那么value就永远也访问不到了,按理也应该被GC回收,但是由于Entry对象还在强引用value,导致value无法被回收,这时「内存泄漏」就发生了,value成了一个永远也无法被访问,但是又无法被回收的对象。

(2)如何避免内存泄漏:

①将ThreadLocal变量存储的对象设置是不可变,因为不可变的对象不需要频繁更新

②尽量避免存储大对象,如果非要存,那么尽量在访问完成后及时调用remove()删除掉。

ThreadLocal的使用场景

(1)当我们在写API接口的时候,通常Controller层会接受来自前端的入参,当这个接口功能比较复杂的时候,可能我们调用的Service层内部还调用了 很多其他的很多方法,通常情况下,我们会在每个调用的方法上加上需要传递的参数。

但是如果我们将参数存入ThreadLocal中,那么就不用显式的传递参数了,而是只需要ThreadLocal中获取即可。

(2)当用户登录后,会将用户信息存入Token中返回前端,当用户调用需要授权的接口时,需要在header中携带 Token,然后拦截器中解析Token,获取用户信息,调用自定义的类(AuthNHolder)存入ThreadLocal中,当请求结束的时候,将ThreadLocal存储数据清空, 中间的过程无需在关注如何获取用户信息,只需要使用工具类的get方法即可。

(3)我们项目有个DateUtils工具类,这个工具类主要是对时间进行格式化,格式化/转化的实现是用的SimpleDateFormat。SimpleDateFormat不是线程安全的,所以我们就用ThreadLocal来让每个线程装载着自己的SimpleDateFormat对象

多线程有什么用?

(1)发挥多核CPU的优势

随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至

16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费

了75%。单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过

线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线

程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用

CPU的目的。

(2)防止阻塞

从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运

行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,

就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取

某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止

运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,

也不会影响其它任务的执行。

(3)便于建模

这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,

建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务

D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

死锁是什么?什么条件下会产出死锁,如何避免死锁?

(1)死锁,是指两个或者两个以上的线程在执行过程中,去争夺同一份共享资源导致相互等待无法继续执行。

(2)产生死锁的原因:

①第一个:互斥条件,共享资源a和b只能被一个线程占用;

②第二个:请求和保持条件,线程T1已经获取共享资源a,在等待共享资源b的时候,不释放共享资源a;

③第三个:不可抢占条件,其他线程不能强行抢占线程T1占有的资源;

④第四个:循环等待条件,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,这形成了循环等待。

(4)线程产生死锁之后,只能通过外部干预来解决问题,比如重启程序,或者Kill线程。所以,我们只能在写代码时规避死锁的产生。那么如何避免死锁产生呢?根据产生死锁的四个必要条件,我们只需要破坏其中任何一 个条件就可以解决。

①第一个互斥条件是没有办法被破坏的,因为它是互斥锁的基本约束。其他三个条件都可以通过人工干预来破坏。

②第二个请求保持条件,我们可以在首次执行一次性申请所有的资源,这样就不存在等待锁的问题了。

③第③个,对于不可抢占条件来说,占用部分资源的线程在进一步申请其他资源的时候如果申请不到,我们可以主动释放它占有的资源。这样不可抢占这个条件就被破坏了。

④第④个,对于循环等待条件来说,可以通过按序申请资源来预防死锁的产生。所谓按序申请,就是给资源编号,所有线程可以按照线性化的序号顺序去申请共享资源,先申请序号小的,再申请序号大的,这样循环等待自然就不存在了。

锁的升级过程(无锁->偏向锁->轻量级锁->重量级锁)

(1)偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入偏向锁。只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有

(2)轻量级锁:

轻量级锁是由偏向所升级来的,如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是synchronized

(3)重量级锁

如果在尝试加轻量级锁的过程中,CAS操作无法成功,说明有其他线程为此对象已经加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

synchronized和lock锁的区别

(1)synchronized是关键字,Lock类是一个接口

(2)Lock只有代码块锁,synchronized有代码块锁和方法锁

(3)synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁

(4)synchronized会自动释放锁(a:线程执行完同步代码会释放锁 ;b:线程执行过程中发生异常会释放锁);Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁

(5)用synchronized关键字修饰的两个线程1和线程2,如果线程1阻塞,线程2则会一直等待下去。Lock锁可以设置超时时间,不用一直等待下去。

(6)synchronized的锁是可重入、不可中断、非公平。Lock锁可重入、可中断、可公平(也可非公平)

(7)synchronized锁适合代码少量的同步问题,Lock锁适合大量代码的同步问题

CountDownLatch(倒计时锁)

用来进行线程同步协作,等待所有线程完成倒计时才恢复运行

public static void main(String[] args) throws InterruptedException {
  CountDownLatch latch = new CountDownLatch(3);
  ExecutorService service = Executors.newFixedThreadPool(4);
  service.submit(() -> {
    log.debug("begin...");
    sleep(1);
    latch.countDown();
    log.debug("end...{}", latch.getCount());
  });
  service.submit(() -> {
    log.debug("begin...");
    sleep(1.5);
    latch.countDown();
    log.debug("end...{}", latch.getCount());
  });
  service.submit(() -> {
    log.debug("begin...");
    sleep(2);
    latch.countDown();
    log.debug("end...{}", latch.getCount());
  });
  service.submit(()->{
    try {
      log.debug("waiting...");
      latch.await();
      log.debug("wait end...");
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  });
}

并发编程为什么有可见性问题

(1)一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性.

(2) 在单核时代,线程都是在一个CPU 上执行,操作的是同一个 CPU 的缓存,一个线程对缓存的写,对另外一个线程来说一定是可见的.

(2) 在多核时代,线程会在不同的 CPU 上执行时,操作的是不同的 CPU 缓存;当出现 线程 A 操作的是 CPU-1上的缓存,而线程 B 操作的是 CPU-2上的缓存,这个时候线程 A 对共享变量 V 的操作对于线程 B 而言就不具备可见性了

volatile关键字如何解决的可见性问题

(1)Java代码的运行是先将Java代码编译成字节码,然后JVM加载字节码最终转换成汇编指令在CPU上运行,被volatile修饰的字段会在其对应的汇编操作指令上加个lock前缀指令,这个指令就可以解决上述的两种可见性问题。

(2)处理器在接收到lock指令的时候会做两件事:

①将缓存行缓存的数据写回到系统内存中。

②这个写回到系统内存中的数据,如果在其他CPU的缓存行中存在相同的数据,则将其置为失效状态。

目录
相关文章
|
26天前
|
Java 程序员
Java社招面试中的高频考点:Callable、Future与FutureTask详解
大家好,我是小米。本文主要讲解Java多线程编程中的三个重要概念:Callable、Future和FutureTask。它们在实际开发中帮助我们更灵活、高效地处理多线程任务,尤其适合社招面试场景。通过 Callable 可以定义有返回值且可能抛出异常的任务;Future 用于获取任务结果并提供取消和检查状态的功能;FutureTask 则结合了两者的优势,既可执行任务又可获取结果。掌握这些知识不仅能提升你的编程能力,还能让你在面试中脱颖而出。文中结合实例详细介绍了这三个概念的使用方法及其区别与联系。希望对大家有所帮助!
163 60
|
21天前
|
安全 Java 程序员
面试必看:如何设计一个可以优雅停止的线程?
嘿,大家好!我是小米。今天分享一篇关于“如何停止一个正在运行的线程”的面试干货。通过一次Java面试经历,我明白了停止线程不仅仅是技术问题,更是设计问题。Thread.stop()已被弃用,推荐使用Thread.interrupt()、标志位或ExecutorService来优雅地停止线程,避免资源泄漏和数据不一致。希望这篇文章能帮助你更好地理解Java多线程机制,面试顺利! 我是小米,喜欢分享技术的29岁程序员。欢迎关注我的微信公众号“软件求生”,获取更多技术干货!
86 53
|
6天前
|
数据采集 Java Linux
面试大神教你:如何巧妙回答线程优先级这个经典考题?
大家好,我是小米。本文通过故事讲解Java面试中常见的线程优先级问题。小明和小华的故事帮助理解线程优先级:高优先级线程更可能被调度执行,但并非越高越好。实际开发需权衡业务需求,合理设置优先级。掌握线程优先级不仅能写出高效代码,还能在面试中脱颖而出。最后,小张因深入分析成功拿下Offer。希望这篇文章能助你在面试中游刃有余!
29 4
面试大神教你:如何巧妙回答线程优先级这个经典考题?
|
2天前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
35 14
|
5天前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
34 13
|
6天前
|
安全 Java 开发者
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
9天前
|
缓存 安全 Java
面试中的难题:线程异步执行后如何共享数据?
本文通过一个面试故事,详细讲解了Java中线程内部开启异步操作后如何安全地共享数据。介绍了异步操作的基本概念及常见实现方式(如CompletableFuture、ExecutorService),并重点探讨了volatile关键字、CountDownLatch和CompletableFuture等工具在线程间数据共享中的应用,帮助读者理解线程安全和内存可见性问题。通过这些方法,可以有效解决多线程环境下的数据共享挑战,提升编程效率和代码健壮性。
37 6
|
25天前
|
算法 安全 Java
Java线程调度揭秘:从算法到策略,让你面试稳赢!
在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
65 16
|
22天前
|
Java 程序员 调度
Java 高级面试技巧:yield() 与 sleep() 方法的使用场景和区别
本文详细解析了 Java 中 `Thread` 类的 `yield()` 和 `sleep()` 方法,解释了它们的作用、区别及为什么是静态方法。`yield()` 让当前线程释放 CPU 时间片,给其他同等优先级线程运行机会,但不保证暂停;`sleep()` 则让线程进入休眠状态,指定时间后继续执行。两者都是静态方法,因为它们影响线程调度机制而非单一线程行为。这些知识点在面试中常被提及,掌握它们有助于更好地应对多线程编程问题。
55 9
|
1月前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
109 17

热门文章

最新文章