新鲜出炉java后端高频面经总结-持续更新中(万字长文,助君青云)(中)

简介: 新鲜出炉java后端高频面经总结-持续更新中(万字长文,助君青云)(中)

JVM


jvm类加载机制


类加载机制:jvm把数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。


类的生命周期:


加载:查找并加载类的二进制数据

验证:确保被加载的类的正确性(文件格式、元数据、字节码、符号引用等验证)

准备:为类的静态变量分配内存,并将其初始化为默认值

解析:把常量池中的符号引用转换为直接引用

初始化:对类的静态变量,静态代码块执行初始化操作

使用:类访问方法区内的数据结构的接口, 对象是Heap区的数据。

卸载:java虚拟机将结束生命周期的几种情况

执行了System.exit()方法

程序正常执行结束

程序在执行过程中遇到了异常或错误而异常终止

由于操作系统出现错误而导致Java虚拟机进程终止

类加载的三种方式:


命令行启动应用时候由JVM初始化加载

通过Class.forName()方法动态加载

通过ClassLoader.loadClass()方法动态加载

jvm类加载机制:


全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。


父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。


缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。


双亲委派机制:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。


判断2个类是否相同:


首先看他们的类加载器是不是一样的,如果不一样,那么肯定不是同一个类


jvm内存模型


jvm局部变量表


局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量

虚拟机栈由栈帧组成,栈帧由局部变量表、操作数栈、动态链接和方法返回四部分组成,有的虚拟机还有一些附加信息


内存模式在jdk1.8的改动


元数据区取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存


废弃永久代原因:官方解释是为了融合其他vm,实际使用中永久代内存经常不够用或发生内存泄露


jvm中重要的内存区域


方法区:专门用来存放已经加载的类信息,常量,静态变量以及方法代码的内存区域

常量池:是方法区的一部分,主要用来存放常量和类中的符号引用等信息;

堆区:存放类的对象实例

栈区:也叫Java虚拟机栈,由一个个的栈帧组成的后进先出的栈式结构,存放方法运行时产生的局部变量,方法出口等信息。当调用一个方法时,虚拟机栈就会创建一个栈帧存放这些数据,当方法调用完成时,栈帧消失,如果方法调用了其他方法,则继续在栈顶创建新的栈帧。


堆、栈、方法区会出现的异常:


内存溢出

方法区 PermGen space:Perm被占满,无法为新的class分配存储空间而引发的异常


主要原因:就是大量动态反射生成的类不断被加载

解决方法:-XX:MaxPermSize=16m,用完时,进行GC

栈溢出(只有栈):递归没返回,或者循环调用造成


内存泄漏:解决办法一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。


怎么让方法区溢出 方法区存的是 类加载信息、静态变量、常量、常量池


静态块中写while循环,调用String的intern。intern()方法设计的初衷:重用String对象,以节省内存消耗,使用intern的话,时间会比不适用intern的时间稍微长点,但是如果不适用intern的话,GC花费的时间更长。intern()方法是把String对象的value放入常量池中


动态发射会加载很多类信息,也会造成方法区溢出


内存分配和垃圾收集gc算法


内存分配策略


对象优先分配在新生代(Eden区),MinorGC(新生代垃圾回收)后存活对象进入Survivor区,一般Eden:Survivor:Survivor= 8:1:1

大对象直接进入老年代

大年龄的对象进入老年代(默认15岁提升入老年代)

动态年龄分配:并不是永远要求对象年龄必须达到「阈值」才能提升至老年代。在有的垃圾收集器实现中,如果Survivor空间中相同年龄的对象占用空间>Survivor总空间的一半,则此年龄的所有对象就可以提前进入老年代,而不是必须达到阈值。

空间分配担保机制:在MinorGC之前,JVM会首先检查老年代最大可用的连续内存空间是否 > 青年代所有对象总空间,并以其作为MajorGC执行的「担保」。如果大于则MinorGC可以正常执行。否则JVM会查看HandlePromotionFailure设置值是否允许担保失败,如果允许,则继续执行MinorGC,否则则执行MajorGC(老年代垃圾回收)用来回收足够的内存空间。


Full GC (新生代+老年代垃圾回收)


Full GC可以理解为Major GC+Minor GC组合后进行的一整个过程,是清理JVM整个堆空间(年轻代和老年代空间)。


Full GC触发条件


调用System.gc()方法时,可通过-XX:+ DisableExplicitGC 参数来禁止调用System.gc()。

当方法区空间不足时。

Minor GC后存活的对象大小超过了老年代剩余空间。

Minor GC时中Survivor幸存区空间不足时,判断是否允许担保失败,不允许则触发Full GC。允许,并且每次晋升到老年代的对象平均大小>老。年代最大可用连续内存空间,也会触发Full GC。

CMS GC异常,CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,会触发Full GC。


垃圾回收算法


标记-清除:碎片,所以尽量不要-(适用老年代)

标记-整理-(适用老年代)

复制算法-(适用新生代)

分代收集算法


垃圾收集器


Serial收集器:(适用新生代+单线程“串行”)它在进行垃圾回收的工作时,必须暂停JVM中的其他工作线程


Serial Old收集器:(适用老年代+单线程“串行”)是Serial收集器在老年代上的版本,同样是采用复制算法的单线程收集器


Parnew收集器:(适用新生代+多线程“并行”)其实就是Serial收集器的多线程版本


Parellel Scavenge收集器:(适用新生代+多线程“并行”)采用复制算法的收集器,主要关注点在于达到一个可控制的吞吐量


Parellel Old收集器:(适用老年代+多线程“并行”)是Parellel Scavenge的老年代版本,采用多线程和标记-压缩算法。


CMS收集器:(标记-清除并发收集器+多线程)


收集过程分为4步:


初始标记:标记GC Roots能直接关联到的对象,速度很快


并发标记:进行GC Roots追踪的过程


重新标记:修正并发标记期间由于用户程序继续执行可能产生变动的那部分对象的标记记录,此阶段会比初始标记长一些,但远小于并发标记的时间。


并发清除


优点:


GC收集间隔时间短,多线程并发。

缺点:


1.并发时对CPU资源占用多,不适合CPU核心数较少的情况。

2.且由于采用标记清除算法,所以会产生内存碎片。

3.无法处理浮动垃圾。(CMS并发清除阶段用户线程还可以继续执行,可能产生新垃圾)

G1收集器


并行与并发:G1能充分利用多CPU下的优势来缩短Stop The World的时间,同时在其他部分收集器需要停止Java线程来执行GC动作时,G1收集器仍然可以通过并发来让Java线程同步执行。


分代收集:与其他收集器一样,分代的概念在G1中任然被保留。可以不需要配合其他的垃圾收集器,就独立管理整个Java堆内存的所有分代区域,且采用不同的方式来获得更好的垃圾收集效果。


空间整合:G1从整体来看,使用的是标记-压缩算法实现的,从局部两个Region来看,采用的是复制算法实现的,对内存空间的利用非常高效,不会像CMS一样产生内存碎片。


可以预测的停顿:除了追求低停顿以外,G1的停顿时间可以被指定在一个时间范围内。

如果不计算维护Remenbered Set的操作,G1收集器的工作阶段大致区分如下:


初始标记

并发标记

最终标记

筛选回收

垃圾回收器分类: JDK1.7和1.8(默认parallel scavenge,parallel old),1.9(默认G1)


1、串行处理器(单线程):serial,serial old


2、并行处理器(多线程):ParNew,parallel scavenge,parallel old 适用情况:“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、科学计算。 缺点:垃圾收集过程中应用响应时间可能加长


3、并发处理器(多线程):cms,G1;特点:对响应时间有高要求(低停顿)


★★★★★ 重点说下G1的垃圾回收过程:初始标记;并发标记;最终标记;选择回收(CMS是并发回收,G1相对于CMS的优点)


问题4:JVM调优?或者做过关于性能提高的工作吗


曾经遇到过性能调优的一个情况,起因是一个POD容器频繁宕机,通过检查发现相比其他容器,GC比较频繁,便考虑到可能是内存分配较小,调大了内存后,GC不频繁,宕机情况也解决


判断对象可以被回收:基于GC Roots的可达性分析算法,如果对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为垃圾对象,会被 GC 回收。


JVM调优


选择合适的垃圾回收器

CPU单核选择Serial垃圾收集器(新生代)

CPU多核

关注吞吐量 ,那么选择PS+PO组合,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器

关注用户停顿时间

JDK版本1.6或者1.7,那么选择CMS垃圾收集器(老年代)

JDK1.8及以上,JVM可用内存6G以上,那么选择G1垃圾收集器

调整内存大小(现象:垃圾收集频率非常频繁。若GC频繁但回收对象少,可能是内存泄露导致对象无法回收)

设置符合预期的停顿时间(现象:程序间接性的卡顿)

调整内存区域大小比例(现象:某一个区域的GC频繁,其他都正常。也有可能是内存泄露情况)

调整对象升老年代的年龄(现象:老年代频繁GC,每次回收的对象很多)

调整大对象的标准(现象:老年代频繁GC,每次回收的对象很多,而且单个对象的体积都比较大)

调整GC的触发时机(现象:CMS,G1 经常 Full GC,程序卡顿严重)

调整JVM本地内存大小(现象:GC的次数、时间和回收的对象都正常,堆内存空间充足,但是报OOM)


jvm使用过的参数


参数 含义
-XX:+UseConcMarkSweepGC 使用CMS垃圾回收器
-XX:+UseCMSCompactAtFullCollection 在使用ConcurrentGC(并发GC)的情况下,防止内存碎片,对存活对象进行整理,使碎片减少
-XX:+CMSClassUnloadingEnabled 老年代启用CMS,但默认是不会回收永久代的。此处对永久代启用类回收,防止内存满
-XX:+UseCMSInitiatingOccupancyOnly 只有在老年代使用了初始化的比例后ConcurrentCollector(并发收集器)启动收集
-XX:+PrintGCDetails 打印GC日志
-XX:+PrintGCDateStamps 打印GC日志对应的时间戳
-XX:-OmitStackTraceInFastThrow 省略异常栈信息从而快速抛出
XX:+HeapDumpOnOutOfMemoryError 可以让JVM在出现内存溢出时候Dump出当前的内存快照dump文件
-XX:MetaspaceSize=2g 初始化的Metaspace大小,该值越大触发Metaspace GC的时机就越晚。
-XX:MaxMetaspaceSize=512m 限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。默认无上限
-XX:MaxDirectMemorySize=1g 指定本地直接内存大小,如果不指定,则默认与Java堆的最大值-Xmx指定一样
-Xmx4g -Xms4g jvm内存最大最小值
-Xmn2g 设置年轻代大小为2G
-XXSurvivorRatio=3 代表Eden:Survivor:Survivor = 3:1:1


dump


HeapDump文件是指定时刻的Java堆栈的快照,是一种镜像文件。


jmap 命令是JDK提供的用于生成堆内存信息的工具,切换到JDK_HOME/bin目录下后,执行dump命令

***(./jmap -dump:live,format=b,file=heap.hprof )***其中pid是JVM进程的id,heap.hprof是生成的heap dump文件,在执行命令的目录下面

使用dump文件分析工具分析展示

jhat 是JDK自带的用于分析JVM Heap Dump文件的工具

命令 jhat heap-dump-file 是文件的路径和文件名

访问 http://localhost:7000/ 即可以看到结果。


java制造OOM


jvm的几个运行时区域都有发生 OutOfMemoryError 异常的可能



锁是多线程环境的一种同步机制,对线程访问资源权限进行控制,实现并发策略


volatile


是轻量级的synchronized,保证共享变量的“可见性”,只能对变量作用,如果使用恰当,比synchronized的使用和执行成本更低(不会引起线程上下文切换和调度)


只保证多线程操作的可见性(将当前处理器缓存行的数据写回到系统内存,同时这个写回内存操作会使其他CPU里缓存了该内存地址的数据无效),不保证原子性


synchronize-互斥锁


只能作用在方法上,互斥锁


Syncronized是重量级锁,加了 syncronized 关键字的方法、代码块中,一次只允许一个线程进入特定代码段,从而避免多线程同时修改同一数据


应用Sychronized注意:


一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;

每个实例都对应有自己的一把锁(this),不同实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象公用同一把锁

synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁


synchronized锁的实现


有两种形式上锁,一个是对方法上锁,一个是构造同步代码块。他们的底层实现其实都一样,在进入同步代码之前先获取锁,获取到锁之后锁的计数器+1,同步代码执行完锁的计数器-1,如果获取失败就阻塞式等待锁的释放。只是他们在同步块识别方式上有所不一样,从class字节码文件可以表现出来,一个是通过方法flags标志,一个是monitorenter和monitorexit指令操作。


synchronized锁底层实现


理解锁实现原理之前要先了解Java的对象头和Monitor,在JVM中,对象是分成三部分存在的:对象头、实例数据(存放类的属性数据信息)、对其填充(仅是字节对齐作用)。


对象头是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word 和 Class Metadata Address组成,其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例。


锁的类型和状态在对象头Mark Word中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。


每一个锁都对应一个monitor对象,当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。


锁的升级


锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁,在 JDK 1.6之前,synchronized 还是一个重量级锁,是一个效率比较低下的锁,但是在JDK 1.6后,Jvm为了提高锁的获取与释放效率对(synchronized )进行了优化,引入了 偏向锁 和 轻量级锁,并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。


升级过程:


无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。


偏向锁:初次执行到synchronized代码块的时候,锁对象变成偏向锁,执行完同步代码块后,线程并不会主动释放偏向锁。


轻量级锁:当锁是偏向锁的时候,却被另外的线程访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。


重量级锁:如果锁竞争情况严重,某个达到最大自旋次数(默认10次)的线程,会将轻量级锁升级为重量级锁。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待被唤醒。

d0863030393e49e995be3eb6da62f8ed.png


公平锁:先申请的先得到锁


非公平锁:先申请的不一定先得到锁。一般来说,使用非公平锁可以获得较大的吞吐量,所以推荐优先使用非公平锁。


悲观锁:在读数据的时候总认为其他线程会对数据进行修改,所以采取加锁的形式,一旦本线程要读取数据时,就加锁,其他线程被阻塞,等待锁的释放。悲观锁总结为悲观加锁阻塞线程


乐观锁:在读数据时总认为其他线程不会对数据做修改,在更新数据时会判断其他线程有没有更新数据,如果有更新,则重新读取,再次尝试更新,循环上述步骤直到更新成功。这样来看乐观锁实际上是没有锁的,只是通过一种比较交换的方法来保证数据同步,总结为乐观无锁回滚重试。


CAS(比较和交换)


可重入锁:允许多个线程多次获取同一把锁,那从锁本身的角度来看,就是可以重新进入该锁。比如有一个递归函数里面有加锁操作,如果这个锁不阻塞自己,就是可重入锁,故也称递归锁 。


JDK中Lock锁接口


Lock相较于Synchronized优势如下:


可中断获取锁:使用synchronized关键字获取锁的时候,如果线程没有获取到被阻塞了,那么这个时候该线程是不响应中断(interrupt)的,而使用Lock.lockInterruptibly()获取锁时被中断,线程将抛出中断异常。

可非阻塞获取锁:使用synchronized关键字获取锁时,如果没有成功获取,只有被阻塞,而使用Lock.tryLock()获取锁时,如果没有获取成功也不会阻塞而是直接返回false。

可限定获取锁的超时时间:使用Lock.tryLock(long time, TimeUnit unit)。

同一个所对象上可以有多个等待队列(Conditin,类似于Object.wait(),支持公平锁模式)。

CopyOnWriteArrayList适用于写少读多的并发场景


ReadWriteLock即为读写锁,他要求写与写之间互斥,读与写之间互斥,读与读之间可以并发执行。在读多写少的情况下可以提高效率


AQS


链接:https://pdai.tech/md/java/thread/java-thread-x-lock-AbstractQueuedSynchronizer.html


AbstractQueuedSynchronizer的分析,最核心的就是sync queue的分析。


每一个结点都是由前一个结点唤醒

当结点发现前驱结点是head并且尝试获取成功,则会轮到该线程运行。

condition queue中的结点向sync queue中转移是通过signal操作完成的。

当结点的状态为SIGNAL时,表示后面的结点需要运行


多线程及线程池


线程生命周期


2f71d39f364145fb82c450add38d3a0c.png

新建状态:


使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。


就绪状态:


当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。


运行状态:


如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。


阻塞状态:


如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:


等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

死亡状态:


一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。


Java 提供了三种创建线程的方法:


通过实现 Runnable 接口;

通过继承 Thread 类本身;

通过 Callable 和 Future 创建线程。


创建线程的三种方式的对比


采用实现Runnable、Callable接口的方式创见多线程时,


优势是:线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势是:编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

使用继承Thread类的方式创建多线程时


优势是:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

劣势是:线程类已经继承了Thread类,所以不能再继承其他父类。

Object类的线程方法。


**notify() **:通知一个在对象上等待的线程,使其从wait()返回,而返回的前提是该线程获取到了对象的锁。


notifyAll(): 通知所有等待在该对象上的线程。


wait():调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意,调用wait()方法后,会释放对象的锁。


wait(long) :超时等待一段时间(参数是毫秒),如果没有通知就超时返回。


wait(long, int) : 对于超时时间更细粒度的控制,可以达到毫秒。


线程池


https://blog.csdn.net/fanrenxiang/article/details/79855992

https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html


线程池核心参数


corePoolSize 线程池核心线程大小:这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut


maximumPoolSize 线程池最大线程数量:当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到队列中。如果队列也满,则会创建新线程处理新提交的任务。


keepAliveTime 空闲线程存活时间:一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,空闲线程会被销毁


unit 空闲线程存活时间单位


workQueue 工作队列:新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列


ArrayBlockingQueue


基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。


LinkedBlockingQuene


基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数),因此使用该工作队列时,参数maxPoolSize其实是不起作用的。


SynchronousQuene


一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。


PriorityBlockingQueue


具有优先级的无界阻塞队列,优先级通过参数Comparator实现。


threadFactory 线程工厂:创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon(守护)线程等等


handler 拒绝策略:当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,就需要拒绝策略,jdk中提供了4种拒绝策略


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

AbortPolicy:直接丢弃任务,并抛出RejectedExecutionException异常。

DiscardPolicy:直接丢弃任务,什么都不做。

DiscardOldestPolicy:抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列

• 定时线程池同步数据


• 多数据库源问题的解决


数据结构


set、list、map区别:https://blog.csdn.net/qq_39241239/article/details/82116734https://www.jb51.net/article/112985.htm


java集合:主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。


java容器-集合


https://blog.csdn.net/dengpeng0419/article/details/47983033


排序


叫车服务,有500量符合要求的车,怎么快速选出前10量?

根据车的远近、司机的评分等因素,进行堆排序,时间复杂度500log10

堆排序是稳定的吗?不是,稳定的有哪些?冒泡、插入、归并


HashMap底层原理


介绍:HashMap是最常用的存储键值对的集合,继承了AbstractMap类,实现了Map等接口,内部原理是基于散列函数计算出元素存储的位置,查询的时候也是根据散列函数继续计算出存储的位置去获取该位置上存储的元素,非并发安全。


底层原理:1.7版本中,底层数据结构是数组+链表,也就是一个数组用来存储value,因为散列函数是很有可能出现哈希碰撞的,也就是两个不同的key计算得出同一个哈希值,结果就存到同一个数组索引上了,那不能覆盖掉前面的值呀,所以数组中存的是链表,如果有冲突了,同一个索引上就使用链表来存储多个值。


1.8版本中,因为链表的查询时间复杂度是O(n),所以冲突很严重,一个索引上的链表非常长,效率就很低了,所以在1.8版本的时候做了优化,当一个链表的长度超过8的时候就转换数据结构,不再使用链表存储,而是使用红黑树,红黑树是一个保证大致平衡的平衡树,所以性能相较AVL树这样的高度平衡树来将性能会更好。

目录
相关文章
|
17天前
|
前端开发 小程序 Java
uniapp上传图片 前端以及java后端代码实现
uniapp上传图片 前端以及java后端代码实现
31 0
|
1月前
|
弹性计算 前端开发 小程序
微信小程序上传文件至阿里云OSS直传(java后端签名+前端直传)
当前的通用文件上传方式是通过前端上传到服务器,再由服务器转存至对象存储。这种方式在处理小文件时效率尚可,但大文件上传因受限于服务器带宽,速度较慢。例如,一个100MB的文件在5Mbps带宽的阿里云ECS上上传至服务器需160秒。为解决此问题,可以采用后端签名的方式,使微信小程序直接上传文件到阿里云OSS,绕过服务器中转。具体操作包括在JAVA后端引入相关依赖,生成签名,并在微信小程序前端使用这个签名进行文件上传,注意设置正确的请求头和formData参数。这样能提高大文件上传的速度。
|
28天前
|
SQL 前端开发 Java
Java后端进阶之路: JavaWeb(四)
Java后端进阶之路: JavaWeb
33 1
|
XML SQL Java
Java后端进阶之路: JavaWeb(三)
Java后端进阶之路: JavaWeb
30 1
|
1月前
|
存储 前端开发 Java
[java后端研发]——文件上传与下载(2种方式)
[java后端研发]——文件上传与下载(2种方式)
|
1月前
|
算法 Java Python
用友Java后端笔试2023-8-5
用友Java后端笔试2023-8-5
30 0
用友Java后端笔试2023-8-5
|
1月前
|
机器学习/深度学习 XML JSON
web后端-java-httpClient
web后端-java-httpClient
|
1月前
|
存储 缓存 安全
大型互联网企业Java后端技术面试题总结(含答案)
大型互联网企业Java后端技术面试题总结(含答案)
45 0
|
1天前
|
安全 Java 调度
Java线程:深入理解与实战应用
Java线程:深入理解与实战应用
13 0
|
1天前
|
Java
Java中的并发编程:理解和应用线程池
【4月更文挑战第23天】在现代的Java应用程序中,性能和资源的有效利用已经成为了一个重要的考量因素。并发编程是提高应用程序性能的关键手段之一,而线程池则是实现高效并发的重要工具。本文将深入探讨Java中的线程池,包括其基本原理、优势、以及如何在实际开发中有效地使用线程池。我们将通过实例和代码片段,帮助读者理解线程池的概念,并学习如何在Java应用中合理地使用线程池。