写点什么

Java 多线程 高频面试题

  • 2022 年 5 月 12 日
  • 本文字数:5574 字

    阅读完需:约 18 分钟



程序计数器有以下两个作用:


  1. 字节码解释器通过改变程序计数器来依次读取指令,从?实现代码的流程控制,如:顺序执?、选择、循环、异常处理;

  2. 在多线程的情况下,程序计数器?于记录当前线程执?的位置,从?当线程被切换回来的时候能够知道该线程上次运?到哪?了。


所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。


[](()2.2 虚拟机栈和本地方法栈为什么是私有的




  • 虚拟机栈: 每个 Java ?法在执?的同时会创建?个栈帧?于存储局部变量表、操作数栈、常量池引?等信息。每一 Java 方法从调??执?完成的过程,就对应着?个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

  • 本地?法栈: 和虚拟机栈所发挥的作??常相似,区别是: 虚拟机栈为虚拟机执? Java ?法 (也就是字节码)服务,?本地?法栈则为虚拟机使?到的 Native ?法服务。


所以,虚拟机栈和本地方法栈是私有的是为了保证线程中的局部变量不被别的线程访问到。


[](()3 多线程的概念


===========================================================================


[](()3.1 并发和并行的区别




  • 并发:同一时间段内,多个任务都在执行。(单位时间内不一定同时执行)

  • 并行:单位时间内,多个任务同时执行。


[](()3.2 上下文切换




多线程编程中?般线程的个数都?于 CPU 核?的个数,??个 CPU 核?在任意时刻只能被?个线程使?,为了让这些线程都能得到有效执?,CPU 采取的策略是为每个线程分配时间?并轮转的形式。当?个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使?,这个过程就属于?次上下?切换。


概括来说就是:当前任务在执?完 CPU 时间?切换到另?个任务之前会先保存??的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是?次上下?切换


上下?切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒??上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下?切换对系统来说意味着消耗?量的 CPU 时间,事实上,可能是操作系统中时间消耗最?的操作。


Linux 相?与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有?项就是,其上下?切换和模式切换的时间消耗?常少。


[](()3.3 为什么使用多线程




  1. 在多 CPU 系统中,可以利用多线程将计算逻辑分配到多个处理器核心上,从而提高 CPU 的利用率;

  2. 改善程序结构。就是将一个即长又复杂的进程划分为多个线程来处理,使程序便于理解和修改。


多编程的?的就是为了能提?程序的执?效率、提?程序运?速度,然而多线程的缺点也有:线程的滥用会给系统带来上下文切换的额外负担,并且线程间的共享变量可能造成死锁的出现。


1 什么时候需要用到多线程


我认为当程序需要提高运行速度,同时改善程序结构时需要利用到多线程。


2 什么是线程安全


线程安全指的是当一个线程在操作一个方法或者语句时,其它线程不能对其进行操作,只能等到该线程结束后才可以进行访问。


3 能想到某个业务场景来需要线程安全吗?


一:比如生产者和消费者模式中,产品的数量就需要保证线程安全,否则当多个线程进行操作时,就可能导致脏读的发生,因为多个线程可能读到的是相同的值,从而导致产品总量超出。


[](()3.4 线程的生命周期和状态





  • 一个线程在创建之后就会处于 NEW(新建)状态,此时调用 start() 方法后线程会进入到 RUNNABLE(运行)状态;

  • 当线程执行 wait() 方法后会进入到 WITTING(等待)状态,进入等待状态的线程需要依靠其它线程的通知才能够返回到运行状态;

  • 当使用 sleep(long time) 或者 wait(long time) 方法可以将线程置于 TIME_WAITING(超时等待)状态,等时间一结束就会自动进入到运行状态;

  • 当线程调用同步方法时,如果该线程没有获得锁的话,该线程就会进入到 BLOCK(阻塞)状态;

  • 一个线程结束之后就会进入到 TERMINATED(终止)状态;


[](()3.5 什么是线程死锁?如何避免死锁?




死锁是指两个或多个线程互相持有对方所需要的资源,并请求锁定对方的资源,导致这些线程一直处于等待其它线程释放资源的状态。


死锁的必要条件


  1. 互斥条件:某个资源在任意?个时刻只由?个线程占用;

  2. 请求与保持条件:?个线程因请求资源?阻塞时,对已获得的资源保持不放;

  3. 不剥夺条件:线程已获得的资源在末使?完之前不能被其他线程强?剥夺,只有自己使?完毕后才释放资源;

  4. 循环等待条件:若干线程之间形成?种头尾相接的循环等待资源关系。


如何避免死锁


我上?说了产?死锁的四个必要条件,为了避免死锁,我们只要破坏产?死锁的四个条件中的其中?个就可以了。现在我们来挨个分析?下:


  1. 破坏互斥条件 :这个条件我们没有办法破坏,因为我们?锁本来就是想让他们互斥的;

  2. 破坏请求与保持条件 :?次性申请所有的资源;

  3. 破坏不剥夺条件 :占?部分资源的线程进?步申请其他资源时,如果申请不到,可以主动释放它占有的资源;

  4. 破坏循环等待条件:通过按照顺序来申请资源,按某?顺序申请资源,释放资源则反序释放。


[](()4 多线程的使用


===========================================================================


[](()4.1 多线程的实现方法




创建多线程的方式有四种,分别是:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口、使用线程池。


四种方式的差异如下:


| 创建多线程的方式 | 特征 |


| --- | --- |


| 继承 Thread 类 | 每次使用时,只需要实现继承了 Thread 类的子类就行:HelloThread h1 = new HelloThread(); |


| 实现 Runnable 接口 | 使用时首先创建实现了接口的类的对象,然后将此对象作为参数传递到 Thread 类的构造器中,从而创建 Thread 类的对象 Window1 w = new Window1(); Thread t1 = new Thread(w);。相比于继承 Thread 类,1)实现的方式没有类的单继承性的局限性;2)降低了线程对象与线程任务之间的耦合性,增强了程序的可扩展性。 |


| 实现 Callable 接口 | 相比于实现 Runnable 接口,1)call()可以有返回值的;2)call()可以抛出异常,被外面的操作捕获,获取异常的信息。而之前的方法只能是 try catch 捕获异常;3)Callable 是支持泛型的。 |


| 使用线程池 | 用从学校到西湖举例,前三种方法创建多线程就是每次去都要造一辆自行车,骑到西湖后就把自行车销毁,而线程池的方法就是搭乘公共交通去西湖。因此,这一种是最常用的方法,好处是:1)提高响应速度(减少了创建新线程的时间);2)降低资源消耗(重复利用线程池中线程,不需要每次都创建);3)便于线程管理。 |


可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接?,这样代码看起来会更加简洁。


参考文献:[传送门](()


[](()4.2 sleep() ?法和 wait() 方法的区别




| | sleep() | wait() |


| :-: | :-: | :-: |


| 来源 | 来自 Thread 类 | 来自 Object 类 |


| 对锁的影响 | 没有释放锁 | 释放锁 |


| 使用范围 | 任何地方 | 只能在同步控制方法或者同步控制块里面使用,否则会抛 IllegalMonitorStateException |


| 恢复方式 | 时间到了之后线程会?动苏醒 | 需要其他线程调用同一对象的 notify()/nofityAll() 才能重新恢复 |


1 sleep() 和 wait() 的区别


  1. sleep() 是 Tread 类的方法,而 wait() 是 Object 类的方法;

  2. sleep() 不会释放锁,而 wait() 会释放锁;

  3. sleep() 在任何方法里都可以使用,而 wait() / notify() 只能在同步方法中使用。


[](ht 《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 tps://blog.csdn.net/weixin_44262126/article/details/120907729)4.3 sleep() 方法和 yield() 方法的区别




线程执行 sleep() 方法后进入超时等待状态,而执行 yield() 方法后进入就绪状态。


sleep() 方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程运行的机会;yield() 方法只会给相同优先级或更高优先级的线程以运行的机会。


[](()4.4 join() 方法的作用




线程中的 join() 用来等待指定线程终止,然后继续执行当前线程。


比如线程 A 执行了 threadB.join() 之后,其含义就是线程 A 等待 threadB 线程终止之后才从 threadB.join() 继续往下执行自己的代码。


[](()4.5 Thread 调用 start() 方法和调用 run() 方法的区别




  • run() :普通的方法调用,在主线程中执行,不会新建一个线程来执行;

  • start():新启动一个线程,这时此线程处于就绪(可运行)状态,一旦得到 CPU 时间片,就开始执行 run() 方法。


总结: 调? start() ?法?可启动线程并使线程进?就绪状态,直接执? run() ?法的话不会以多线程的?式执?。


[](()4.6 synchronized 关键字




参考 Java 基础部分对其的讲解;


构造?法不能使? synchronized 关键字修饰。构造?法本身就属于线程安全的,不存在同步的构造?法?说。


[](()4.7 synchronized 和 Lock 的区别




  • Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;

  • Lock 在发生异常时,如果没有主动通过 unLock() 去释放锁,很可能会造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;synchronized 在发生异常时,会自动释放锁,因此不会导致死锁现象发生;

  • Lock 的使用更加灵活,可以有响应中断、有超时时间等;而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,直到获取到锁;

  • 在性能上,随着近些年 synchronized 的不断优化,Lock 和 synchronized 在性能上已经没有很明显的差距了,所以性能不应该成为我们选择两者的主要原因。官方推荐尽量使用 synchronized,除非 synchronized 无法满足需求时,则可以使用 Lock。


1 synchronized 和 ReentrantLock 的异同


  • ReentrantLock 是 Lock 接口的一个实现类,而 synchronized 是 Java 中的关键字;

  • ReentrantLock 在发生异常时,需要手动声明释放锁,所以最好在 finally 中声明释放锁,而 synchronized 在发生异常时,会自动释放锁,因此不会导致死锁现象发生;

  • ReentrantLock 更加灵活,提供的方法也更多。


[](()4.8 synchronized 关键字和 volatile 关键字的区别




synchronized 关键字和 volatile 关键字是两个互补的存在,?不是对?的存在。


  • volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定? synchronized 关键字要好;

  • volatile 关键字只能?于变量,? synchronized 关键字还可以修饰?法以及代码块;

  • volatile 关键字能保证数据的可?性,但不能保证数据的原?性。 synchronized 关键字两者都能保证;

  • volatile 关键字主要?于保证变量在多个线程之间的可?性,? synchronized 关键字解决的是多个线程之间访问资源的同步性。


[](()4.9 为什么要弄?个 CPU 高速缓存




CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题,


缓存是内存中少部分数据的复制品。


[](()5 线程池


========================================================================


[](()5.1 为什么使用线程池




池化技术的思想主要是为了减少每次获取资源的消耗,提?对资源的利?率。


使?线程池的好处:


  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗;

  • 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能?即执?;

  • 提高线程的可管理性:线程是稀缺资源,如果?限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使?线程池可以进?统?的分配,调优和监控。


[](()5.2 线程池的原理





[](()5.3 线程池的代码实现




/**


  • 创建线程的方式四:使用线程池

  • 好处:

  • 1.提高响应速度(减少了创建新线程的时间)

  • 2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)

  • 3.便于线程管理


*/


public class Pool {


public static void main(String[] args) {


//1. 提供指定线程数量的线程池


ExecutorService service = Executors.newFixedThreadPool(10);


// service1.setCorePoolSize(15);


// service1.setKeepAliveTime();


//2.执行指定的线程的操作。需要提供实现 Runnable 接口或 Callable 接口实现类的对象


service.execute(new NumberThread());//适合适用于 Runnable


service.execute(new NumberThread1());//适合适用于 Runnable


//3.关闭连接池


service.shutdown();


}


}


class NumberThread implements Runnable{


@Override


public void run() {


// 逻辑代码


}


}


class NumberThread1 implements Runnable{


@Override


public void run() {


// 逻辑代码


}


}


[](()5.4 线程池的核心属性




  • hreadFactory(线程工厂):用于创建工作线程的工厂;

  • corePoolSize(核心线程数):当线程池运行的线程少于 corePoolSize 时,将创建一个新线程来处理请求,即使其他工作线程处于空闲状态;

  • workQueue(队列):用于保留任务并移交给工作线程的阻塞队列;

  • maximumPoolSize(最大线程数):线程池允许开启的最大线程数;

  • handler(拒绝策略):往线程池添加任务时,将在下面两种情况触发拒绝策略:1)线程池运行状态不是 RUNNING;2)线程池已经达到最大线程数,并且阻塞队列已满时;

  • keepAliveTime(保持存活时间):如果线程池当前线程数超过 corePoolSize,则多余的线程空闲时间超过 keepAliveTime 时会被终止。


[](()5.5 线程池的拒绝策略(饱和策略)




  • AbortPolicy:中止策略。默认的拒绝策略,直接抛出 RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。

  • DiscardPolicy:抛弃策略。什么都不做,直接抛弃被拒绝的任务。

  • DiscardOldestPolicy:抛弃最老策略。抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。如果阻塞队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用。

  • CallerRunsPolicy:调用者运行策略。在调用者线程中执行该任务。该策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者(调用线程池执行任务的主线程),由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有时间来处理完正在执行的任务。


[](()5.6 一些面试问题




1 线程池是什么

用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
Java多线程 高频面试题_Java_爱好编程进阶_InfoQ写作社区