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

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

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

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

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

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

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

正文

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

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锁的膨胀过程:

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

1、如果是偏向锁:

1.1、检查当前mark word中记录是否是当前线程id,如果是当前线程id,则获得偏向锁执行同步代码 块。

1.2、如果不是当前线程id,cas操作替换线程id,替换成功获得偏向锁(线程复用),替换失败锁撤销升 级轻量锁(同一类对象多次撤销升级达到阈值20,则批量重偏向,这个点可以稍微提一下,详见下面的注意)

2、升级轻量锁

升级轻量锁对于当前线程,分配栈帧锁记录lock_record(包含mark word和object-指向锁记录首地 址),对象头mark word复制到线程栈帧的锁记录 mark word存储的是无锁的hashcode(里面有重入次数 问题)。

3、重量级锁(纯理论可结合源码)

CAS自旋达到一定次数升级为重量级锁(多个线程同时竞争锁时)

存储在ObjectMonitor对象,里面有很多属性ContentionList、EntryList 、WaitSet、 owner。当一个线程尝试获取锁时,如果该锁已经被占用,则该线程封装成ObjectWaiter对象插到 ContentionList队列的对首,然后调用park挂起。该线程锁时方式会从ContentionList或EntryList挑 一个唤醒。线程获得锁后调用Object的wait方法,则会加入到WaitSet集合中(当前锁或膨胀为重量级锁)

注意:

1.偏向锁在JDK1.6以上默认开启,开启后程序启动几秒后才会被激活

2.偏向锁撤销是需要在safe_point,也就是安全点的时候进行,这个时候是stop the word的,所以说偏向 锁的撤销是开销很大的,如果明确了项目里的竞争情况比较多,那么关闭偏向锁可以减少一些偏向锁撤销的开销

3.以class为单位,为每个class维护一个偏向锁撤销计数器。每一次该class的对象发生偏向撤销操作时 (这个时候进入轻量级锁),该计数器+1,当这个值达到重偏向阈值(默认20,也就是说前19次进行加锁的时候, 都是假的轻量级锁,当第20次加锁的时候,就会走批量冲偏向的逻辑)时,JVM就认为该class的偏向锁有问 题,因此会进行批量重偏向。每个class对象也会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch值。每次发生批量重偏向时,就将该值+1, 同时遍历JVM中所有线程的站,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次 获取锁时,发现当前对象的epoch值和class不相等,那就算当前已经偏向了其他线程,也不会执行撤销操 作,而是直接通过CAS操作将其mark word的Thread Id改为当前线程ID

23. CAS的ABA问题怎么解决的?

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

通过加版本号控制,只要有变更,就更新版本号

24. 描述一下AQS?

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

状态变量state:

AQS中定义了一个状态变量state,它有以下两种使用方法:

(1)互斥锁

当AQS只实现为互斥锁的时候,每次只要原子更新state的值从0变为1成功了就获取了锁,可重入是通过不断把state原子更新加1实现的。

(2)互斥锁 + 共享锁

当AQS需要同时实现为互斥锁+共享锁的时候,低16位存储互斥锁的状态,高16位存储共享锁的状态,主要用于实现读写锁。

互斥锁是一种独占锁,每次只允许一个线程独占,且当一个线程独占时,其它线程将无法再获取互斥锁及共享锁,但是它自己可以获取共享锁。

共享锁同时允许多个线程占有,只要有一个线程占有了共享锁,所有线程(包括自己)都将无法再获取互斥锁,但是可以获取共享锁。

AQS队列

AQS中维护了一个队列,获取锁失败(非tryLock())的线程都将进入这个队列中排队,等待锁释放后唤醒下一个排队的线程(互斥锁模式下)。

condition队列

AQS中还有另一个非常重要的内部类ConditionObject,它实现了Condition接口,主要用于实现条件锁。

ConditionObject中也维护了一个队列,这个队列主要用于等待条件的成立,当条件成立时,其它线程将signal这个队列中的元素,将其移动到AQS的队列中,等待占有锁的线程释放锁后被唤醒。

Condition典型的运用场景是在BlockingQueue中的实现,当队列为空时,获取元素的线程阻塞在notEmpty条件上,一旦队列中添加了一个元素,将通知notEmpty条件,将其队列中的元素移动到AQS队列中等待被唤醒。

获取锁、释放锁的这些方法基本上都穿插在ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch的源码解析中

需要子类实现的方法

上面一起学习了AQS中几个重要的模板方法,下面我们再一起学习下几个需要子类实现的方法:

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

这几个方法为什么不直接定义成抽象方法呢?

因为子类只要实现这几个方法中的一部分就可以实现一个同步器了,所以不需要定义成抽象方法。

25.介绍一下volatile的功能?

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

保证线程可见性

防止指令重排序

26.volatile的可见性和禁止指令重排序怎么实现的?

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

可见性:

volatile的功能就是被修饰的变量在被修改后可以立即同步到主内存,被修饰的变量在每次是用之前都从主内存刷新。本质也是通过内存屏障来实现可见性

写内存屏障(Store Memory Barrier)可以促使处理器将当前store buffer(存储缓存)的值写回主存。

读内存屏障(Load Memory Barrier)可以促使处理器处理invalidate queue(失效队列)。进而避免由于Store Buffer和Invalidate Queue的非实时性带来的问题。

  • 禁止指令重排序:

volatile是通过内存屏障来禁止指令重排序JMM内存屏障的策略

在每个 volatile 写操作的前面插入一个 StoreStore 屏障。

在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。

最后

各位读者,由于本篇幅度过长,为了避免影响阅读体验,下面我就大概概括了整理了

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

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

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
ory Barrier)可以促使处理器处理invalidate queue(失效队列)。进而避免由于Store Buffer和Invalidate Queue的非实时性带来的问题。

  • 禁止指令重排序:

volatile是通过内存屏障来禁止指令重排序JMM内存屏障的策略

在每个 volatile 写操作的前面插入一个 StoreStore 屏障。

在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。

最后

各位读者,由于本篇幅度过长,为了避免影响阅读体验,下面我就大概概括了整理了

[外链图片转存中…(img-TJL90OqT-1713380344912)]

[外链图片转存中…(img-Xttorcd4-1713380344912)]

[外链图片转存中…(img-oiIRs97p-1713380344913)]

[外链图片转存中…(img-ZBtaZohk-1713380344914)]

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值