大厂面试题详解一:Java基础篇

487 阅读9分钟

wait sleep 区别

wait()是Object的方法,调用会放弃对象锁,进入等待队列,待调用notify()/notifyAll()唤醒指定的线程或者所有线程,才会进入锁池,竞争同步锁,进而得到执行.

sleep()是线程线程类(Thread)的方法,调用会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复

引用类型

1、强引用 强引用指向的对象不允许被回收。强引用其实也就是我们平时A a = new A()这个意思

2、软引用 堆内存不够用时会被回收

3、弱引用 下次垃圾回收时,一定被回收

4、虚引用 不影响对象回收,对象回收的统计工作

HashMap HashTable区别

1、实现方式不同:Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类。Dictionary 是 JDK 1.0 添加的,貌似没人用过这个,我也没用过。

2、初始化容量不同:HashMap 的初始容量为16,Hashtable 初始容量为11,两者的负载因子默认都是0.75。

3、扩容机制不同:当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。

4、失败方式不同:HashMap 是 fail-fast 的,而 Hashtable 是fail-safe的。所以,当其他线程改变了HashMap 的结构,如:增加、删除元素,将会抛出ConcurrentModificationException 异常,而 Hashtable 则不会。

fail-fast:依靠modCount保证,集合元素个数发生改变,modCount会被改变,迭代时会比较modCount == expectedModCount ,不相等就抛出异常

fail-safe:集合结构被改变时,复制原集合的一部分数据,在复制的数据里做修改。所以他不能保证遍历的是最新的内容,并且需要额外的开销

HashMap 原理

1、数据结构

hash表(数组+链表)

key hash,定数组位置,equals找链表位置

2、初始size:满足2的幂

为什么这样设定?性能:可以用位运算使用hash定数组位置 tab[(n - 1) & hash]

3、hash方法实现

int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

4、 为什么这样设计?

将key的原生hashcode 低16位换成 低16位与高16位的位异或运算的结果,将高位的影响传播到地位,避免高位因为table长度限制无法参与到index的计算(tab[(n - 1) & hash])中,减少冲突。

5、为什么只做到这种程度?仅仅将高16位与低16位异或处理?

一些情况,这种处理是不需要的(hash低位已经有效区分),之所以简单处理,尽量避免在这种情况下的性能损失,是一种质量与性能的平衡的结果

6、扩容reSize

触发条件:size达到 容量*负载因子

结果: 新数组,不考虑极端的情况下:size*2

7、最佳实践

因为扩容非常消耗性能,所以应该预估用量,合理初始化容量,避免resize

HashMap高并发下的问题

1、扩容迁移可能造成数据丢失、数据覆盖

2、扩容迁移可能造成死链

1.8之前采用头插法,可能导致死链

1.8之后采用尾插法,不会改变链表的顺序,其实不会出现死链了

二叉树

1、二叉查找树

a. 左树节点<=根节点 右树节点>=根节点

b. 二分查找

c. 缺点可能一个树节点过长影响性能

2、红黑树(自平衡的二叉查找树)

a. 节点是红色或黑色。

b. 根节点是黑色。

c. 每个叶子节点都是黑色的空节点(NIL节点)。

d. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

e. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

f. 变色

g. 左旋:逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而自己成为自己的左孩子

h. 右旋转:顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子取代,而自己成为自己的右孩子

没有套路!不发私信!不加QQ!大厂面试题一:java基础篇

ConcurrentHashMap原理

1、1.7与1.8的实现与不同

1.7 Segment ReentrantLock 分段锁

1.8 抛弃segment,使用CAS+synchronized 实现同步,与HashMap一样,增加红黑树的结构,避免链表过长导致的性能问题

2、扩容过程

分配数组段,链表&红黑树拆分,迁移

多线程并行迁移

3、segment put实现

没有套路!不发私信!不加QQ!大厂面试题一:java基础篇

4、CAS put实现

没有套路!不发私信!不加QQ!大厂面试题一:java基础篇

Synchronized原理

依赖monitor对象进行加锁

加锁时:count+1,其他线程进入entry_list等待

wait时:count-1,该线程进入wait_set等待被唤醒

结束时:count-1,entry_list线程可以继续获取锁

对象头:hashcode\锁信息\分代年龄\GC标志

Synchronized锁优化

JVM 使用了锁升级的优化方式,先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会升级为重量级锁。

自旋锁:为了不转入内核态挂起和恢复线程,线程自旋等待,自旋时间可设置

自适应自旋锁:自旋的时间不再固定

锁消除:同步的代码,不可能存在共享数据竞争,无需加锁

锁粗化:锁同步范围扩大

轻量级锁:偏向锁失败,<=2个线程竞争锁

偏向锁:锁总是由同一个线程多次获得,锁ID记录在对象头部,再次请求时,直接获得锁

存储内容

标志位

状态

对象哈希码、分代年龄

01

未锁定

指向锁记录的指针

00

轻量级锁定

指向重量级锁的指针

10

重量级锁定

空,不需要记录信息

11

GC标记

偏向线程ID\时间戳、分代年龄

01

可偏向

没有套路!不发私信!不加QQ!大厂面试题一:java基础篇

ReentrantLock原理\AQS实现原理

state变量 -> CAS -> 失败后进入队列等待 -> 释放锁后唤醒

abstract queue synchronizer 抽象队列同步器

ReentrantLock lock = new ReentrantLock(); //传参true false 公平锁 或非公平锁

没有套路!不发私信!不加QQ!大厂面试题一:java基础篇

CAS原理

CompareAndSwap

通过硬件把两个操作 实现为一个原子性操作

没有套路!不发私信!不加QQ!大厂面试题一:java基础篇

CAS的缺点:

  1. 循环时间长开销很大:getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销

  2. 只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性

3. ABA问题:果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。(解决办法再加个版本号或者时间戳)

CountDownLatch

AQS实现,volatile 变量 state 维持倒数状态,多线程共享变量可见

1、主线程调用CountDownLatch.await()时 会尝试获得锁 获取成功的标志是 state=0(该state 初始化CountDownLatch时传入的)

2、如果获取失败就会进入同步队列 阻塞

3、其它线程 调用CountDownLatch.countDown时,会尝试释放锁,释放成功的条件是 state由1变成0,这时会唤醒同步队列中的主线程

4、主线程醒来后再循环中继续尝试获取锁,这时候state已经等于0,获取锁成功,返回await方法返回,主线程继续执行

CyclicBarrier

可重用栅栏

用于当前线程需要等待其他线程操作完成后继续操作,内部维护一个count标志需要同步的线程数,每个线程调用CycliecBarrier.await方法时都会--count,如果count!= 0 执行condition.awiat 进入等待队列,所以最后一个线程执行时 count=0,会唤醒所有阻塞线程

Semaphore

Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可,内部类 Sync extends AbstractQueuedSynchronizer设置 state为 信号量数量,在acquire 和 release时 维护数量,acquire时 remaining = available - acquires remaining<0 则进入同步队列 阻塞,release时唤醒同步队列中的阻塞线程

线程池实现原理

1、最大线程数maximumPoolSize

2、核心线程数corePoolSize

3、活跃时间keepAliveTime

4、阻塞队列workQueue

5、拒绝策略RejectedExecutionHandler

  • AbortPolicy -- 当任务添加到线程池中被拒绝时,它将抛出RejectedExecutionException 异常。

  • CallerRunsPolicy -- 当任务添加到线程池中被拒绝时,会在线程池当前正在运行的Thread线程池中处理被拒绝的任务。

  • DiscardOldestPolicy -- 当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中。

  • DiscardPolicy -- 当任务添加到线程池中被拒绝时,线程池将丢弃被拒绝的任务。

6、流程

a. 当任务提交,线程池创建根据corePoolSize大小创建若干任务数量线程执行任务

b. 当任务超过corePoolSize数量,任务进入阻塞队列

c. 当阻塞队列满之后,继续根据任务数量创建不超过maximumPoolSize-corePoolSize线程来执行任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后自动销毁

d. 如果达到maximumPoolSize,阻塞队列还是满的状态,使用拒绝策略

ThreadLocal原理

1、ThreadLocal数据结构

  • Thread对象持有一个成员变量 ThreadLocal.ThreadLocalMap threadLocals。

  • ThreadLocalMap并不是一个常规的Map 它包含了 一个 Entry的数组,private Entry[] table。

  • Entry本身是一个弱引用,指向ThreadLocal对象,又有个value成员变量 Entry extends WeakReference<ThreadLocal<?>> Object value。

  • Entry具备保存一个key value键值对的能力,Thread成员变量 ThreadLocalMap, ThreadLocalMap 包含了一组键值对,键为ThreadLocal对象,值为set时候的值。

2、ThreadLocal这样设计的好处?

相比synchronized ,threadlocal每个线程保存自己的本地变量的副本,使用空间换时间

3、内存泄漏问题

如果key指向threadlocal的弱引用被回收了,那么entry就存在key为null的map,这个map一直被线程强引用,除非线程结束,否则一直无法被回收也无法被访问

没有套路!不发私信!不加QQ!大厂面试题一:java基础篇

4、父子线程间的传递(InheritableThreadLocal)

Thread类内部维护一个inheritableThreadLocals变量,Thread初始化的时候,判断父线程中inheritableThreadLocals中有无线程本地变量,有则复制到该线程的inheritableThreadLocals变量中

5、使用线程池时候获取父线程ThreadLocal

Transmittable ThreadLocal(阿里开源)

BIO/NIO/AIO区别以及原理

1、同步 :两个同步任务相互依赖,并且一个任务必须以依赖于另一任务的某种方式执行。比如在A=>B事件模型中,你需要先完成 A 才能执行B。再换句话说,同步调用被调用者未处理完请求之前,调用不返回,调用者会一直等待结果的返回。

2、异步:两个异步的任务完全独立的,一方的执行不需要等待另外一方的执行。再换句话说,异步调用一调用就返回结果不需要等待结果返回,当结果返回的时候通过回调函数或者其他方式拿着结果再做相关事情。

3、阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。

4、非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

5、NIO:同步非阻塞

通过selector开启一个线程轮询channel是否有读写事件

Channel通道

Buffer缓冲区

Selector选择器

6、AIO:异步非阻塞

new一个对象的过程

1、先检测类是否已经被加载

类加载机制

a. 加载:字节码加载成二进制

b. 验证:格式验证,语义验证,操作验证

c. 准备:静态变量赋值、分配内存空间

d. 解析

e. 初始化(先父后子):初始化static代码块(线程安全)

2、创建对象

分配对象需要内存:本类和父类的所有实例变量

实例变量赋默认值:方法区实例变量定义拷贝到堆区,赋值

执行初始化代码:先初始化父类再子类