【面试题】—— Java多线程篇(17题)

文章目录

    • 1.什么是多线程?
    • 2.线程和进程的区别?
    • 3.我们为什么要使用线程?线程的优缺点?
    • 4.创建线程的方法有哪些?
    • 5.线程的状态有哪些?
    • 6.线程的优先级?
    • 7.线程常用的方法以及其作用?
    • 8.使用过线程池吗?为什么要使用线程池?
    • 9.Java线程分类?
    • 10.什么是死锁?
    • 11.死锁产生的原因、条件是什么?
    • 12.如何预防死锁?
    • 13.为什么不建议使用Executor静态工程来创建线程池?
    • 14.如何创建线程池?
    • 15.线程池常用的参数有哪些?
    • 16.常用的线程池有哪些?
    • 17.说一下ThreadPoolExecutor线程池的执行流程是什么样的?

1.什么是多线程?

线程是操作系统能够进行运算调度的最小单位。线程是一个程序内部的顺序控制流。

2.线程和进程的区别?

进程包含线程。一个进程中可以存在很多线程,每条线程执行不同的任务。比如你打开网易云一边听歌一遍下载歌曲,那么此时网易云整个应用算是一个进程,而播放音乐是该进程下的一个线程,下载歌曲是该进程下的另一个线程。
线程是比进程更小的执行单元。

3.我们为什么要使用线程?线程的优缺点?

  • 优点
    • 使程序执行效率更高。能够让任务实现并行执行。
    • 更充分的利用CPU资源。更好地利用系统资源。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,大大提高程序的效率。
    • 增加了程序应用性和灵活性。由于同一进程的所有线程是共享同一内存,所以不需要特殊的数据传送机制,不需要建立共享存储区或共享文件,从而使得不同任务之间的协调操作与运行、数据的交互、资源的分配等问题更加易于解决。
    • 提升用户体验。比如那些比较耗时的任务,可以起一个单独线程放到后台进行执行,而用户可以做其他的事情。
  • 缺点
    • 多线程消耗的内存空间比较多。
    • 线程的切换需要消耗资源。
    • 多线程下容易造成死锁和数据不安全的情况。

4.创建线程的方法有哪些?

  • 继承Thread重写run方法。
    • 特点:(1)无法获取子线程返回值。(2)run方法不可以抛出异常。
class MyThread extends Thread {
	@Override
    public void run() {
        // 线程执行的代码
    }
}

// 创建并启动线程
MyThread thread = new MyThread();
thread.start();

  • 实现Runnable接口。
    • 特点:(1)无法获取子线程返回值。(2)run方法不可以抛出异常。
class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的代码
    }
}

// 创建并启动线程
Thread thread = new Thread(new MyRunnable());
thread.start();
  • 实现Callable接口。
    • 特点:(1)可以获取子线程返回值。(2)run方法可以抛异常。

5.线程的状态有哪些?

  • 新生状态(NEW):执行new Thread()时创建线程,线程对象一旦被创建就会处于新生状态。
  • 运行状态(RUNNABLE):执行Start()后,线程处于就绪状态,但是不会立即执行,抢占CUP时间片。
  • 阻塞状态(BLOCKED):在等待monitor(监视器)锁的线程的状态。假如在同一个JVM中,有A、B两个线程并行执行,A首先进入了同步块或者是方法后,线程B此时就处于阻塞状态。
  • 等待状态(WAITING):
  • 限时等待(TIMED_WAITING):
  • 终止状态(TERMINATED/'tɜːrmə,net/):
    【面试题】—— Java多线程篇(17题)_第1张图片

6.线程的优先级?

线程的优先级一共被分为10个等级,线程默认的优先级是5。

  • 在有时间片轮询机制中,“高优先级”的线程的被CUP分配时间片的概率要大于“低优先级”的线程。但是并不是“高优先级”的线程一定会先执行。
  • 在无时间片轮询机制中,“高优先级”的线程会被先执行。如果有“低优先级”的线程正在运行,有“高优先级”的线程处于可运行状态的话,就执行完“低优先 级”的线程后,再执行”高优先级“的线程
class ThreadTest extends Thread{
    public void run(){
        ...
    }
}
...
 Thread t1=new ThreadTest("thread1");    // t1线程
 Thread t2=new ThreadTest("thread2");    // t2线程
 t1.setPriority(1);  // 设置t1的优先级为1
t2.setPriority(5);  // 设置t2的优先级为5

7.线程常用的方法以及其作用?

  • threadTest.run():线程的执行体,一般要重写,执行体中的内容就是当前线程执行的内容。
  • threadTest.start():启动线程的方法,调用此方法那么该线程就处于了就绪状态。
  • Object.wait():使当前线程处于等待(阻塞)状态,并且释放所持有的对象的锁。在使用线程的此方法时,需要处理Interrupted(中断异常)异常。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。
    • 调用该方法后当前线程进入睡眠状态,直到以下事件发生。
    • 其他线程调用了该对象的notify方法。
    • 其他线程调用了该对象的notifyAll方法。
    • 其他线程调用了interrupt中断该线程。
    • 时间间隔到了。
  • threadTest.suspend():将线程挂起,必须由其他线程调用resume()。
  • Thread.join():join()方法把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的join()方法,直到线程A执行完毕后才会执行线程B。join的主要作用就是:让主线程等待子线程结束才能继续运行。join的底层是调用wait()方法,wait()方法在调用后释放锁资源,所以join方法在调用的时候也是释放锁资源的。使用join()方法也是需要处理Interrupted(中断异常)异常。
  • Thread.yield():暂停当前正在运行的线程对象放弃当前CPU资源,并执行其他线程。让当前处于正在运行的线程回到可运行状态,并允许与当前线程有同样优先级的其它线程获取运行的机会,故yield()的另一个作用是,让具有相同优先级或者高于当前优先级的线程轮替执行。但是调用了yield()方法后,并不能一定让步,因为让步的线程还有可能被其它线程调度程序再次选中,所以可能达不到让步的效果。
  • Thread.sleep():sleep()是一个静态本地方法。让线程睡眠,此期间线程不消耗CPU资源。此方法和wait()方法很像,但是唯一点不同的是:wait()会释放锁资源,而sleep()不会。
  • object.nodify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
  • object.nodifyAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
  • threadTest.setPriority():设置线程的优先级。
  • Thread.currentThread():输出当前线程。

8.使用过线程池吗?为什么要使用线程池?

我们可以通过ThreadPoolExecutor来创建一个线程池。

如果需要处理大量任务,频繁地创建和销毁线程会浪费时间和效率,尤其是浪费内存。为了让线程重复利用,让它们继续执行其他任务而不是立即销毁,线程池应运而生。

虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。与其它多线程应用程序一样,用线程池构建的应用程序容易遭受同步错误和死锁等并发风险。此外,线程池还容易遭受特定的风险,如与池有关的死锁、资源不足和线程泄漏。

9.Java线程分类?

  • 用户线程(User Thread)
    创建的普通线程都是用户线程。
  • 守护线程(Daemon Thread)
    所谓守护线程就是在程序运行的时候在后台提供一种通用服务对线程,比如垃圾回收就是一个守护线程。当所有非守护线程结束后,程序也就终止了,同时会杀死程序中的所有守护线程。
  • 用户线程切换为守护线程
    将用户线程切换为守护线程使用的是Thread.setDaemon()方法。但是要注意切换的时机,必须在该线程启动之间进行设置。否则会报出IllegalThreadStateException()异常,因为不能把运行中的线程转变为守护线程。
  • 用户线程和守护线程的区别
    二者主要的区别就是JVM虚拟机离开的时间。
    用户线程:当存在任何一个用户线程未离开,JVM是不会离开的。
    守护线程:如果只剩下守护线程未离开,JVM是可以离开的。

10.什么是死锁?

两个或者多个进程、线程 无限期地等待永远不会发生的条件,系统处于停滞状态,这就是死锁。

11.死锁产生的原因、条件是什么?

  • 原因:
    • 系统资源不足
    • 进程运行推进的顺序不合适
    • 资源分配不当
  • 条件:
    • 互斥条件:一个资源一次只能被一个线程使用。
    • 占有且等待:一个进程获取了一份资源A,再请求另一个份资源B而阻塞时,对已获得的A资源保持不放。
    • 不可强行占有:进程已获得的资源,在未使用完之前,不能强行剥夺。
    • 循环等待条件:若干线程之间形成一种头尾衔接的循环等待资源关系。

12.如何预防死锁?

通过设置某些限制条件,去破坏死锁的四个条件中的一个或者多个条件,来预防死锁。但是由于所施加的限制条件往往太苛刻,因而导致系统资源利用率和系统吞吐量降低。

  • 打破互斥条件:在系统里取消互斥。若资源不被一个进程独占使用,那么死锁是肯定不会发生的。但一般来说在所列的四个条件中,“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件。
  • 打破占有且等待条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待。
    每个进程提出新的资源申请前,必须先释放它先前所占有的资源。
  • 打破不可剥夺条件:当进程占有某些资源后又进一步申请其他资源而无法满足,则该进程必须释放它原来占有的资源。
  • 打破环路等待条件:实现资源有序分配策略,将系统的所有资源统一编号,所有进程只能采用按序号递增的形式申请资源。

13.为什么不建议使用Executor静态工程来创建线程池?

阿里巴巴Java开发手册,明确指出不允许使用Executors静态工厂构建线程池
原因是线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:
1:FixedThreadPool 和 SingleThreadPool:
允许的请求队列(底层实现是LinkedBlockingQueue)长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
2:CachedThreadPool 和 ScheduledThreadPool
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

14.如何创建线程池?

调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。

private static ExecutorService executor = new ThreadPoolExecutor(10,10,60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue(10));

15.线程池常用的参数有哪些?

  • corePoolSize:核心线程数量,会一直存在,除非allowCoreThreadTimeOut设置为true
  • maximumPoolSize:线程池允许的最大线程池数量
  • keepAliveTime:线程数量超过corePoolSize,空闲线程的最大超时时间
  • unit:超时时间的单位
  • workQueue:工作队列,保存未执行的Runnable 任务
  • threadFactory:创建线程的工厂类
  • handler:当线程已满,工作队列也满了的时候,会被调用。被用来实现各种拒绝策略。

16.常用的线程池有哪些?

  • newSingleThreadExecutor
    单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务
  • newFixedThreadExecutor(n)
    固定数量的线程池,每提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行
  • newCacheThreadExecutor(推荐使用)
    可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。
  • newScheduleThreadExecutor
    大小无限制的线程池,支持定时和周期性的执行线程

17.说一下ThreadPoolExecutor线程池的执行流程是什么样的?

  • 线程池的创建: 首先,创建一个 ThreadPoolExecutor 实例,并配置线程池的参数,包括核心线程数、最大线程数、线程存活时间、任务队列等。这些参数决定了线程池的行为。

  • 任务提交: 将任务提交给线程池执行。任务可以是 Runnable 或 Callable 接口的实现类。线程池会管理任务的执行。

  • 核心线程处理任务: 如果当前线程池中的线程数量没有达到核心线程数,那么会创建新的核心线程来执行任务。每个任务都会分配一个线程来处理。

  • 任务队列存储任务: 如果线程池中的线程数已经达到核心线程数,但是任务继续提交,那么这些任务会被存储在任务队列中等待执行。任务队列可以是 LinkedBlockingQueue、ArrayBlockingQueue 等类型,具体根据线程池配置而定。

  • 最大线程处理任务: 如果任务队列已满,且线程池中的线程数量还没有达到最大线程数,那么线程池会创建新的线程来处理任务,直到线程数达到最大线程数。

  • 饱和策略处理任务: 如果线程池中的线程数已经达到最大线程数,而且任务队列也已满,那么会根据设置的饱和策略来处理任务。饱和策略可以是丢弃任务、抛出异常、阻塞等等,具体取决于线程池的配置。

  • 任务执行: 线程池中的线程会从任务队列中取出任务并执行。执行完成后,线程可能会被回收(如果超过核心线程数,根据线程存活时间和空闲时间策略),或者继续执行其他任务。

  • 任务完成: 任务执行完成后,线程池可以将结果返回给调用方(对于 Callable 任务),或者什么都不返回(对于 Runnable 任务)。

  • 线程池维护: 线程池会根据配置自动维护线程数量,例如根据线程存活时间和空闲时间策略来回收或创建线程。

  • 关闭线程池: 当不再需要线程池时,应该调用 shutdown() 或 shutdownNow() 方法来关闭线程池,释放资源。

你可能感兴趣的:(面试,java,开发语言,面试)