1、Java中垃圾收集的方法有哪些 难度系数:⭐
采用分区分代回收思想:
- 复制算法 年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)
a) 效率高,缺点:需要内存容量大,比较耗内存
b) 使用在占空间比较小、刷新次数多的新生区
- 标记-清除 老年代一般是由标记清除或者是标记清除与标记整理的混合实现
a) 效率比较低,会差生碎片。
- 标记-整理 老年代一般是由标记清除或者是标记清除与标记整理的混合实现
a) 效率低速度慢,需要移动对象,但不会产生碎片。
2、如何判断一个对象是否存活(或者GC对象的判定方法) 难度系数:⭐
- 引用计数法
所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.
引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。
- 可达性算法(引用链法)
- 该算法的基本思路就是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
- 在java中可以作为GC Roots的对象有以下几种:虚拟机栈中引用的对象、方法区类静态属性引用的对象、方法区常量池引用的对象、本地方法栈JNI引用的对象。
3、什么情况下会产生StackOverflowError(栈溢出)和OutOfMemoryError(堆溢出)怎么排查 难度系数:⭐⭐
- 引发 StackOverFlowError 的常见原因有以下几种
- 无限递归循环调用(最常见)
- 执行了大量方法,导致线程栈空间耗尽
- 方法内声明了海量的局部变量
- native 代码有栈上分配的逻辑,并且要求的内存还不小,比如 java.net.SocketInputStream.read0 会在栈上要求分配一个 64KB 的缓存(64位 Linux)。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】即可免费获取
- 引发 OutOfMemoryError的常见原因有以下几种
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收
- 代码中存在死循环或循环产生过多重复的对象实体
- 启动参数内存值设定的过小
- 排查:可以通过jvisualvm进行内存快照分析
- 栈溢出、堆溢出案例演示
public class StackOverFlowTest {
private static int count = 1;
public static void main(String[] args) {
//模拟栈溢出
//getDieCircle();
//模拟堆溢出
getOutOfMem();
}
public static void getDieCircle(){
System.out.println(count++);
getDieCircle();
}
public static void getOutOfMem(){
while (true) {
Object o = new Object();
System.out.println(o);
}
}
}
Java
4、什么是线程池,线程池有哪些(创建) 难度系数:⭐
线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率
在 JDK 的 java.util.concurrent.Executors 中提供了生成多种线程池的静态方法。
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4);
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4);
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
然后调用他们的 execute 方法即可。
这4种线程池底层 全部是ThreadPoolExecutor对象的实现,阿里规范手册中规定线程池采用ThreadPoolExecutor自定义的,实际开发也是。
- newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。这种类型的线程池特点是:
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】即可免费获取
- newFixedThreadPool
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
- newSingleThreadExecutor
创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
- newScheduleThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行。例如延迟3秒执行。
5、为什么要使用线程池 难度系数:⭐
- 线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最 大数量,超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
- 主要特点:线程复用;控制最大并发数:管理线程。
第一:降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进 行统一的分配,调优和监控
8、线程池底层工作原理 难度系数:⭐
- 第一步:线程池刚创建的时候,里面没有任何线程,等到有任务过来的时候才会创建线程。当然也可以调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建corePoolSize个线程
- 第二步:调用execute()提交一个任务时,如果当前的工作线程数<corePoolSize,直接创建新的线程执行这个任务
- 第三步:如果当时工作线程数量>=corePoolSize,会将任务放入任务队列中缓存
- 第四步:如果队列已满,并且线程池中工作线程的数量<maximumPoolSize,还是会创建线程执行这个任务
- 第五步:如果队列已满,并且线程池中的线程已达到maximumPoolSize,这个时候会执行拒绝策略,JAVA线程池默认的策略是AbortPolicy,即抛出RejectedExecutionException异常
9、ThreadPoolExecutor对象有哪些参数 怎么设定核心线程数和最大线程数 拒绝策略有哪些 难度系数:⭐
参数与作用:共7个参数
- corePoolSize:核心线程数,
在ThreadPoolExecutor中有一个与它相关的配置:allowCoreThreadTimeOut(默认为false),当allowCoreThreadTimeOut为false时,核心线程会一直存活,哪怕是一直空闲着。而当allowCoreThreadTimeOut为true时核心线程空闲时间超过keepAliveTime时会被回收。
- maximumPoolSize:最大线程数
线程池能容纳的最大线程数,当线程池中的线程达到最大时,此时添加任务将会采用拒绝策略,默认的拒绝策略是抛出一个运行时错误(RejectedExecutionException)。值得一提的是,当初始化时用的工作队列为LinkedBlockingDeque时,这个值将无效。
- keepAliveTime:存活时间,
当非核心空闲超过这个时间将被回收,同时空闲核心线程是否回收受allowCoreThreadTimeOut影响。
- unit:keepAliveTime的单位。
- workQueue:任务队列
常用有三种队列,即SynchronousQueue,LinkedBlockingDeque(无界队列),ArrayBlockingQueue(有界队列)。
- threadFactory:线程工厂,
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】即可免费获取
ThreadFactory是一个接口,用来创建worker。通过线程工厂可以对线程的一些属性进行定制。默认直接新建线程。
- RejectedExecutionHandler:拒绝策略
也是一个接口,只有一个方法,当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的rejectedExecution法。默认是抛出一个运行时异常。
线程池大小设置:
- 需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型
- 每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉及到网络传输以及底层系统资源依赖有关系
如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行线程数,加入 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1 如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态,导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。
一个公式:线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间)/
线程 CPU 时间 )* CPU 数目
这个公式的线程 cpu 时间是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner测试大量运行次数求出平均值)
拒绝策略:
- AbortPolicy:直接抛出异常,默认策略;
- CallerRunsPolicy:用调用者所在的线程来执行任务;
- DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
- DiscardPolicy:直接丢弃任务;当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务
10、常见线程安全的并发容器有哪些 难度系数:⭐
- CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap
- CopyOnWriteArrayList、CopyOnWriteArraySet采用写时复制实现线程安全
- ConcurrentHashMap采用分段锁的方式实现线程安全
11、Atomic原子类了解多少 原理是什么 难度系数:⭐
Java 的原子类都存放在并发包 java.util.concurrent.atomic下,如下图:
基本类型
- 使用原子的方式更新基本类型
- AtomicInteger:整型原子类
- AtomicLong:长整型原子类
- AtomicBoolean:布尔型原子类
数组类型
- 使用原子的方式更新数组里的某个元素
- AtomicIntegerArray:整形数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray:引用类型数组原子类
引用类型
- AtomicReference:引用类型原子类
- AtomicStampedReference:原子更新引用类型里的字段原子类
- AtomicMarkableReference :原子更新带有标记位的引用类型
- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,以及解决使用 CAS 进行原子更新时可能出现的 ABA 问题
- AtomicInteger 类利用 CAS (Compare and Swap) + volatile + native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
- CAS 的原理,是拿期望值和原本的值作比较,如果相同,则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是个本地方法,这个方法是用来拿“原值”的内存地址,返回值是 valueOffset;另外,value 是一个 volatile 变量,因此 JVM 总是可以保证任意时刻的任何线程总能拿到该变量的最新值。
12、synchronized底层实现是什么 lock底层是什么 有什么区别 难度系数:⭐⭐⭐
Synchronized原理:
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词),然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。
代码块的同步是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】即可免费获取
Lock原理:
- Lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)
- Lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。
- Lock释放锁的过程:修改状态值,调整等待链表。
- Lock大量使用CAS+自旋。因此根据CAS特性,lock建议使用在低锁冲突的情况下。
Lock与synchronized的区别:
- Lock的加锁和解锁都是由java代码配合native方法(调用操作系统的相关方法)实现的,而synchronize的加锁和解锁的过程是由JVM管理的
- 当一个线程使用synchronize获取锁时,若锁被其他线程占用着,那么当前只能被阻塞,直到成功获取锁。而Lock则提供超时锁和可中断等更加灵活的方式,在未能获取锁的 条件下提供一种退出的机制。
- 一个锁内部可以有多个Condition实例,即有多路条件队列,而synchronize只有一路条件队列;同样Condition也提供灵活的阻塞方式,在未获得通知之前可以通过中断线程以 及设置等待时限等方式退出条件队列。
- synchronize对线程的同步仅提供独占模式,而Lock即可以提供独占模式,也可以提供共享模式
synchronized | Lock |
关键字 | 类 |
自动加锁和释放锁 | 需要手动调用unlock方法释放锁 |
jvm层面的锁 | API层面的锁 |
非公平锁 | 可以选择公平或者非公平锁 |
锁是一个对象,并且锁的信息保存在了对象中 | 代码中通过int类型的state标识 |
有一个锁升级的过程 | 无 |
13、了解ConcurrentHashMap吗 为什么性能比HashTable高,说下原理 难度系数:⭐⭐
ConcurrentHashMap是线程安全的Map容器,JDK8之前,ConcurrentHashMap使用锁分段技术,将数据分成一段段存储,每个数据段配置一把锁,即segment类,这个类继承ReentrantLock来保证线程安全,JKD8的版本取消Segment这个分段锁数据结构,底层也是使用Node数组+链表+红黑树,从而实现对每一段数据就行加锁,也减少了并发冲突的概率。
hashtable类基本上所有的方法都是采用synchronized进行线程安全控制,高并发情况下效率就降低 ,ConcurrentHashMap是采用了分段锁的思想提高性能,锁粒度更细化
14、ConcurrentHashMap底层原理 难度系数:⭐⭐⭐
- Java7 中 ConcurrentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。
-
public V put(K key, V value) {
-
Segment<K,V> s;
-
if (value == null)
-
throw new NullPointerException();
-
int hash = hash(key);
-
// hash 值无符号右移 28位(初始化时获得),然后与 segmentMask=15 做与运算
-
// 其实也就是把高4位与segmentMask(1111)做与运算
-
// this.segmentMask = ssize - 1;
-
//对hash值进行右移segmentShift位,计算元素对应segment中数组下表的位置
-
//把hash右移segmentShift,相当于只要hash值的高32-segmentShift位,右移的目的是保留了hash值的高位。然后和segmentMask与操作计算元素在segment数组中的下表
-
int j = (hash >>> segmentShift) & segmentMask;
-
//使用unsafe对象获取数组中第j个位置的值,后面加上的是偏移量
-
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
-
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
-
// 如果查找到的 Segment 为空,初始化
-
s = ensureSegment(j);
-
//插入segment对象
-
return s.put(key, hash, value, false);
-
}
-
/**
-
* Returns the segment for the given index, creating it and
-
* recording in segment table (via CAS) if not already present.
-
*
-
* @param k the index
-
* @return the segment
-
*/
-
@SuppressWarnings("unchecked")
-
private Segment<K,V> ensureSegment(int k) {
-
final Segment<K,V>[] ss = this.segments;
-
long u = (k << SSHIFT) + SBASE; // raw offset
-
Segment<K,V> seg;
-
// 判断 u 位置的 Segment 是否为null
-
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
-
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
-
// 获取0号 segment 里的 HashEntry<K,V> 初始化长度
-
int cap = proto.table.length;
-
// 获取0号 segment 里的 hash 表里的扩容负载因子,所有的 segment 的 loadFactor 是相同的
-
float lf = proto.loadFactor;
-
// 计算扩容阀值
-
int threshold = (int)(cap * lf);
-
// 创建一个 cap 容量的 HashEntry 数组
-
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
-
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck
-
// 再次检查 u 位置的 Segment 是否为null,因为这时可能有其他线程进行了操作
-
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
-
// 自旋检查 u 位置的 Segment 是否为null
-
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
-
== null) {
-
// 使用CAS 赋值,只会成功一次
-
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
-
break;
-
}
-
}
-
}
-
return seg;
-
}
-
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
-
// 获取 ReentrantLock 独占锁,获取不到,scanAndLockForPut 获取。
-
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
-
V oldValue;
-
try {
-
HashEntry<K,V>[] tab = table;
-
// 计算要put的数据位置
-
int index = (tab.length - 1) & hash;
-
// CAS 获取 index 坐标的值
-
HashEntry<K,V> first = entryAt(tab, index);
-
for (HashEntry<K,V> e = first;;) {
-
if (e != null) {
-
// 检查是否 key 已经存在,如果存在,则遍历链表寻找位置,找到后替换 value
-
K k;
-
if ((k = e.key) == key ||
-
(e.hash == hash && key.equals(k))) {
-
oldValue = e.value;
-
if (!onlyIfAbsent) {
-
e.value = value;
-
++modCount;
-
}
-
break;
-
}
-
e = e.next;
-
}
-
else {
-
// first 有值没说明 index 位置已经有值了,有冲突,链表头插法。
-
if (node != null)
-
node.setNext(first);
-
else
-
node = new HashEntry<K,V>(hash, key, value, first);
-
int c = count + 1;
-
// 容量大于扩容阀值,小于最大容量,进行扩容
-
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
-
rehash(node);
-
else
-
// index 位置赋值 node,node 可能是一个元素,也可能是一个链表的表头
-
setEntryAt(tab, index, node);
-
++modCount;
-
count = c;
-
oldValue = null;
-
break;
-
}
-
}
-
} finally {
-
unlock();
-
}
-
return oldValue;
-
}
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】即可免费获取
Java
- Java8 中的 ConcurrentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。
-
public V put(K key, V value) {
-
return putVal(key, value, false);
-
}
-
/** Implementation for put and putIfAbsent */
-
final V putVal(K key, V value, boolean onlyIfAbsent) {
-
// key 和 value 不能为空
-
if (key == null || value == null) throw new NullPointerException();
-
int hash = spread(key.hashCode());
-
int binCount = 0;
-
for (Node<K,V>[] tab = table;;) {
-
// f = 目标位置元素
-
Node<K,V> f; int n, i, fh;// fh 后面存放目标位置的元素 hash 值
-
if (tab == null || (n = tab.length) == 0)
-
// 数组桶为空,初始化数组桶(自旋+CAS)
-
tab = initTable();
-
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
-
// 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出
-
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
-
break; // no lock when adding to empty bin
-
}
-
else if ((fh = f.hash) == MOVED)
-
tab = helpTransfer(tab, f);
-
else {
-
V oldVal = null;
-
// 使用 synchronized 加锁加入节点
-
synchronized (f) {
-
if (tabAt(tab, i) == f) {
-
// 说明是链表
-
if (fh >= 0) {
-
binCount = 1;
-
// 循环加入新的或者覆盖节点
-
for (Node<K,V> e = f;; ++binCount) {
-
K ek;
-
if (e.hash == hash &&
-
((ek = e.key) == key ||
-
(ek != null && key.equals(ek)))) {
-
oldVal = e.val;
-
if (!onlyIfAbsent)
-
e.val = value;
-
break;
-
}
-
Node<K,V> pred = e;
-
if ((e = e.next) == null) {
-
pred.next = new Node<K,V>(hash, key,
-
value, null);
-
break;
-
}
-
}
-
}
-
else if (f instanceof TreeBin) {
-
// 红黑树
-
Node<K,V> p;
-
binCount = 2;
-
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
-
value)) != null) {
-
oldVal = p.val;
-
if (!onlyIfAbsent)
-
p.val = value;
-
}
-
}
-
}
-
}
-
if (binCount != 0) {
-
if (binCount >= TREEIFY_THRESHOLD)
-
treeifyBin(tab, i);
-
if (oldVal != null)
-
return oldVal;
-
break;
-
}
-
}
-
}
-
addCount(1L, binCount);
-
return null;
-
}
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】即可免费获取