Java面试题及答案,2022年最新版,针对高频面试点

本项目是本人参加BAT等其他公司电话、现场面试之后总结出来的针对Java面试的知识点或真题,每个点或题目都是在面试中被问过的。

一、Java基础 

1、JVM原理

①、Java内存区域的分配

JVM虚拟机内存模型实现规范:

按线程是否共享分为以下区域:

所有线程共享的数据区:

  1. 方法区(JVM规范中的一部分,不是实际的实现): 存储每一个类的结构信息(运行时常量池、静态变量、方法数据、构造函数和普通方法的字节码、JIT编译后的代码),没有要求使用垃圾回收因为回收效率太低。(运行时常量池:存放编译器生成的各种字面量和符号引用,在类加载后放到运行时常量池中)
  2. 堆区: 最大的一块区域,是大部分类实例、对象、数组分配内存的区域,没有限制只能将对象分配在堆,所以出现逃逸分析的技术

每个线程都会有一块私有的数据区:

  1. 虚拟机栈: 虚拟机栈与线程同时创建,每个方法在执行时在其中创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法返回地址。正常调用完成后恢复调用者的局部变量表、操作数栈、递增程序计数器来跳过刚才执行的指令,或抛出异常不将返回值返回给调用者
  2. 本地方法栈: 功能与虚拟机栈相同,为native方法服务
  3. pc寄存器: 任意时刻线程只会执行一个方法的代码,如果不是native的,就存放当前正在执行的字节码指令的地址,如果是native,则是undefined

以HotSpot虚拟机实现为例,Java8中内存区域如下:

与规范中的区别:

  1. 直接内存:非Java标准,是JVM以外的本地内存,在Java4出现的NIO中,为了防止Java堆和Native堆之间往复的数据复制带来的性能损耗,此后NIO可以使用Native的方式直接在Native堆分配内存。JDK中有一种基于通道(Channel)和缓冲区(Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。
  2. 元数据区(方法区的实现):Java7以及之前是使用的永久代来实现方法区,大小是在启动时固定的。Java8中用元空间替代了永久代,元空间并不在虚拟机中,而是使用本地内存,并且大小可以是自动增长的,这样减少了OOM的可能性。元空间存储JIT即时编译后的native代码,可能还存在短指针数据区CCS
  3. 堆区: Java7之后运行时常量池从方法区移到这里,为Java8移除永久带的做好准备
  4. 插播一条广告:需要开通正版IDEA的可以联系我,56元一年,正版授权激活,官网可查有效期,有需要的加我微信:poxiaozhiai6,备注:911。

②、Java对象不都是分配在堆上

逃逸分析:

逃逸是指在某个方法之内创建的对象除了在方法体之内被引用之外,还在方法体之外被其它变量引用到;这样带来的后果是在该方法执行完毕之后,该方法中创建的对象将无法被GC回收。由于其被其它变量引用,由于无法回收,即称为逃逸。

逃逸分析技术可以分析出某个对象是否永远只在某个方法、线程的范围内,并没有“逃逸”出这个范围,逃逸分析的一个结果就是对于某些未逃逸对象可以直接在栈上分配提高对象分配回收效率,对象占用的空间会随栈帧的出栈而销毁。

③、类加载机制

加载过程:

  1. 加载(获取来自任意来源的字节流并转换成运行时数据结构,生成Class对象)
  2. 验证(验证字节流信息符合当前虚拟机的要求,防止被篡改过的字节码危害JVM安全)
  3. 准备(为类变量分配内存并设置初始值)
  4. 解析(将常量池的符号引用替换为直接引用,符号引用是用一组符号来描述所引用的目标,直接引用是指向目标的指针)
  5. 初始化(执行类构造器、类变量赋值、静态语句块)

类加载器:

启动类加载器:用C++语言实现,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库,无法被Java程序直接引用
扩展类加载器:用Java语言实现,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用
系统类加载器:用Java语言实现,它负责加载系统类路径ClassPath指定路径下的类库,开发者可以直接使用

双亲委派:

定义:如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

优点:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次防止恶意覆盖Java核心API。

三次大型破坏双亲委派模式的事件:

  1. 在双亲委派模式出来之前,用户继承ClassLoader就是为了重写loadClass方法,但双亲委派模式需要这个方法,所以1.2之后添加了findClass供以后的用户重写
  2. 如果基础类要调回用户的代码,如JNDI/JDBC需要调用ClassPath下的自己的代码来进行资源管理,Java团队添加了一个线程上下文加载器,如果该加载器没有被设置过,那么就默认是应用程序类加载器
  3. 为了实现代码热替换,OSGi是为了实现自己的类加载逻辑,用平级查找的逻辑替换掉了向下传递的逻辑。但其实可以不破坏双亲委派逻辑而是自定义类加载器来达到代码热替换。比如这篇文章
  4. 插播一条广告:需要开通正版IDEA的可以联系我,56元一年,正版授权激活,官网可查有效期,有需要的加我微信:poxiaozhiai6,备注:911。

④、内存分配(堆上的内存分配)

新生代

进入条件

优先选择在新生代的Eden区被分配。

老年代

进入条件

  1. 大对象,-XX:PretenureSizeThreshold 大于这个参数的对象直接在老年代分配,来避免新生代GC以及分配担保机制和Eden与Survivor之间的复制
  2. 经过第一次Minor GC仍然存在,能被Survivor容纳,就会被移动到Survivor中,此时年龄为1,当年龄大于预设值就进入老年代
  3. 如果Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象进入老年代
  4. 如果Survivor空间无法容纳新生代中Minor GC之后还存活的对象

⑤、GC回收机制

回收对象

不可达对象:通过一系列的GC Roots的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时则此对象是不可用的。
GC Roots包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(Native方法)引用的对象。

彻底死亡条件:
条件1:通过GC Roots作为起点的向下搜索形成引用链,没有搜到该对象,这是第一次标记。
条件2:在finalize方法中没有逃脱回收(将自身被其他对象引用),这是第一次标记的清理。

如何回收

新生代因为每次GC都有大批对象死去,只需要付出少量存活对象的复制成本且无碎片所以使用“复制算法”
老年代因为存活率高、没有分配担保空间,所以使用“标记-清理”或者“标记-整理”算法

复制算法:将可用内存按容量划分为Eden、from survivor、to survivor,分配的时候使用Eden和一个survivor,Minor GC后将存活的对象复制到另一个survivor,然后将原来已使用的内存一次清理掉。这样没有内存碎片。
标记-清除:首先标记出所有需要回收的对象,标记完成后统一回收被标记的对象。会产生大量碎片,导致无法分配大对象从而导致频繁GC。
标记-整理:首先标记出所有需要回收的对象,让所有存活的对象向一端移动。

Minor GC条件

当Eden区空间不足以继续分配对象,发起Minor GC。

Full GC条件

  1. 调用System.gc时,系统建议执行Full GC,但是不必然执行
  2. 老年代空间不足(通过Minor GC后进入老年代的大小大于老年代的可用内存)
  3. 方法区空间不足

2、垃圾收集器

①、串行收集器

串行收集器Serial是最古老的收集器,只使用一个线程去回收,可能会产生较长的停顿

新生代使用Serial收集器复制算法、老年代使用Serial Old标记-整理算法

参数:-XX:+UseSerialGC,默认开启-XX:+UseSerialOldGC

②、并行收集器

并行收集器Parallel关注可控的吞吐量,能精确地控制吞吐量与最大停顿时间是该收集器最大的特点,也是1.8的Server模式的默认收集器,使用多线程收集。ParNew垃圾收集器是Serial收集器的多线程版本。

新生代复制算法、老年代标记-整理算法

参数:-XX:+UseParallelGC,默认开启-XX:+UseParallelOldGC

③、并发收集器

并发收集器CMS是以最短停顿时间为目标的收集器。G1关注能在大内存的前提下精确控制停顿时间且垃圾回收效率高。

CMS针对老年代,有初始标记、并发标记、重新标记、并发清除四个过程,标记阶段会Stop The World,使用标记-清除算法,所以会产生内存碎片。

参数:-XX:+UseConcMarkSweepGC,默认开启-XX:+UseParNewGC

G1将堆划分为多个大小固定的独立区域,根据每次允许的收集时间优先回收垃圾最多的区域,使用标记-整理算法,是1.9的Server模式的默认收集器

参数:-XX:+UseG1GC

3、Stop The World

Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互

STW总会发生,不管是新生代还是老年代,比如CMS在初始标记和重复标记阶段会停顿,G1在初始标记阶段也会停顿,所以并不是选择了一款停顿时间低的垃圾收集器就可以避免STW的,我们只能尽量去减少STW的时间。

那么为什么一定要STW?因为在定位堆中的对象时JVM会记录下对所有对象的引用,如果在定位对象过程中,有新的对象被分配或者刚记录下的对象突然变得无法访问,就会导致一些问题,比如部分对象无法被回收,更严重的是如果GC期间分配的一个GC Root对象引用了准备被回收的对象,那么该对象就会被错误地回收。

4、Java内存模型

定义:JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性

实现:volatile、synchronized、final、concurrent包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字

主内存:所有变量都保存在主内存中
工作内存:每个线程的独立内存,保存了该线程使用到的变量的主内存副本拷贝,线程对变量的操作必须在工作内存中进行

每个线程都有自己的本地内存共享副本,如果A线程要更新主内存还要让B线程获取更新后的变量,那么需要:

  1. 将本地内存A中更新共享变量
  2. 将更新的共享变量刷新到主内存中
  3. 线程B从主内存更新最新的共享变量

5、happens-before

我们无法就所有场景来规定某个线程修改的变量何时对其他线程可见,但是我们可以指定某些规则,这规则就是happens-before。特别关注在多线程之间的内存可见性。

它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们解决在并发环境下两操作之间是否可能存在冲突的所有问题。

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
  9. 插播一条广告:需要开通正版IDEA的可以联系我,56元一年,正版授权激活,官网可查有效期,有需要的加我微信:poxiaozhiai6,备注:911。

6、JVM调优

前提:在进行GC优化之前,需要确认项目的架构和代码等已经没有优化空间

目的:优化JVM垃圾收集性能从而增大吞吐量或减少停顿时间,让应用在某个业务场景上发挥最大的价值。吞吐量是指应用程序线程用时占程序总用时的比例。暂停时间是应用程序线程让与GC线程执行而完全暂停的时间段

对于交互性web应用来说,一般都是减少停顿时间,所以有以下方法:

  1. 如果应用存在大量的短期对象,应该选择较大的年轻代;如果存在相对较多的持久对象,老年代应该适当增大
  2. 让大对象进入年老代。可以使用参数-XX:PetenureSizeThreshold 设置大对象直接进入年老代的阈值。当对象的大小超过这个值时,将直接在年老代分配
  3. 设置对象进入年老代的年龄。如果对象每经过一次 GC 依然存活,则年龄再加 1。当对象年龄达到阈值时,就移入年老代,成为老年对象
  4. 使用关注系统停顿的 CMS 回收器

7、集合

①、ArrayList与LinkedList区别

ArrayList LinkedList
数组 双向链表
增删的时候在扩容的时候慢,通过索引查询快,通过对象查索引慢 增删快,通过索引查询慢,通过对象查索引慢
当数组无法容纳下此次添加的元素时进行扩容
扩容之后容量为原来的1.5倍

②、HashMap

  1. JDK 1.8 以前 HashMap 的实现是 数组+单链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题
  2. 为什么线程不安全?多线程PUT操作时可能会覆盖刚PUT进去的值;扩容操作会让链表形成环形数据结构,形成死循环
  3. 容量的默认大小是 16,负载因子是 0.75,当 HashMap 的 size > 16*0.75 时就会发生扩容(容量和负载因子都可以自由调整)。
  4. 为什么容量是2的倍数?在根据hashcode查找数组中元素时,取模性能远远低于与性能,且和2^n-1进行与操作能保证各种不同的hashcode对应的元素也能均匀分布在数组中

③、HashMap rehash过程:

  1. 空间不够用了,所以需要分配一个大一点的空间,然后保存在里面的内容需要重新计算 hash
  2. 如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小
  3. 在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了

④、ConcurrentHashMap原理

ConcurrentHashMap演进从Java7到Java8 | 技术世界 | java,concurrenthashmap,java 8,CAS,多线程,并发,技术世界,郭俊 Jason,大数据架构

HashTable 在每次同步执行时都要锁住整个结构。ConcurrentHashMap 锁的方式是稍微细粒度的。 ConcurrentHashMap 将 hash 表分为 16 个桶(默认值)
最大并发个数就是Segment的个数,默认值是16,可以通过构造函数改变一经创建不可更改,这个值就是并发的粒度,每一个segment下面管理一个table数组,加锁的时候其实锁住的是整个segment

Java7

ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。HashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。

Java8

  1. 为进一步提高并发性,放弃了分段锁,锁的级别控制在了更细粒度的table元素级别,也就是说只需要锁住这个链表的head节点,并不会影响其他的table元素的读写,好处在于并发的粒度更细,影响更小,从而并发效率更好
  2. 使用CAS + synchronized 来保证实现put操作:如果Key对应的数组元素为null,则通过CAS操作将其设置为当前值。如果Key对应的数组元素(也即链表表头或者树的根元素)不为null,则对该元素使用synchronized关键字申请锁,然后进行操作。如果该put操作使得当前链表长度超过一定阈值,则将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)),插入操作完成之后如果所有元素的数量大于当前容量(默认16)*负载因子(默认0.75)就进行扩容。

8、多线程

①、线程的生命周期

新建 -- 就绪 -- 运行 -- 阻塞 -- 就绪 -- 运行 -- 死亡

②、问:你怎么理解多线程的

  1. 定义:多线程是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
  2. 存在的原因:因为单线程处理能力低。打个比方,一个人去搬砖与几个人去搬砖,一个人只能同时搬一车,但是几个人可以同时一起搬多个车。
  3. 实现:在Java里如何实现线程,Thread、Runnable、Callable。
  4. 问题:线程可以获得更大的吞吐量,但是开销很大,线程栈空间的大小、切换线程需要的时间,所以用到线程池进行重复利用,当线程使用完毕之后就放回线程池,避免创建与销毁的开销。

③、线程间通信的方式

  1. 等待通知机制 wait()、notify()、join()、interrupted()
  2. 并发工具synchronized、lock、CountDownLatch、CyclicBarrier、Semaphore

④、锁

锁是什么

锁是在不同线程竞争资源的情况下来分配不同线程执行方式的同步控制工具,只有线程获取到锁之后才能访问同步代码,否则等待其他线程使用结束后释放锁

synchronized

通常和wait,notify,notifyAll一块使用。
wait:释放占有的对象锁,释放CPU,进入等待队列只能通过notify/all继续该线程。
sleep:则是释放CPU,但是不释放占有的对象锁,可以在sleep结束后自动继续该线程。
notify:唤醒等待队列中的一个线程,使其获得锁进行访问。
notifyAll:唤醒等待队列中等待该对象锁的全部线程,让其竞争去获得锁。

lock

拥有synchronize相同的语义,但是添加一些其他特性,如中断锁等候和定时锁等候,所以可以使用lock代替synchronize,但必须手动加锁释放锁

两者的区别

  • 性能:资源竞争激烈的情况下,lock性能会比synchronized好;如果竞争资源不激烈,两者的性能是差不多的
  • 用法:synchronized可以用在代码块上,方法上。lock通过代码实现,有更精确的线程语义,但需要手动释放,还提供了多样化的同步,比如公平锁、有时间限制的同步、可以被中断的同步
  • 原理:synchronized在JVM级别实现,会在生成的字节码中加上monitorenter和monitorexit,任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。monitor是JVM的一个同步工具,synchronized还通过内存指令屏障来保证共享变量的可见性。lock使用AQS在代码级别实现,通过Unsafe.park调用操作系统内核进行阻塞
  • 功能:比如ReentrantLock功能更强大
    1. ReentrantLock可以指定是公平锁还是非公平锁,而synchronized只能是非公平锁,所谓的公平锁就是先等待的线程先获得锁
    2. ReentrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程
    3. ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制

我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化。ReentrantLock和Atomic如果用的不好,不仅不能提高性能,还可能带来灾难。

锁的种类

  • 公平锁/非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁
ReentrantLock通过构造函数指定该锁是否是公平锁,默认是非公平锁。Synchronized是一种非公平锁

  • 可重入锁

指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
ReentrantLock, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁
Synchronized,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁

  • 独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有
ReentrantLock是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的
Synchronized是独享锁

  • 乐观锁/悲观锁

悲观锁在Java中的使用,就是各种锁
乐观锁在Java中的使用,是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新

  • 偏向锁/轻量级锁/重量级锁

针对Synchronized的锁状态:
偏向锁是为了减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。指一段同步代码一直被一个线程所访问,在无竞争情况下把整个同步都消除掉
轻量级锁是为了减少无实际竞争情况下,使用重量级锁产生的性能消耗。指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过CAS自旋的形式尝试获取锁,不会阻塞
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁

  • 自旋锁/自适应自旋锁

指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU,默认自旋次数为10
自适应自旋锁的自旋次数不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定,是虚拟机对锁状况的一个预测

volatile

功能:

  1. 主内存和工作内存,直接与主内存产生交互,进行读写操作,保证可见性;
  2. 禁止 JVM 进行的指令重排序。

⑤、ThreadLocal

使用ThreadLocal<UserInfo> userInfo = new ThreadLocal<UserInfo>()的方式,让每个线程内部都会维护一个ThreadLocalMap,里边包含若干了 Entry(K-V 键值对),每次存取都会先获取到当前线程ID,然后得到该线程对象中的Map,然后与Map交互。

⑥、线程池

起源

new Thread弊端:

  • 每次启动线程都需要new Thread新建对象与线程,性能差。线程池能重用存在的线程,减少对象创建、回收的开销。
  • 线程缺乏统一管理,可以无限制的新建线程,导致OOM。线程池可以控制可以创建、执行的最大并发线程数。
  • 缺少工程实践的一些高级的功能如定期执行、线程中断。线程池提供定期执行、并发数控制功能

线程池时核心参数

  • corePoolSize:核心线程数量,线程池中应该常驻的线程数量
  • maximumPoolSize:线程池允许的最大线程数,非核心线程在超时之后会被清除
  • workQueue:阻塞队列,存储等待执行的任务
  • keepAliveTime:线程没有任务执行时可以保持的时间
  • unit:时间单位
  • threadFactory:线程工厂,来创建线程
  • rejectHandler:当拒绝任务提交时的策略(抛异常、用调用者所在的线程执行任务、丢弃队列中第一个任务执行当前任务、直接丢弃任务)

创建线程的逻辑

以下任务提交逻辑来自ThreadPoolExecutor.execute方法:

  1. 如果运行的线程数 < corePoolSize,直接创建新线程,即使有其他线程是空闲的
  2. 如果运行的线程数 >= corePoolSize
    2.1 如果插入队列成功,则完成本次任务提交,但不创建新线程
    2.2 如果插入队列失败,说明队列满了
    2.2.1 如果当前线程数 < maximumPoolSize,创建新的线程放到线程池中
    2.2.2 如果当前线程数 >= maximumPoolSize,会执行指定的拒绝策略

阻塞队列的策略

  • 直接提交。SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take。将任务直接提交给线程而不保持它们。
  • 无界队列。当使用无限的 maximumPoolSizes 时,将导致在所有corePoolSize线程都忙时新任务在队列中等待。
  • 有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。

⑦、并发包工具类

CountDownLatch

计数器闭锁是一个能阻塞主线程,让其他线程满足特定条件下主线程再继续执行的线程同步工具。

 图中,A为主线程,A首先设置计数器的数到AQS的state中,当调用await方法之后,A线程阻塞,随后每次其他线程调用countDown的时候,将state减1,直到计数器为0的时候,A线程继续执行。

使用场景:
并行计算:把任务分配给不同线程之后需要等待所有线程计算完成之后主线程才能汇总得到最终结果
模拟并发:可以作为并发次数的统计变量,当任意多个线程执行完成并发任务之后统计一次即可

Semaphore

信号量是一个能阻塞线程且能控制统一时间请求的并发量的工具。比如能保证同时执行的线程最多200个,模拟出稳定的并发量。

public class CountDownLatchTest {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Semaphore semaphore = new Semaphore(3); //配置只能发布3个运行许可证
        for (int i = 0; i < 100; i++) {
            int finalI = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire(3); //获取3个运行许可,如果获取不到会一直等待,使用tryAcquire则不会等待
                    Thread.sleep(1000);
                    System.out.println(finalI);
                    semaphore.release(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}

由于同时获取3个许可,所以即使开启了100个线程,但是每秒只能执行一个任务

使用场景:
数据库连接并发数,如果超过并发数,等待(acqiure)或者抛出异常(tryAcquire)

CyclicBarrier

可以让一组线程相互等待,当每个线程都准备好之后,所有线程才继续执行的工具类

与CountDownLatch类似,都是通过计数器实现的,当某个线程调用await之后,计数器减1,当计数器大于0时将等待的线程包装成AQS的Node放入等待队列中,当计数器为0时将等待队列中的Node拿出来执行。

与CountDownLatch的区别:

  1. CountDownLatch是一个线程等其他线程,CyclicBarrier是多个线程相互等待
  2. CyclicBarrier的计数器能重复使用,调用多次

使用场景: 有四个游戏玩家玩游戏,游戏有三个关卡,每个关卡必须要所有玩家都到达后才能允许通过。其实这个场景里的玩家中如果有玩家A先到了关卡1,他必须等到其他所有玩家都到达关卡1时才能通过,也就是说线程之间需要相互等待。

编程题

交替打印奇偶数

public class PrintOddAndEvenShu {
    private int value = 0;

    private synchronized void printOdd() {
        while (value <= 100) {
            if (value % 2 == 1) {
                System.out.println(Thread.currentThread() + ": -" + value++);
                this.notify();
            } else {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }

    }

    private synchronized void printEven() {
        while (value <= 100) {
            if (value % 2 == 0) {
                System.out.println(Thread.currentThread() + ": --" + value++);
                this.notify();
            } else {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        PrintOddAndEvenShu print = new PrintOddAndEvenShu();
        Thread t1 = new Thread(print::printOdd);
        Thread t2 = new Thread(print::printEven);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

多线程交替打印alibaba: 记一道阿里多线程笔试题_从入门到脱发的博客-CSDN博客 我自行实现的简单解法:


public class Test {
    private int currentI = 0;

    private synchronized void printSpace(int inputLength) {
        while (true) {
            if (currentI == inputLength) {
                currentI = 0;
                System.out.print(" ");
                this.notifyAll();
            } else {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private synchronized void printLetter(int i, char c) {
        while (true) {
            if (i == currentI) {
                currentI++;
                System.out.print(c);
                this.notifyAll();
            } else {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private synchronized void print(String input) throws Exception {
        Test test = new Test();
        for (int i = 0; i < input.length(); i++) {
            char c = input.charAt(i);
            int finalI = i;
            Thread tt = new Thread(() -> test.printLetter(finalI, c));
            tt.start();
        }
        Thread space = new Thread(() -> test.printSpace(input.length()));
        space.start();
        space.join();
    }


    public static void main(String[] args) throws Exception {
        Test test = new Test();
        test.print("alibaba");
    }
}

9、IO

①、BIO

Block-IO:InputStream和OutputStream,Reader和Writer。属于同步阻塞模型

同步阻塞:一个请求占用一个进程处理,先等待数据准备好,然后从内核向进程复制数据,最后处理完数据后返回

②、NIO

NonBlock-IO:Channel、Buffer、Selector。属于IO多路复用的同步非阻塞模型

同步非阻塞:进程先将一个套接字在内核中设置成非阻塞再等待数据准备好,在这个过程中反复轮询内核数据是否准备好,准备好之后最后处理数据返回

IO多路复用:同步非阻塞的优化版本,区别在于IO多路复用阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的IO系统调用上。换句话说,轮询机制被优化成通知机制,多个连接公用一个阻塞对象,进程只需要在一个阻塞对象上等待,无需再轮询所有连接

在Java的NIO中,是基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector用于监听多个通道的事件(比如:连接打开,数据到达)

因此,单个线程可以监听多个数据通道,Selector的底层实现是epoll/poll/select的IO多路复用模型,select方法会一直阻塞,直到channel中有事件就绪:

与BIO区别如下:

  1. 通过缓冲区而非流的方式进行数据的交互,流是进行直接的传输的没有对数据操作的余地,缓冲区提供了灵活的数据处理方式
  2. NIO是非阻塞的,意味着每个socket连接可以让底层操作系统帮我们完成而不需要每次开个线程去保持连接,使用的是selector监听所有channel的状态实现
  3. NIO提供直接内存复制方式,消除了JVM与操作系统之间读写内存的损耗

③、AIO

Asynchronous IO:属于事件和回调机制的异步非阻塞模型

AIO得到结果的方式:

  • 基于回调:实现CompletionHandler接口,调用时触发回调函数
  • 返回Future:通过isDone()查看是否准备好,通过get()等待返回数据

但要实现真正的异步非阻塞IO,需要操作系统支持,Windows支持而Linux不完善

10、问题排查

①、Java生产环境下问题排查

在生产环境中,我们无法通过断点调试、新增log、可视化工具去立马查看当前的运行状态和拿到错误信息,此时,借助Java自带的命令行工具以及相关dump分析工具以及一些小技巧,可以大大提升我们排查问题的效率。

②、运行参数

下面会列出一些常用且非常有效的命令以及参数来查看运行时Java程序的信息,从而辅助你了解程序运行状态。还有大量可用的功能由其他参数提供,自行参阅oracle文档

查看JVM参数

jps -l 查看所有正在运行的Java程序,同时显示启动类类名,获取到PID

4706 org.apache.catalina.startup.Bootstrap
5023 sun.tools.jps.Jps

jinfo -flags PID 查看运行时进程参数与JVM参数

Attaching to process ID 28987, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.171-b11
Non-default VM flags: -XX:CICompilerCount=3 -XX:InitialHeapSize=132120576 -XX:MaxHeapSize=2092957696 -XX:MaxNewSize=697303040 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=44040192 -XX:OldSize=88080384 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
Command line:  -Dspring.config.location=application.properties -Dspring.profiles.active=staging

java -XX:+PrintFlagsFinal -version 查看当前虚拟机默认JVM参数

查看即时GC状态

jstat -gc PID 1000 10 每秒查看一次gc信息,共10次

输出比较多的参数,每个字段的解释参看 https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html

 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
512.0  512.0   15.3   0.0    4416.0   1055.2   11372.0     7572.5   14720.0 14322.5 1664.0 1522.8     40    0.137   8      0.039    0.176

 期间可能碰到提示sun.jvm.hotspot.runtime.VMVersionMismatchException: Supported versions are 24.181-b01. Target VM is 25.171-b11的问题,原因在于安装了多个版本,使用whichls -l可简介定位到与当前执行Java程序相同的Java版本

③、错误排查

内存问题

内存泄露导致OOM?内存占用异常的高?这是生产环境常常出现的问题,Java提供dump文件供我们对内存里发生过的事情进行了记录,我们需要借助一些工具从中获取有价值的信息。

导出Dump文件

  1. 提前对Java程序加上这些参数印dump文件 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./
  2. 对正在运行的程序使用jmapjmap -dump:format=b,file=heap.hprof PID

分析Dump文件

如果Dump文件不太大的话,可以传到 World-class heap Dump analysis - Java, Android memory dump analyzer 来分析

文件比较大,且想进行更加系统的分析,推荐使用MAT分析,有如下几种常用查看方式

  1. 首页中的【Leak Suspects】能推测出问题所在
  2. 点击【Create a histogram from an arbitrary set of objects】查到所有对象的数量
  3. 右键点击某个对象【Merge Shortest Paths to GC Roots】-> 【exclude all phantom/weak/soft etc. references】能查询到大量数量的某个对象是哪个GC ROOT引用的

线程问题

任务长时间不退出?CPU 负载过高?很可能因为死循环或者死锁,导致某些线程一直执行不被中断,但是不报错是最烦人的,所以日志里看不到错误信息,并且又不能用dump文件分析,因为跟内存无关。这个时候就需要用线程分析工具来帮我们了。

导出jstack文件

使用jstack PID > 文件,如果失败请加-F参数,如果还失败请使用Java程序启动时使用的用户执行jstack,下面是jstack的部分输出格式

      线程名                                                              PID的16进制
"http-nio-8080-Acceptor-0" #17 daemon prio=5 os_prio=0 tid=0x00007fac2c4bd000 nid=0x29f4 runnable [0x00007fac192f6000]
   java.lang.Thread.State: RUNNABLE(tomcat的工作线程正在运行,有NEW/RUNNABLE/BLOCKED/WAITING/TIMED_WATING/TERMINATED状态)
    at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method)
    at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422)
    at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250)
    - locked <0x00000000faf845a8> (a java.lang.Object)
    at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:682)
    at java.lang.Thread.run(Thread.java:748)

jstack的输出可以看到所有的线程以及他们的状态,我们就可以看有哪些我们自己创建的正在运行的线程,那很可能就是那个一直在执行的线程了,此时线程名就格外重要了,所以建议创建新线程时指定有意义的线程名。当然,通过PID查找也非常方便。

排查步骤

  1. top 查看到哪个java程序负载高
  2. top -p PID -H 查看该进程所有进程的运行状态
  3. 记录下高负载的线程ID,printf "&x" PID转换成16进制
  4. jstack PID > 文件
  5. 在jstack文件中用转换成16进制之后的线程ID查询线程运行堆栈
  6. 从堆栈中了解到线程在执行什么任务,并结合业务与代码判断问题所在

二、Web框架、数据库

三、通用基础

四、分布式

五、微服务

六、算法

七、项目举例

八、系统设计

九、智力题

十、除开知识点,一定要准备好以下套路

  1. 个人介绍,需要准备1分钟和5分钟两个版本,包括学习经历、工作经历、项目经历、个人优势、一句话总结。一定要自己背得滚瓜烂熟,张口就来
  2. 抽象概念,当面试官问你是如何理解多线程的时候,你要知道从定义、来源、实现、问题、优化、应用方面系统性地回答
  3. 项目强化,至少与知识点的比例是五五开,所以必须针对简历中的两个以上的项目,形成包括【架构和实现细节】,【正常流程和异常流程的处理】,【难点+坑+复盘优化】三位一体的组合拳
  4. 压力练习,面试的时候难免紧张,可能会严重影响发挥,通过平时多找机会参与交流分享,或找人做压力面试来改善
  5. 表达练习,表达能力非常影响在面试中的表现,能否简练地将答案告诉面试官,可以通过给自己讲解的方式刻意练习
  6. 重点针对,面试官会针对简历提问,所以请针对简历上写的所有技术点进行重点准备

猜你喜欢

转载自blog.csdn.net/qq_41701956/article/details/126808210