27道多线程核心面试题(附答案),你真的准备好面试了吗?

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文

  • LockSupport.parkNanos(long…)

  • LockSupport.parkUntil(long…)

2.TERMINATED: 终止线程的线程状态。线程已完成执行

阿里面试官首次分享完整版多线程核心题,你准备好跳槽了吗?

7. 程序开多少线程合适?

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

1.这里需要区别下应用是什么样的程序:

CPU 密集型程序,一个完整请求,I/O操作可以在很短时间内完成, CPU还有很多运算要处理,也就是说 CPU 计算的比例占很大一部分,线程等待时间接近0

  • 单核CPU: 一个完整请求,I/O操作可以在很短时间内完成, CPU还有很多运算要处理,也就是说 CPU 计算的比例占很大一部分,线程等待时间接近0。单核CPU处 理CPU密集型程序,这种情况并不太适合使用多线程

  • 多核 : 如果是多核CPU 处理 CPU 密集型程序,我们完全可以最大化地利用 CPU核心数,应用并发编程来提高效率。CPU 密集型程序的最佳线程数就是:因此对于CPU 密集型来说,理论上 线程数量 = CPU 核数(逻辑),但是实际上,数量一般会设置为 CPU 核数(逻辑)+ 1(经验值)计算(CPU)密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作

2.I/O 密集型程序,与 CPU 密集型程序相对,一个完整请求,CPU运算操作完成之后还有很多 I/O 操作要做,也就是说 I/O 操作占比很大部分,等待时间较长,线程等待时间所占比例越高,需要越多线程;线程CPU时间所占比例越高,需要越少线程

  • I/O 密集型程序的最佳线程数就是: 最佳线程数 = CPU核心数 (1/CPU利用率) =CPU核心数 (1 + (I/O耗时/CPU耗时))

  • 如果几乎全是 I/O耗时,那么CPU耗时就无限趋近于0,所以纯理论你就可以说是2N(N=CPU核数),当然也有说 2N + 1的,1应该是backup

  • 一般我们说 2N + 1 就即可

8. 描述一下notify和notifyAll区别?

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

1.首先最好说一下 锁池 和 等待池 的概念

  • 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

  • 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.

2.如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁

3.当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而otifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争

4.所谓唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。

9. 描述一下synchronized和lock区别 ?

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

如下表

阿里面试官首次分享完整版多线程核心题,你准备好跳槽了吗?

可以再多说下 synchronized的 加锁流程

由于HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低从而引入偏向锁。偏向锁在获取资源的时候会在锁对象头上记录当前线程ID,偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断锁对象头中线程ID是否为自己,如果是当前线程重入,直接进入同步操作,不需要额外的操作。默认在开启偏向锁和轻量锁的情况下,当线程进来时,首先会加上偏向锁,其实这里只是用一个状态来控制,会记录加锁的线程,如果是线程重入,则不会进行锁升级。获取偏向锁

流程:

1. 判断是否为可偏向状态–MarkWord中锁标志是否为‘01’,是否偏向锁是否为‘1’

2. 如果是可偏向状态,则查看线程ID是否为当前线程,如果是,则进入步骤 ‘V’,否则进入步骤‘III’

3. 通过CAS操作竞争锁,如果竞争成功,则将MarkWord中线程ID设置为当前线程ID,然后执行‘V’;竞争失败,则执行‘IV’

4. CAS获取偏向锁失败表示有竞争。当达到safepoint时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块

5. 执行同步代码

轻量级锁是相对于重量级锁需要阻塞/唤醒涉及上下文切换而言,主要针对多个线程在不同时间请求同一把锁的场景。轻量级锁获取过程:

  • 进行加锁操作时,jvm会判断是否已经是重量级锁,如果不是,则会在当前线程栈帧中划出一块空间,作为该锁的锁记录,并且将锁对象MarkWord复制到该锁记录中

  • 复制成功之后,jvm使用CAS操作将对象头MarkWord更新为指向锁记录的指针,并将锁记录里的owner指针指向对象头的MarkWord。如果成功,则执行‘III’,否则执行‘IV’

  • 更新成功,则当前线程持有该对象锁,并且对象MarkWord锁标志设置为‘00’,即表示此对象处于轻量级锁状态

  • 更新失败,jvm先检查对象MarkWord是否指向当前线程栈帧中的锁记录,如果是则执行‘V’,否则执行‘VI’

  • 表示锁重入;然后当前线程栈帧中增加一个锁记录第一部分(Displaced Mark Word) 为null,并指向Mark Word的锁对象,起到一个重入计数器的作用

  • 表示该锁对象已经被其他线程抢占,则进行自旋等待(默认10次),等待次数达到阈值仍未获取到锁,则升级为重量级锁

当有多个锁竞争轻量级锁则会升级为重量级锁,重量级锁正常会进入一个cxq的队列,在调用wait方法之后,则会进入一个waitSet的队列park等待,而当调用notify方法唤醒之后,则有可能进入EntryList。重量级锁加锁过程:

  • 分配一个ObjectMonitor对象,把Mark Word锁标志置为‘10’,然后Mark Word存储指向ObjectMonitor对象的指针。ObjectMonitor对象有两个队列和一个指针,每个需要获取锁的线程都包装成ObjectWaiter对象

  • 多个线程同时执行同一段同步代码时,ObjectWaiter先进入EntryList队列,当某个线程获取到对象的monitor以后进入Owner区域,并把monitor中的owner变量设置为当前线程同时monitor中的计数器count+1;

10.简单描述一下ABA问题?

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

1.有两个线程同时去修改一个变量的值,比如线程1、线程2,都更新变量值,将变量值从A更新成B。

2.首先线程1、获取到CPU的时间片,线程2由于某些原因发生阻塞进行等待,此时线程1进行比较更新(CompareAndSwap),成功将变量的值从A更新成B。

3.更新完毕之后,恰好又有线程3进来想要把变量的值从B更新成A,线程3进行比较更新,成功将变量的值从B更新成A。 4. 线程2获取到CPU的时间片,然后进行比较更新,发现值是预期的A,然后又更新成了B。但是线程1并不知道,该值已经有了A->B->A这个过程,这也就是我们常说的ABA问题。

4.可以通过加版本号或者加时间戳解决,或者保证单向递增或者递减就不会存在此类问题。

11.实现一下DCL?

===========

阿里面试官首次分享完整版多线程核心题,你准备好跳槽了吗?

12.实现一个阻塞队列(用Condition写生产者与消费者就)?

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

阿里面试官首次分享完整版多线程核心题,你准备好跳槽了吗?

阿里面试官首次分享完整版多线程核心题,你准备好跳槽了吗?

13.实现多个线程顺序打印abc?

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

阿里面试官首次分享完整版多线程核心题,你准备好跳槽了吗?

阿里面试官首次分享完整版多线程核心题,你准备好跳槽了吗?

阿里面试官首次分享完整版多线程核心题,你准备好跳槽了吗?

阿里面试官首次分享完整版多线程核心题,你准备好跳槽了吗?

14.服务器CPU数量及线程池线程数量的关系?

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

首先确认业务是CPU密集型还是IO密集型的,

如果是CPU密集型的,那么就应该尽量少的线程数量,一般为CPU的核数+1;

如果是IO密集型:所以可多分配一点 cpu核数*2 也可以使用公式:CPU 核数 / (1 - 阻塞系数);其中阻塞系数 在 0.8 ~ 0.9 之间。

15.多线程之间是如何通信的?

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

1、通过共享变量,变量需要volatile 修饰

2、使用wait()和notifyAll()方法,但是由于需要使用同一把锁,所以必须通知线程释放锁,被通知线程才能获取到锁,这样导致通知不及时。

3、使用CountDownLatch实现,通知线程到指定条件,调用countDownLatch.countDown(),被通知线程进行countDownLatch.await()。

4、使用Condition的await()和signalAll()方法。

16.synchronized关键字加在静态方法和实例方法的区别?

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

修饰静态方法,是对类进行加锁,如果该类中有methodA 和methodB都是被synchronized修饰的静态方法,此时有两个线程T1、T2分别调用methodA()和methodB(),则T2会阻塞等待直到T1执行完成之后才能执行。

修饰实例方法时,是对实例进行加锁,锁的是实例对象的对象头,如果调用同一个对象的两个不同的被synchronized修饰的实例方法时,看到的效果和上面的一样,如果调用不同对象的两个不同的被synchronized修饰的实例方法时,则不会阻塞。

17.countdownlatch的用法?

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

两种用法:

1、让主线程await,业务线程进行业务处理,处理完成时调用countdownLatch.countDown(),CountDownLatch实例化的时候需要根据业务去选择CountDownLatch的count;

2、让业务线程await,主线程处理完数据之后进行countdownLatch.countDown(),此时业务线程被唤醒,然后去主线程拿数据,或者执行自己的业务逻辑。

18.线程池问题:

=========

(1)Executor提供了几种线程池

1、newCachedThreadPool()(工作队列使用的是 SynchronousQueue)

创建一个线程池,如果线程池中的线程数量过大,它可以有效地回收多余的线程,如果线程数不足,那么它可 以创建新的线程。

不足:这种方式虽然可以根据业务场景自动的扩展线程数来处理我们的业务,但是最多需要多少个线程同时处 理却是我们无法控制的。

优点:如果当第二个任务开始,第一个任务已经执行结束,那么第二个任务会复用第一个任务创建的线程,并 不会重新创建新的线程,提高了线程的复用率。

作用:该方法返回一个可以根据实际情况调整线程池中线程的数量的线程池。即该线程池中的线程数量不确 定,是根据实际情况动态调整的。

2、newFixedThreadPool()(工作队列使用的是 LinkedBlockingQueue)

这种方式可以指定线程池中的线程数。如果满了后又来了新任务,此时只能排队等待。

优点:newFixedThreadPool 的线程数是可以进行控制的,因此我们可以通过控制最大线程来使我们的服务 器达到最大的使用率,同时又可以保证即使流量突然增大也不会占用服务器过多的资源。

作用:该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也 不会销毁已经创建好的线程,自始自终都是那几个固定的线程在工作,所以该线程池可以控制线程的最大并发数

3、newScheduledThreadPool()

该线程池支持定时,以及周期性的任务执行,我们可以延迟任务的执行时间,也可以设置一个周期性的时间让 任务重复执行。该线程池中有以下两种延迟的方法。

scheduleAtFixedRate 不同的地方是任务的执行时间,如果间隔时间大于任务的执行时间,任务不受执行 时间的影响。如果间隔时间小于任务的执行时间,那么任务执行结束之后,会立马执行,至此间隔时间就会被 打乱。

scheduleWithFixedDelay 的间隔时间不会受任务执行时间长短的影响。

作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。

4、newSingleThreadExecutor()

这是一个单线程池,至始至终都由一个线程来执行。

作用:该方法返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务 队列中,等待这一个线程空闲,当这个线程空闲了再按 FIFO 方式顺序执行任务队列中的任务。

5、

newSingleThreadScheduledExecutor()

只有一个线程,用来调度任务在指定时间执行。

作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。只不过和上面的区别是该线 程池大小为 1,而上面的可以指定线程池的大小。

(2)线程池的参数

int corePoolSize,//线程池核心线程大小

int maximumPoolSize,//线程池最大线程数量

long keepAliveTime,//空闲线程存活时间

TimeUnit unit,//空闲线程存活时间单位,一共有七种静态属性(TimeUnit.DAYS天,TimeUnit.HOURS 小时,TimeUnit.MINUTES分钟,TimeUnit.SECONDS秒,TimeUnit.MILLISECONDS毫 秒,TimeUnit.MICROSECONDS微妙,TimeUnit.NANOSECONDS纳秒)

BlockingQueue workQueue,//工作队列

ThreadFactory threadFactory,//线程工厂,主要用来创建线程(默认的工厂方法是:

Executors.defaultThreadFactory()对线程进行安全检查并命名)

RejectedExecutionHandler handler//拒绝策略(默认是:

ThreadPoolExecutor.AbortPolicy不 执行并抛出异常)

(3)拒绝策略

当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进 来,就会执行拒绝策略。

jdk中提供了4种拒绝策略:

ThreadPoolExecutor.CallerRunsPolicy:该策略下,在调用者线程中直接执行被拒绝任务的 run 方法,除非线程池已经 shutdown,则直接抛弃任 务。

ThreadPoolExecutor.AbortPolicy:该策略下,直接丢弃任务,并抛出 RejectedExecutionException 异常。

ThreadPoolExecutor.DiscardPolicy:该策略下,直接丢弃任务,什么都不做。

ThreadPoolExecutor.DiscardOldestPolicy:该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。

除此之外,还可以根据应用场景需要来实现 RejectedExecutionHandler 接口自定义策略。

(4)任务放置的顺序过程

任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。 了解这部分就相当于了解了线程池的核心运行机制。

首先,所有任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行 线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝 该任务。其执行过程如下:

首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。

如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。

如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。

如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队 列已满,则创建并启动一个线程来执行新提交的任务。

如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

其执行流程如下图所示:

阿里面试官首次分享完整版多线程核心题,你准备好跳槽了吗?

(5)任务结束后会不会回收线程

final void runWorker(Worker w) {

Thread wt = Thread.currentThread();

Runnable task = w.firstTask;

w.firstTask = null;

w.unlock(); // allow interrupts

boolean completedAbruptly = true;

try {

while (task != null || (task = getTask()) != null) {

w.lock();

if ((runStateAtLeast(ctl.get(), STOP) ||

(Thread.interrupted() &&

runStateAtLeast(ctl.get(), STOP))) &&

!wt.isInterrupted())

wt.interrupt();

try {

beforeExecute(wt, task);

Throwable thrown = null;

try {

task.run();

} catch (RuntimeException x) {

thrown = x; throw x;

} catch (Error x) {

thrown = x; throw x;

} catch (Throwable x) {

thrown = x; throw new Error(x);

} finally {

afterExecute(task, thrown);

}

} finally {

task = null;

w.completedTasks++;

w.unlock();

}

}

completedAbruptly = false;

} finally {

processWorkerExit(w, completedAbruptly);

}

}

首先线程池内的线程都被包装成了一个个的

java.util.concurrent.ThreadPoolExecutor.Worker,然 后这个worker会马不停蹄的执行任务,执行完任务之后就会在while循环中去取任务,取到任务就继续执行,取 不到任务就跳出while循环(这个时候worker就不能再执行任务了)执行 processWorkerExit方法,这个方 法呢就是做清场处理,将当前woker线程从线程池中移除,并且判断是否是异常进入processWorkerExit方 法,如果是非异常情况,就对当前线程池状态(RUNNING,shutdown)和当前工作线程数和当前任务数做判断,是否要加入一个新的线程去完成最后的任务.

(6)未使用的线程池中的线程放在哪里

private final HashSet workers = new HashSet();

(7)线程池线程存在哪

private final HashSet workers = new HashSet();

19.Java多线程的几种状态及线程各个状态之间是如何切换的?

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

阿里面试官首次分享完整版多线程核心题,你准备好跳槽了吗?

阿里面试官首次分享完整版多线程核心题,你准备好跳槽了吗?

20.如何在方法栈中进行数据传递?

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

通过方法参数传递;通过共享变量;如果在用一个线程中,还可以使用ThreadLocal进行传递.

21.描述一下ThreadLocal的底层实现形式及实现的数据结构?

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

Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null:

阿里面试官首次分享完整版多线程核心题,你准备好跳槽了吗?

只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建它们。

阿里面试官首次分享完整版多线程核心题,你准备好跳槽了吗?

除此之外,和我所想的不同的是,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量,防止出现内存泄漏。

阿里面试官首次分享完整版多线程核心题,你准备好跳槽了吗?

22.Sychornized是否是公平锁?

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

不是公平锁

23.描述一下锁的四种状态及升级过程?

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

以下是32位的对象头描述

阿里面试官首次分享完整版多线程核心题,你准备好跳槽了吗?

synchronized锁的膨胀过程:

当线程访问同步代码块。首先查看当前锁状态是否是偏向锁(可偏向状态)

最后

毕竟工作也这么久了 ,除了途虎一轮,也七七八八面试了不少大厂,像阿里、饿了么、美团、滴滴这些面试过程就不一一写在这篇文章上了。我会整理一份详细的面试过程及大家想知道的一些问题细节

美团面试经验

美团面试
字节面试经验
字节面试
菜鸟面试经验
菜鸟面试
蚂蚁金服面试经验
蚂蚁金服
唯品会面试经验
唯品会

因篇幅有限,图文无法详细发出

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
8906c69ef4bb210abec48.png)

synchronized锁的膨胀过程:

当线程访问同步代码块。首先查看当前锁状态是否是偏向锁(可偏向状态)

最后

毕竟工作也这么久了 ,除了途虎一轮,也七七八八面试了不少大厂,像阿里、饿了么、美团、滴滴这些面试过程就不一一写在这篇文章上了。我会整理一份详细的面试过程及大家想知道的一些问题细节

美团面试经验

[外链图片转存中…(img-GbA4TjMQ-1713380369932)]
字节面试经验
[外链图片转存中…(img-5kZcpViZ-1713380369932)]
菜鸟面试经验
[外链图片转存中…(img-19jmZ2C4-1713380369932)]
蚂蚁金服面试经验
[外链图片转存中…(img-WIzuaRLn-1713380369932)]
唯品会面试经验
[外链图片转存中…(img-B5ZgyFZV-1713380369933)]

因篇幅有限,图文无法详细发出

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-90YLcUoo-1713380369933)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 18
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值