当前位置: 首页 > news >正文

面试八股文之——JVM与并发编程/多线程

众所周知,程序员的技术能力考核大部分来源于面试和笔试,少数人可以靠着开源项目或者是证书、个人作品(书籍)等提升求职竞争力而直接获取offer。绝大多数程序员依旧是靠面试来获取offer。因此对待面试题,很多时候,应聘者需要做很多的准备,本文将对JVM与并发编程高频面试题目进行分享。

一、JVM篇

1.如何理解JVM,结构是如何设计的?

1.JVM是一套标准,所谓虚拟机,像真实的计算机一样,可以运行字节码指令。JVM的好处是可以屏蔽操作系统实现细节,使得Java代码一次编写,到处运行,做到跨平台
2.JVM厂商很多,诸如Hotspot、IBMJ9、JRockit等,主流的HotSpot VM进行分析。
3.HotSpot 主要分为3个核心部分:类加载子系统、运行时数据区、执行引擎。
首先将编译好的.class文件装载至类加载子系统,主要负责查找验证类文件、内存分配、对象赋值
加载完后,由运行时数据区负责数据的存储和数据交换。
运行数据区分为2部分:线程共享内存区、线程隔离内存区。
线程共享内存区分为:堆、方法区(也叫非堆,JDK1.8之后改为元空间)
线程隔离内存区:程序计数器、本地方法栈、虚拟机栈
堆:用于存储对象实例,方法区:用于存储运行时常量池、方法元数据、类元数据、字段
栈负责运行方法,本地方法栈运行Native方法,程序计数器负责保存每个线程执行方法的地址。
执行引擎主要包含JIT和GC。JIT负责将字节码翻译成机器码,可以通过JVM参数来设置解释执行还是编译执行。GC负责垃圾回收。
JNI技术可以查找并调用C++/C代码,DLL等。

2.什么是双亲委派机制?

1.得从类加载机制聊起,一个Java类要进行实例化,首先要进行编译成.class文件,然后由JVM加载至内存中,JVM加载后就得到一个完整的字节码对象,此时就可以进行实例化了。这个类加载过程中要使用到类加载器,JVM设计了3类加载器,启动类、扩展类、应用类加载器,它们负责对不同作用范围的Jar包和class文件进行加载,启动类加载器主要负责核心类库加载,如%{JDK_HOME}%/lib下的rt.jar、resource.jar等,扩展类加载器负责%{JDK_HOME}%/lib/ext/目录下Jar和class文件的加载,应用类加载器则负责classpath下的所有jar包和类文件。
类加载过程的顺序是,所有的class文件优先交个父类加载器进行加载,层层递归,只有当父类加载器无法完成加载,则传递给下一级类加载器完成。这么做有两个好处
1.避免核心类库被篡改,比如用户自定义一个java.lang.String类无法覆盖JDK中的String类。
2.避免类重复加载,如果父类加载器已经加载过,则无需重复加载。

3.JVM如何判定一个对象可以进行回收?

1.引用计数法
给每一个对象添加一个计数器,用来计算当前对象被引用的次数,当其值为0时,意味着可以被垃圾回收,
优点:简单高效。
缺点: 需要额外的存储空间,且无法解决发生循环引用的对象回收。因此主流JVM没有采用这个方法。
2.可达性分析法
首先找到一定不能被GC回收的对象作为GCRoot这个过程会触发STW,比如虚拟机栈引用的对象、本地方法栈引用的对象等;
然后以GCRoot为起点,依次向下进行搜索,寻找它的直接、间接引用对象。如果发现对象不可达,则认为对象可以被GC了。

4.JVM中GC算法的理解?

GC算法主要有3中,标记复制、标记清除、标记整理算法。
JVM新生代算法采用标记复制,新生代分为三部分,分别是Eden、From Survivor、To Survivor区,默认是8:1:1,新生代对象优先分配至Eden区,只有当Eden区满了,才会触发MinorGC,其中每次MinorGC过程中,会将Eden区和From Survivor的存活对象进行标记,然后直接复制到To Survivor区,From Survivor和To Survivor进行交换,如此循环往复运行。空间利用率低,适用于存活对象少,GC对象多的情况。因此适用于新生代。
标记清除:对存活对象进行标记,直接回收没有被标记的对象,容易产生内存碎片,随着系统运行时间的推移,内存碎片使得系统无法分配大量连续内存空间,导致频繁触发GC。适用于老年代。
标记整理:对存活对象进行标记,把所有存活对象整理至内存空间的一端,另一端空间直接释放。在标记清除上增加了一个移动动作。适用于老年代。

5.JVM分代年龄为啥是15?一定要达到15才能被分配至老年代吗?

一个对象分为三部分:
对象头、实例数据、对齐填充。
对象头分为:MarkWord、类型指针、数组长度(数组特有)。
其中MarkWord存储对象的分代年龄,hashCode,锁标志、线程ID等信息,
无论是32位操作系统还是64位操作系统,都是采用4位来存储GC分代年龄。因此最大值就只能是15
两种情况不用达到15,对象即可直接分配至老年代:
1.大对象【连续存储空间的大对象,如数组】,新生代没有足够的存储空间进行分配。
2.JVM引入了动态对象年龄判断机制,只需要满足这个动态年龄对象机制,即可分配至老年代。不管GC分代年龄是否达到15。

6.JVM 为什么使用元空间替代永久代?

1.JDK1.7版本中,永久代内存有限,虽然可以通过JVM参数来设置,但是JVM加载的class类总数、大小很难确定,所以很容易出现OOM问题。采用元空间来替代永久代,由于元空间使用的本地内存,上限很大,可以有效避免此类OOM的发生。
2.永久代对象的回收是通过FullGC来实现的,使用元空间替代后,可以在不暂停的情况下释放类数据,简化了FullGC过程,有效提升了GC性能
3.Oracle要合并HotSpot和JRockit代码,JRockit是没有永久代的。

二、并发编程

1.什么是AQS?

AQS,抽象队列同步器,是JUC包下多个组件的底层实现,如Lock、Semaphore、CountDownLatch等基于AQS实现。本质提供了两种锁机制,共享锁和排他锁。
排他锁,即多线程在竞争统一资源时,同一时刻只允许一个线程访问该共享资源。比如Lock中的ReentrantLock的实现就使用到了AQS的排他锁功能。
共享锁也称为读锁,就是同一时刻允许多个线程同时获得锁资源,比如Semaphore、CountDownLatch都用到了AQS的共享锁功能。

2.AQS底层原理?

AQS内部由两个核心部分构成:
1.volatile修饰的state变量,作为一个竞态条件。
2.基于双向链表结构维护的FIFO线程等待队列。
工作原理:多个线程通过对state变量进行修改来实现竞态条件,竞争失败的线程会加入FIFO队列并阻塞,抢占成功的线程在释放资源之后,后续的线程会按照FIFO顺序实现有序唤醒。
AQS里面提供了两种资源共享方式:独占资源和共享资源,独占资源可以用来实现排他锁,比如ReentrantLock。共享资源,可以用来实现读锁,CountDownLatch和Semaphore就是用了共享资源的方式,可实现同时唤醒多个线程。

3.AQS为什么要使用双向链表?

1.双向链表使用两个指针保存这个节点的前置节点和后置节点,能够在O(1)的情况下找到前置节点,插入和删除操作比单链表要快
2.没有竞争成功的线程是要放入FIFO线程等待队列中进行阻塞,这个前提必须保证当前线程的前置节点线程必须是正常状态,这么设计是为了避免链表中存在异常线程无法正常唤醒后续线程的问题。因此必须保留前置节点,如不保留,只能通过从head节点开始遍历获取前置节点线程状态,性能非常低。
3.为了减少线程阻塞和唤醒的开销,公平锁的设计下,只有head节点的下一个节点才有必要去竞争锁,后续线程竞争锁意义不大,否则容易造成羊群效应,大量线程在阻塞前尝试竞争锁,带来比较大的性能开销。
4.Lock接口有一个lockInterruptibly方法,表示处于锁阻塞的线程允许被中断,即处于阻塞的线程是允许外部线程通过interrupt方法触发唤醒并中断的,被中断的线程状态改为CANCELLED,这个状态的线程无需竞争锁,但是仍然存在于双向链表中,因此后续锁竞争中,需要将其删除,否则会阻塞正常线程无法正常唤醒。

4.CAS是什么?

1.全称CompareAndSwap,是Java Unsafe类里面的方法,即比较和替换,比较的是state内存地址偏移量对应的值和传入的预期值,如相等则采用新值进行替换操作。底层利用CPU原语操作,在多核CPU下,会增加一个Lock指令对缓存或者总线进行加锁,来保证多线程下对共享变量操作的原子性。

5.乐观锁、悲观锁?

1.乐观锁,即对共享数据的操作持乐观态度,认为别的线程不会同时修改该数据,因此不会直接上锁,而是在更新时在对共享数据进行判断。适用于读多写少的场景。典型的就是CAS、AtomicInteger等
悲观锁,即对共享数据的操作持悲观态度,在操作数据过程中总认为别的线程会同时修改数据,所以每次操作时直接加锁,来实现线程安全。适用于读少写多的场景。典型就是synchronized/ReentrantLock。

6.什么时候会发生死锁,如何避免死锁?

1.多个线程在运行过程中,因争夺同一个共享资源而发生相互等待的现象。在没有外部干预的情况下,线程会一直陷入阻塞状态,即发生死锁。
产生死锁四个条件:
1.互斥,共享资源同一时刻只能被一个线程持有
2.请求和保持,线程在没有获取待获取的共享资源时,不会释放已经获取的共享资源。
3.不可抢占,其他线程不能强行抢占已经占有线程的共享资源。
4.循环等待,线程A等待线程B持有的资源,线程B等待线程A持有的资源。
互斥是不能被破坏的,只能破坏其他三个条件,
请求和保持条件下:首次执行时一次性获取所有资源
不可抢占条件:主动释放已经持有的资源
循环等待条件:按序申请资源,给资源编号,所有线程按照线性化的编号依次获取共享资源。

7.synchronized和Lock的区别?

1.synchronized是Java关键字,Lock则是接口,都是用来保证现场安全的方式。
2.synchronized只能实现非公平锁,采用悲观锁,阻塞获取锁,被动释放锁,不可中断,可重入
3.Lock采用乐观锁机制,可实现公平、非公平锁,锁可中断、主动释放锁、非阻塞竞争锁,可重入

8.可重入锁的作用什么?

1.一个线程持有互斥锁资源,在释放锁资源之前再去竞争获取该锁,无需等待,只需要记录重入次数即可。
大部分锁都是可重入的,synchronized和ReentrantLock等,不可重入的锁有:JDK8的StampedLock
锁的可重入性是避免死锁的发生,避免已经持有锁资源的线程发生等待自己释放锁的情况。

9.ReentrantLock的实现原理有什么?

1.可重入
2.支持公平锁和非公平锁
3.提供阻塞竞争锁lock方法和非阻塞竞争锁tryLock方法
锁的竞争是通过CAS来实现,没有获取锁的线程,加入到AQS中,锁释放后,将从队列头部唤醒下一个等待的线程。
公平锁:严格按照请求的顺序分配锁资源
非公平锁:允许插队来抢占锁资源
公平锁实现过程:线程在竞争获取锁过程中,先判断AQS是否存在等待线程,有则直接加入队列的尾部进行阻塞等待。
非公平锁实现过程:不管AQS是否存在等待线程,直接参与锁资源竞争,抢夺失败则放入AQS队列中进行等待。
非公平锁性能高,公平锁能满足业务场景下先到先得的需求。公平锁性能下降的原因:锁竞争过程中,由于采用公平策略,后续竞争资源的线程则加入AQS进行等待,AQS将FIFO等待队列的线程唤醒,中间涉及内核态的转换,产生较大的性能损耗。

10.数据库行锁、间隙锁、临键锁的理解?

1.行锁、间隙锁、临键锁都是Mysql里Innodb引擎下解决事务隔离性一序列排他锁,行锁也叫记录锁。
1.间隙锁,即锁住索引之间的间隙。左闭右开原则。这个在读已提交隔离下不存在。
2.行锁,即锁住某一行数据记录。这个是对主键加锁。
3.临键锁,等于行锁+间隙锁。
4.普通索引(非唯一索引)查询中自动增加临键锁,即锁住上一个索引值到下一个索引值的范围记录,左闭右开原则【Mysql8.0以上版本】,如果命中记录则记录全部锁住。如果没有命中记录,退化为间隙锁, 锁的是索引。
5.主键索引/唯一索引精准查询,命中则使用行锁,没有命中则使用间隙锁(左右开区间),如使用范围查询,部分命中,没命中的记录会自动使用间隙锁。

11.阻塞队列被异步消费,如何保持顺序?
`

1.阻塞队列本身符合FIFO特性的队列,因此队列中的元素在被消费时符合先进先出的原则。
2.阻塞队列,使用condition条件等待来维护两个等待队列,一个是队列为空时,存储被阻塞的消费者,一个是队列满时,存储被阻塞的生产者。将线程存储至等待队列的过程,都符合FIFO特性。
当阻塞队列存在多个任务时候,启用多个消费者线程消费任务,有序性的保证来自于消费者线程每次消费任务都必须获取排他锁。
当阻塞队列为空,多个消费者线程将会进入阻塞,存入等待队列中,存储也符合FIFO特性,因此当阻塞队列中有任务时,被阻塞的消费者线程也将按照FIFO的顺序进行唤醒。保证消费的顺序。

12.ArrayBlockingQueue的实现原理?

1.基于数组实现的阻塞队列,即队列元素存储于一个数组结构里,由于数组有长度限制,为了达到循环生产和消费的目的,ArrayBlockingQueue使用了循环数组。
2. 线程的阻塞和唤醒用到了JUC包下的ReentrantLock和Condition。Condition相当于wait/notify在JUC包里的实现。

13.Thread和Runnable的区别?

1.Thread作为实现Runnable的类,是Runnable的一个具体实现。Runnable作为一个线程的顶级接口,从面向对象/面向接口编程的角度来说,Runnable相当于一个任务,而Thread才是实际处理任务的线程。因此往线程池提交一个任务,传递的是Runnable,而非Thread。

14.什么是守护线程?

1.守护线程,是一种专门为用户线程提供服务的线程,生命周期依赖于用户线程,即用户线程结束了,那么守护线程即自动退出,比如JVM中的GC回收线程。

  1. Blocked和Waiting两种线程状态的区别?

1.两种都是线程阻塞状态,Blocked是竞争获取锁失败,被动触发陷入阻塞状态,比如synchronized关键字多线程竞争下,竞争失败的线程则自动进入Blocked状态,Waiting则是主动触发进入阻塞状态,比如使用Object.wait(),LockSupport.park()方法等。
Blocked的唤醒是自动的,Waiting状态的线程需使用特定的方法唤醒,比如Object.notify(),LockSupport.unpark()

16.为什么启动线程不能直接调用run方法,start方法为啥不能调用两次?

1.start方法中包含了创建新线程的特殊逻辑,如果直接调用run方法,就是一个普通方法调用,并不会开启新线程。
2.调用第二次会抛出illegalThreadStateException,多次调用start方法会被认为编程错误。

17.谈谈对线程池的理解?

1.线程池本质是一种池化技术,池化技术利用资源复用的思想,比较常见的池化技术包括连接池、内存池、对象池。
核心目的有两个:
1.避免线程频繁创建和销毁带来的性能开销,因为创建线程会涉及CPU上下文切换、内存分配等工作。
2.统一对线程资源进行管理,避免线程无休止的创建带来资源利用率过高,影响整个系统性能。

18.线程池是如何回收线程的?

核心线程池初始化的两种方式:
1.不断往线程池加入任务,被动构建核心线程。
2.调用prestartAllCoreThreads方法,直接创建核心线程数。
回收线程池,通过阻塞队列的poll方法来完成,poll方法中有个超时时间和超时时间单位参数,当超过这个时间没有获取到任务时候,poll方法返回null,从而终止线程,完成线程回收。通常不会对核心线程进行回收。如果希望核心线程也被回收,则可以设置allowCoreThreadTimeOut为true。【不建议这么做】因为线程池本身就是利用线程复用来提升任务处理效率,且即使没有任务,核心线程也会处于阻塞中,并没有占用CPU资源

19.线程池如何实现复用的?

1.线程池采用生产者-消费者模型来实现线程复用,使用一个中间容器,来解耦生产者和消费者的任务处理过程。生产者不断生产任务并保存至容器中,消费者不断从容器中消费任务。
这个容器就是阻塞队列,当队列为空,消费者线程将陷入阻塞,线程池将回收多余的空闲线程,当队列任务到来时,线程池将唤醒阻塞线程继续消费任务。

20.线程池如何知道一个线程的任务已经执行完成?

1.线程池内部有一个钩子方法afterExecute(),可以覆写这个方法来获取工作线程的执行结果。
2.线程池外部可以通过submit()方法提交任务,提供了一个Future的返回值,通过future.get()获取任务的执行结果,只有当线程完成才会返回结果。
3.使用CountDownLatch计数器,在线程完成时候,调用countDown方法,唤醒await方法阻塞的线程。

21.当任务数大于线程池的核心线程数,如何让任务不进入队列?

1.将线程池的队列换成SynchronousQueue,这个队列不能存储任何元素,每生产一个任务,必须需要一个消费者线程来处理。它会直接启动最大线程来处理所有任务。

22.什么是伪共享,如何避免伪共享?

1.为了提升CPU利用率,平衡CPU和内存的速度差异,设计了三级缓存,其中CPU向内存发起IO操作,一次性会读取64个字节作为一个缓存行,缓存至CPU高速缓存中。
2.Java中一个Long类型占用8个字节,也是就说一个缓存行可以存储8个Long类型,基于空间局部性原理,如果一个存储器位置被引用,那么它附近的位置也会被引用。正式因为这种缓存行的引入,如果多个线程修改同一个缓存行里面的多个独立变量,由于缓存一致性协议,就会让缓存失效,影响彼此的性能,这就是伪共享。
解决方案:
1.使用对齐填充,如果读取的目标数据小于64个字节,增加一些无意义的成员变量来填充。
2.JDK8提供了@Contented注解,被@Contented注解声明的类或者是字段,会被加载到独立的缓存行。

23.谈谈对CompletableFuture的理解?

1.JDK8引入的一个基于事件驱动的异步回调类。当使用异步线程执行任务,我们希望在任务结束后触发一个后续的动作。提供了五种方式,来将异步任务组成一个具有先后关系的处理链。然后事件驱动任务链执行。
(1) thenCombine 将两个任务组合在一起,当两个任务并行完成后触发回调。
(2) thenCompose 串行组合,上一个任务结束,自动触发下个任务的回调,作为中间链路处理。这个Function函数传入对象必须是实现了CompletionStage<U>接口的类型
(3)thenAccept 串行组合,上一个任务结束,自动触发下个任务的回调,但是没有返回值,用于最后对结果的处理
(4) thenApply 串行组合,上一个任务结束,自动触发下个任务的回调,有返回值,这个Function函数可以传入普通对象
(5) thenRun 串行组合,执行实现了Runnable接口的任务

24.ThreadLocal原理的理解?

  1. ThreadLocal是一种线程隔离机制,提供了多线程场景下对共享变量操作的安全性。
    2.传统对于共享变量的安全访问控制都是基于锁机制来实现,由于加锁会带来性能下降,因此ThreadLocal采用空间换时间的策略,每个线程都存储共享变量的副本,既解决了线程安全,又避免加锁带来的性能开销。
    3.ThreadLocal中采用ThreadLocalMap来存储共享变量的副本,所有对共享变量的操作,都来自于对ThreadLocalMap的变更。

25.CountDownLatch和CycliBarrier有什么区别?

1.CountDownLatch计数器只能使用一次,而CycliBarrier可以通过reset方法,计数器重复使用
2.CycliBarrier用于处理更为复杂的业务场景,比如聚合计算、计算发生错误计算器重置重新执行
3.CountDownLatch会阻塞主线程,而CycliBarrier不会,只会阻塞子线程

26.谈谈对Happens-Before的理解?

1.Happens-Before是一种可见性模型,多线程环境下,由于指令重排会发生数据可见性问题,为此JMM通过指定Happens-Before关系像开发人员提供跨越线程内存可见性的保证。如果一个操作的执行结果对另一个操作可见,那么这个两个操作之间必然存在Happens-Before关系。
常见的Happens-Before规则有:
1.程序顺序规则,
2.传递性规则,
3.volatile变量规则
4.监视器锁规则
5.线程启动规则
6.线程join方法规则

27.如何安全中断一个正在运行的线程?

1.JavaAPI提供了一个stop方法可以用来中断线程,但是不是安全地。但是JavaAPI中提供了一个interrupt方法,我们可以在线程中,根据当前线程的中断状态来编写结束代码,外部可以通过调用interrupt方法来中断正在运行的线程。

28.SimpleDateFormat是线程安全的吗?

1.不是,SimpleDateFormat内部有一个Calendar对象引用,多线程情况下,同时操作这个对象,就会出现数据脏读的情况。
因此为了实现线程安全,可以采用如下措施:
1.SimpleDateFormat作为非全局使用的局部变量,牺牲空间保证线程安全
2.将其放入ThreadLocal中。
3.操作加锁,性能下降
4.使用替代API,LocalDateTimer/DateTimeFormmatter等。

29.并发场景下,ThreadLocal会造成内存泄漏吗?

1.ThreadLocal对采用ThreadLocalMap来存储共享变量副本,其中ThreadLocalMap采用Entry来保存共享变量,其中Entry对象对ThreadLocal对象使用软引用但是Value采用的强引用。因此当外部ThreadLocal对象没有强引用,则会被GC,导致Entry的Key为空,但是由于Value采用强引用,Value值无法被GC,此时Value就成为死亡且不能回收的对象,发生内存泄漏。
解决方案:
1.每次使用完ThreadLocal,手动调用remove方法清除数据。
2.ThreadLocal变量尽可能定义为static final类型,避免频繁创建ThreadLocal对象,保持对ThreadLocal对象强引用。

http://www.dtcms.com/a/356444.html

相关文章:

  • Azure、RDP、NTLM 均现高危漏洞,微软发布2025年8月安全更新
  • 【物联网】什么是 DHT11(数字温湿度传感器)?
  • C++ 编译和运行 LibCurl 动态库和静态库
  • SyncBack 备份同步软件: 使用 FTPS、SFTP 和 HTTPS 安全加密传输文件
  • 【2025 完美解决】Failed connect to github.com:443; Connection timed out
  • 网络编程(2)—多客户端交互
  • 跨境物流新引擎:亚马逊AGL空运服务赋能卖家全链路升级
  • Pycharm 登录 Github 失败
  • idea2023.3遇到了Lombok失效问题,注释optional和annotationProcessorPaths即可恢复正常
  • “FAQ + AI”智能助手全栈实现方案
  • 极飞科技AI智慧农业实践:3000亩棉田2人管理+产量提15%,精准灌溉与老农操作门槛引讨论
  • autojs RSA加密(使用public.pem、private.pem)
  • 【拍摄学习记录】03-曝光
  • Lora与QLora
  • 创维E910V10C_晶晨S905L2和S905L3芯片_线刷固件包
  • SpringMVC相关梳理
  • 第三方软件测试:【深度解析SQL注入攻击原理和防御原理】
  • [Mysql数据库] 知识点总结6
  • 《Linux 网络编程六:数据存储与SQLite应用指南》
  • LabVIEW转速仪校准系统
  • uniapp跨平台开发---uni.request返回int数字过长精度丢失
  • uni-app + Vue3 开发H5 页面播放海康ws(Websocket协议)的视频流
  • 学习:uniapp全栈微信小程序vue3后台(6)
  • Uniapp + UView + FastAdmin 性格测试小程序方案
  • 2025最新uni-app横屏适配方案:微信小程序全平台兼容实战
  • 项目一系列-第9章 集成AI千帆大模型
  • 实现自己的AI视频监控系统-第二章-AI分析模块5(重点)
  • js AbortController 实现中断接口请求
  • 【MFC教程】C++基础:01 小黑框跑起来
  • 【MFC应用创建后核心文件详解】项目名.cpp、项目名.h、项目名Dlg.cpp 和 项目名Dlg.h 的区别与作用