JAVA春招实习八股文 2024年3月
- Java并发
- 单例模式
- 使用多线程可能存在的问题
- 进程和线程的区别
- java 如何开启多线程
- volatile 作用
- 什么是CountDownLatch、CylicBarrier、Semaphore?
- 多个线程同时执行?依次执行?交替执行?
- 如何对一个很长的字符串快速排序
- 什么是AQS
- 什么是悲观锁,乐观锁
- 独占锁(排他锁)
- 共享锁
- 公平锁和非公平锁
- 分段锁
- 优化锁的操作
- 轻量级锁
- 轻量级锁中CAS的细节!!CAS的精髓
- MarkWord
- 什么是自旋CAS
- 锁粗化
- 适应性自旋
- Synchronized和Lock的区别
- 线程安全方案
- happens before
- happens before 操作一定是时间上的先行吗?
- happens before 和 as-if-serial
- Synchronized的理解(精通)
- 什么是可重入锁,有什么特点
- Java线程锁机制是怎么样的?(synchronized的实现原理)(偏向锁,轻量锁,重量锁)
- 对象头、markWord
- 为什么ThreadLocaMap不用thread
- ThreadLocal是什么
- ThreadLocal弱引用是如何解决内存泄漏问题的
- Redis
- IO模型
- 操作系统
- 计算机网络
- JVM
- Spring
- 数据库
- 集合
- java基础
- 重载和重写的区别
- String为什么不可变的
- StringBuffer StringBuilder
- java语言的特点
- "=="和equals方法究竟有什么区别?
- String s1 = new String("abc");这句话创建了几个字符串对象?
- 避免多个字符串对象拼接
- 自动装箱和拆箱
- 如何选用集合?
- 为什么构造函数不能重写
- Integer与int的区别
- ceil、floor、round
- 强引用、弱引用
- JVM、JRE、JDK
- java和c++的区别
- java基本数据类型
- 为什么要有包装类型.
- 自动拆箱引起的NPE问题
- 为什么说 Java 语言“编译与解释并存”?
- 引用拷贝、浅拷贝、深拷贝
- 为什么重写equals同时要重写hashcode
- Timer、TimeTask
- 遇到过哪些异常
- 用过哪些注解
- Java8新特性
更新至2024年3月15日,每周更新补全。
Java并发
单例模式
- 饿汉式,使用静态代码块私有构造函数的方法,在类加载时候就创建实例,而不是按需创建。
- 懒汉式,由线程手动去创建,但是有可能存在重复创建的线程安全问题。
- 双检索,解决懒汉式的线程安全问题。外部检查是为了防止每次尝试创建对象都进行加锁,加锁和内部检查是为了解决并发问题。
- volatile修饰实例,来防止指令重排序导致的线程问题。(给实例分配了地址,由于指令重排导致还没完成初始化,但是其他线程发现实例不为空,就取走了未初始化的实例)
使用多线程可能存在的问题
- 线程安全问题
- 死锁、饥饿问题
- 同步导致的性能开销(上下文切换)的开销
进程和线程的区别
- 进程是操作系统进行资源分配的最小单元
- 线程是CPU调度的最小单元
java 如何开启多线程
- 继承Thread类,重写run方法。(继承的缺点是单继承)
- 实现Runable接口,实现run方法。(接口的优点是一个类可以实现多个接口)
- 实现Callable接口,通过FutureTask创建线程,获取返回值
volatile 作用
- 线程可见性,被volatile修饰的变量在线程中被修改时会刷新到主线程中。(synchronize和lock也是可以保证可见性的,释放锁之前会将变量刷回至主存。)
- 禁止指令重排。参考双检锁实现的单例模式,防止对象初始化的时候因为指令重排导致的线程安全问题。
什么是CountDownLatch、CylicBarrier、Semaphore?
- CountDownLatch,
- 某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为
new CountDownLatch(n)
,每当一个任务线程执行完毕,就将计数器减1countdownLatch.countDown()
,当计数器的值变为0时,在CountDownLatch上await()
的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。注意:(countdownLatch.countDown()`不会阻塞线程,线程会继续执行后续的代码。)(3把枪都响了,你才能开始跑) - 情景2:多个线程在指定条件下同时开始执行。做法是初始化一个共享的
CountDownLatch(1)
,将其计算器初始化为1,多个线程在开始执行任务前首先countdownlatch.await()
,当主线程调用countDown()
时,计数器变为0,多个线程同时被唤醒。(跑步比赛,所有人就位,等枪响才能跑) - 一次性使用,不能重复使用
- 某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为
- CylicBarrier,
new CyclicBarrier(10)
,当第10个线程都调用了CyclicBarrier的await阻塞等待时,才同时开始执行。可以重复使用,俗称循环屏障。(跑步比赛,所有人就位就能跑) - Semaphore,可以初始化n个信号,最多同时有n个线程取得了信号量开始执行,其他进程需要等待信号释放。(十个接力棒,拿到就能跑)
多个线程同时执行?依次执行?交替执行?
- 同时执行用CountDownLatch
- 多个线程依次执行,采用共享的volatile变量作为信号,一个线程执行完之后才把变量更改为下一个线程所需的信号。如初始化volatile变量signal为1,当signal为1的时候线程1才向下执行,执行结束才改signal为2。(signal为2的时候线程2才开始执行)(线程要轮询voltile变量,占用CPU)
- .多个线程交替执行采用Semaphore对象a,b,c作为信号量,初始化a可获取,bc不可获取。当一个线程获取a开始执行,结束后才释放下一个线程需要的信号量b,未得到所需信号量的线程阻塞等待。相比于第二个方法,Semaphore可以阻塞未得到信号量的线程,提高cpu利用率。
如何对一个很长的字符串快速排序
- Fork/Join框架,分治思想。(归并排序的多线程实现)
什么是AQS
- AQS是jdk中的lock锁工具类的底层核心类
什么是悲观锁,乐观锁
- 悲观锁就是线程执行代码前一定要加锁,不然就会有人抢占资源。
- 乐观锁执行的时候默认没人抢占资源,不上锁。CAS(比较和交换),就是乐观锁的思想。通过比较预估值和旧值,判断数据是否被人修改了,如果没有被修改,自己再进行修改(修改)。CAS是底层实现的一个原子操作。
独占锁(排他锁)
- JDK中的synchronized和java.util.concurrent(JUC)包中Lock的实现类就是独占锁。
- 读写锁中的写锁也是独占锁。
共享锁
- 共享锁是指锁可被多个线程所持有。
- 在 JDK 中 ReentrantReadWriteLock 的读锁就是一种共享锁,读锁可共享,多个进程只读不写。
公平锁和非公平锁
- 非公平锁就是进程不一定按申请顺序获得锁
- 在 java 中 synchronized 关键字是非公平锁,ReentrantLock默认也是非公平锁,但是可以有公平锁的实现。
分段锁
分段锁不是具体的锁,是一种设计,只是锁住数据结构的一部分内容。
在 Jdk1.7中 ConcurrentHashMap 底层就用了分段锁,使用Segment,提供并发性。
优化锁的操作
锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术可以减少锁操作的开销
轻量级锁
- JVM在执行当前线程时,首先会在当前线程栈帧中创建锁记录Lock Record的空间用于存储锁对象目前的Mark Word的拷贝。如果当前对象没有被锁定,那么锁标志位为01状态。(锁标记位位于对象头的Mark Word区域)
- 然后,虚拟机使用CAS操作将对象Mark Word拷贝到锁记录中,并且试图将Mark Word更新为指向Lock Record的指针。
- 如果更新成功了,那么这个线程就拥用了该对象的锁,并且对象Mark Word的锁标志位更新为(Mark Word中最后的2bit)00,即表示此对象处于轻量级锁定状态
- 如果这个更新操作失败,JVM会检查当前的Mark Word中是否存在指向当前线程的栈帧的指针,如果有,说明该锁已经被获取,可以直接调用。(由于重入导致的更新失败,此时重入次数+1)
- 如果没有,则说明该锁被其他线程抢占了,如果有两条以上的线程竞争同一个锁,那轻量级锁就不再有效,直接膨胀为重量级锁,没有获得锁的线程会被阻塞。
- 此时,锁的标志位为10.Mark Word中存储的指向重量级锁的指针。 轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头中,如果成功,则表示没有发生竞争关系。如果失败,表示当前锁存在竞争关系。锁就会膨胀成重量级锁。
轻量级锁中CAS的细节!!CAS的精髓
加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁已经升级为重量级锁了,要通过重量级解锁的步骤进行解锁。
为什么加锁前要拷贝对象的markword到线程栈区的锁记录中
因为线程获取锁对象成功后,锁对象的markword将用一块区域存放指向线程锁记录的指针,从而丢失一部分信息。提前拷贝markword是为了释放锁时候进行还原。
MarkWord
- 对象头由markword和指向类的指针组成
- markword中存放了锁的标记位,GC age,偏向锁标记等
什么是自旋CAS
- 内存值(V)—>内存实际值;
- 预估值(A)—>读取到的内存中的值
- 更新值(B)—>需要更新到V的值;
- 每次在进行更新操作时,当且仅当V==A(内存值和读取值相等时,表示没人更改过数据),后 V=B(将内存值更新为B)
- 举例子,a线程要对值为1的内存V进行+1操作,首先读取到内存值为1,即A=1 ; 如果此刻b线程对V完成了V+1操作,那么a线程对V进行更新的时候就会发现A!=V,这个时候a线程就知道有人改变过数据了,a要重置预估值准备下一次更新。
- ABA 问题,数据有可能被多次更改,改回了旧值,导致V仍然等于A。线程就无法发现数据是否发生了改变。(通过添加一个相当于版本号的标记变量解决,每次不仅要比较A和V,还要比较版本号前后是否一致)
- 自旋只能保证单个变量的原子性,如需保证多个变量的原子性,可以把多个变量封装到一个AtomicReference 类对象实现。
锁粗化
以下代码会进行多次连续的加锁解锁操作,JIT在运行时会将他们粗化在一个锁操作进行
public static String test04(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
适应性自旋
- 如果在同一个锁对象上,有线程刚刚通过自旋成功获取过锁,并且持有锁的线程正在运行中,那么JVM会认为该锁通过自旋获取到锁的可能性很大,会自动增加等待时间。比如增加到100此循环。
- 相反,如果对于某个锁,线程通过自旋很少成功获取锁。那再以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。
- 一个线程通过自旋
Synchronized和Lock的区别
- 实现方式:Synchronized是Java语言内置的关键字,而Lock是一个Java接口。
- 锁的获取和释放:Synchronized是隐式获取和释放锁,由Java虚拟机自动完成;而Lock需要显式地调用lock()方法获取锁,并且必须在finally块中调用unlock()方法来释放锁。
- 可中断性:在获取锁的过程中,如果线程被中断,synchronized会抛出InterruptedException异常并且自动释放锁,而Lock则需要手动捕获异常并处理释放锁。
- 锁状态:Synchronized无法判断锁的状态,而Lock可以通过tryLock()、isLocked()来判断锁的状态(线程是否可能取到锁、锁是否被占用等)。
- 场景:如果在简单的并发场景下,推荐使用Synchronized;而在需要更高级的锁控制时,可以考虑使用Lock。
线程安全方案
- 互斥同步: synchronized 和 ReentrantLock
- 非阻塞同步: CAS, AtomicXXXX
- 无同步方案: 栈封闭(对象的引用被线程私有),本地存储(Thread Local),可重入代码(final)
happens before
Java 内存模型下一共有 8 条 happens-before 规则,如果线程间的操作无法从如下几个规则推导出来,那么它们的操作就没有顺序性保障,虚拟机或者操作系统就能随意地进行重排序,从而可能会发生并发安全问题。
- 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。(同一线程需满足)
- 管程锁定规则(Monitor Lock Rule):一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。这里必须强调的是同一个锁,而 “后面” 是指时间上的先后顺序。(同一个锁需满足)
- volatile 变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作,这里的 “后面” 同样是指时间上的先后顺序。(对volatile变量操作需满足)
- 线程启动规则(Thread Start Rule):Thread 对象的 start () 方法先行发生于此线程的每一个动作。
- 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过 Thread.join () 方法结束、Thread.isAlive () 的返回值等手段检测到线程已经终止执行。
- 线程中断规则(Thread Interruption Rule):对线程 interrupt () 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted () 方法检测到是否有中断发生。
- 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize () 方法的开始。
- 传递性(Transitivity):如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那就可以得出操作 A 先行发生于操作 C 的结论。
happens before 操作一定是时间上的先行吗?
不一定,不如在同一线程中,happens before需要确保程序由控制流顺序执行,但是由于指令重排,局部顺序可能发生变化。
happens before 和 as-if-serial
- happens before是虚拟机需要遵守的多线程之间的顺序原则
- as-if-serial主要是面向单线程的顺序规则,保证指令重排不影响程序语义。
Synchronized的理解(精通)
- 一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;
- 每个实例都对应有自己的一把锁(this),不同实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象公用同一把锁
- synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁
什么是可重入锁,有什么特点
- 可重入锁是指锁住的代码块内部,可以再次调用该对象的锁,但是需要记得释放次数+1
- 允许一个线程在多次获取同一个锁的时候,不会造成死锁。
Java线程锁机制是怎么样的?(synchronized的实现原理)(偏向锁,轻量锁,重量锁)
- Java的锁就是对象头的MarkWord中记录的一个锁状态
- 偏向锁线程第一次访问锁,锁指向了该线程。第二次线程再访问该锁时,不会触发同步,减少开销。
- 轻量级锁,出现竞争时,偏向锁升级为轻量锁。进程靠自旋操作尝试获取锁,消耗cpu资源。
- 重量级锁,竞争加剧时,操作系统调度各个进程获取锁的顺序。(升级到重量级锁需要等到全局安全点)
- jdk1.6之前均为重量级锁。在竞争较小的情况,更适合轻量级锁。在没有竞争的情况,更适合偏向锁。
对象头、markWord
- 对象头有两个区域,一个是存放(GC age、HashCode、锁标记位、偏向线程ID)的markWord
- 另一个是类型指针存指向方法区的类型信息,也就是这个对象的类型信息(属于哪个类)。
为什么ThreadLocaMap不用thread
ThreadLocal是什么
- ThreadLocal是用于线程绑定私有变量的,虽然可以用加锁方式实现,但是加锁会造成开销。(存储私有变量+线程安全)
- 线程内部可以随时调用ThreadLocal绑定的变量,避免传递参数,降低代码的耦合度。(降低耦合)
ThreadLocal弱引用是如何解决内存泄漏问题的
- 当前线程维护一个ThreadLocalMap,其中有一个个Entry对象。内存泄漏就可能发生在Entry对象当中。
- 在Entry中,key指向ThreadLocal,如果这个指向是强引用,即使ThreadLocal的栈区引用释放了,ThreadLocal的实际内存也不会释放。如果是弱引用,则ThreadLocal的栈区引用释放了,ThreadLocal的实际内存也释放了。Key将指向null。
- 在调用ThreadLocal的set、get方法时,如果判断Key指向null,则会释放对应的value,避免内存泄漏。
- 以上只是减缓内存泄漏风险,ThreadLocal使用完毕后,应该主动remove释放内存,防止内存泄漏。
Redis
缓存击穿、缓存穿透和缓存雪崩
- 缓存击穿是指1个热点key突然过期/失效,导致大量请求到达数据库。
- 互斥锁解决,拿到锁的才能访问数据库
- 不设置过期,设置合理的淘汰策略
- 缓存雪崩是指大量热点key同时过期/失效/未生效。
- 缓存预热解决
- 设置不同过期时间
- 缓存穿透恶意传入大量不存在的key
- 返回null的缓存
- 布隆过滤器(hash冲突,布隆说不存在肯定不存在,说存在可能不存在)
Redis淘汰策略
- 可指定淘汰范围:所有key or 设置了过期时间的key.
- 先进先出
- 最近最久未使用(按最长时间未使用淘汰)
- 删除即将过期
- 最不经常使用(按频率最小淘汰)
Redis过期删除策略
- 惰性删除:命中key,发现过期,则删除
- 定期删除:定期检查所有key,删除过期key
- Redis结合以上两种策略。
修改数据时,缓存和数据库如何保持一致性
- 延迟双删,a先删缓存,b再删数据库,c随后删缓存。
- 缺少c,会导致ab操作之间,有其他请求命中数据库,返回未更新的数据并保存至缓存中。
Redis持久化机制?
1.RDB:指定的时间间隔内将内存中的数据集快照写入磁盘
2.AOF:将Redis执行过的所有写指令记录下来(读操作不记录),Redis启动之初会读取该文件重新构建数据
Redis支持哪些数据类型
- String、List、Hash、Set、Z-Set
- Z-Set在数据量少或者数据长度短的时候,可采用压缩列表而不是跳表来实现。
Redis为什么使用跳表(Z-Set),不用b+树。
- Redis操作内存中的数据,而数据库的磁盘IO比较耗时。b+树更矮,一般只需要三层,可以进行更少的IO操作。
- Z-Set结构简单,双向链表+多级索引,构建和维护的成本较低。
Redis是单线程为什么还这么快?
- 读写数据都是在内存中完成
- 采用了IO多路复用技术和非阻塞IO
- 避免了多线程间的竞争、同步、上下文切换的开销
如何用redis实现分布式锁?需要注意什么?
- 使用的是SETNX命令(SET if Not eXists)
- 防止客户端带着锁结束,导致死锁。为锁设置过期时间。
- 防止运行中的客户端的锁过期,可以设置定时延长。
IO模型
什么是阻塞IO和非阻塞IO
- IO是之进程对操作系统发起的读写过程,
- 阻塞IO是指进程在发起IO后,阻塞等待系统返回IO结果。
- 非阻塞IO是指进程在发起IO后,继续执行之后的代码,通过轮询和回调的方式获取操作系统的IO结果。
什么是BIO(同步阻塞IO)
- 以读事件为例,进程发起读操作后,若内核缓冲区没有所需数据
- 第一阶段,阻塞等待直到内核缓冲区数据准备就绪。
- 第二阶段,阻塞等待内核数据拷贝到用户缓冲区。
什么是NIO(同步阻塞IO)
- 以读事件为例,进程发起读操作后,若内核缓冲区没有所需数据
- 第一阶段,不等待内核缓冲区数据准备就绪,先做自己的事情,直到内核缓冲区数据准备就绪。
- 第二阶段,阻塞等待内核数据拷贝到用户缓冲区。
什么是select IO多路复用
- BIO和NIO是单个进程进行读写事件的过程。
- Select IO多路复用是指Selcet同时管理多个需要进行读写的进程,把他们的文件描述符列表fd_set传给内核态,内核态将有读写事件发生的文件描述符进行标记,再通过select返回给进程。
- 这样一批的进程就能得到读写事件是否就绪的消息。虽然select的过程是阻塞的,但是避免了单个线程发起用户态到内核态的切换。
- fd_set在用户态和内核态的拷贝,影响了性能,在epoll中得到解决。
什么是poll/epoll IO多路复用
- select fd_set的大小是1024/2048(取决系统是32位/64位)
- poll和select最明显的区别是采用结构体数组poll_fd存储文件描述符列表,没有大小限制。
- epoll原理比较复杂,晚点补充。
操作系统
CPU数量为1的进程执行死循环会发生什么?如何解决?
- 系统资源被该进程独占,其他进程无法获得CPU时间片,系统可能会变得不响应或变得非常缓慢。
计算机网络
粘包和拆包
- 粘包就是发送缓存剩余空间较大,tcp协议将多个小消息合并成一个消息进行传输
- 拆包就是TCP发送缓存不够大,把一个大的消息拆分成多个小消息传输。
粘包和拆包解决方案
- 固定长度(不足补零)
- 约定分隔符
- 自定义消息结构体,包含长度信息。(发送方和接收方制定协议)
http协议状态码
| 状态码 | 类别 | 含义 |
| ------ | ------------- | ---------------------------------------------------- |
| 1** | 信息 | 服务器收到请求,需要请求者继续执行操作 |
| 2** | 成功 | 操作被成功接收并处理 |
| 3** | 重定向 | 需要进一步的操作以完成请求 |
| 4** | 客户端错误 | 请求包含语法错误或无法完成请求 |
| 5** | 服务器错误 | 服务器在处理请求的过程中发生了错误 |
301 和 302的区别(腾讯实习题)
301 永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替
302 临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI
TCP报文请求头
TCP
- 面向连接、可靠、基于字节流
- 效率低,准确高,适用于文件传输(不允许数据丢失)
UDP
- 基于报文、不可靠的
- 效率高,准确低,适用于即时通信(不怕数据丢失)
TCP三次握手
- a客户端发出请求
- b服务端回应请求(知道你要断开连接了)
- c客户端确认服务端的回应(我收到你的信息了,不用怕信息丢失)
- 如果缺少步骤c,服务端就不知道自己发送的信息是否被客户端接收。
TCP四次挥手
- a客户端发出断开请求
- b服务端回应请求(知道你要断开连接了,等我我发完最后的东西)
- c服务端发出断开请求(我发完了,可以断开了)
- d客户端回复服务端(我收到你的信息了,不用怕信息丢失)
JVM
类加载过程
- 加载 , 把字节码中的类信息加载到方法区,并在堆区创建一个类对象,作为对类的访问入口
- 验证 ,这一步为了验证加载到的类信息是否有错误
- 准备 ,为类的静态变量分配内存空间,并赋默认值。(static int value = 3),此时value值为0.(区别于初始化)
- 解析,把符号引用转化为直接引用(this字面量、方法和字段等指向实际内存地址或者偏移量)
- 初始化,为类变量赋予显式值(static int value = 3),此时value值为3.
什么是即时编译JIT(Just in Time)
- 传统JVM解析器执行Java程序是先通过javac对其进行源码编译然后转为字节码文件,然后再通过解释字节码转为机器指令一条条读取翻译的。
- Java程序经过编译再执行的话,执行速度必然比直接执行要慢很多
- 而HotSpot虚拟机针对这种场景进行了优化,引进了JIT即时编译技术。
- JIT技术的引入不会影响原本JVM编译执行,只是当发现某个方法或者代码块运行特别频繁时会将其标记为热点代码。然后会将其直接编译为本地机器相关的机器码并优化,最后将这部分代码缓存起来。
- JIT除了具有热点缓存的功能外,还会对代码做各种优化,包括:逃逸分析、 锁消除、 锁粗化、 方法内联、 空值检查消除、 类型检测消除、 公共子表达式消除
- JIT是HotSpot JVM的一项技术,Hotspot JVM是JVM规范的一个实现。
逃逸分析
- ****逃逸分析的核心思想就是分析对象动态作用域
- 方法逃逸,当一个对象在方法中被定义后,它可能被外部方法所引用
- 线程逃逸,某些情况还需要被外部线程访问
- 如果JIT逃逸分析得出一个对象不会发生逃逸,则可能进行同步省略、标量替换、栈上分配的优化
标量替换、栈上分配、同步消除
- 变相实现栈上分配的一个方法
- 如果一个未发生逃逸的对象中只含有基本类型属性,则可能使用这些基本类型属性去代替整个对象,避免在堆区占用内存。
- 同步消除(锁消除)是将一些不必要的(没发生竞争的)锁删除。
- 锁粗化,是将紧密相连的同步块合并成一个大的同步块,避免用户态内核态切换之间产生的开销。
JVM内存模型
- 栈区 :存储java函数调用时的临时变量、对象地址。函数执行后,栈会依次清空内部(先进后出)。
- 本地方法栈 :本地方法即native修饰的非java方法,包括著名的CAS
- 程序计数器:记录程序运行的位置,为了保证多线程切换。
- 方法区(元空间): 存储静态方法、静态变量、全局变量
- 堆区 : 存储对象(GC所要处理的区域)
JVM垃圾回收机制
- 通过对象的引用次数去回收,对象引用次数为0时回收。(特殊情况:两个对象之间存在互相引用,导致无法自动回收,通过GCRoot解决)
- GCRoot,检索栈、本地方法栈、方法区中的代码,没有被以上区域直接/间接引用的对象将被回收。
- 标记清理:需要回收的对象打上标记,再做清理–>产生内存碎片。CMS垃圾收集器的基本思想,用于老年代。(Stop the World)
- 标记整理:需要回收的对象打上标记,整理后再做清理
- 复制清理:不需要回收的对象拷贝到新空间,释放原空间。ParNew垃圾收集器的基本思想,用于新生代。
JVM垃圾回收过程
- 对象首选存储E区(Eden 亚当、伊甸园)
- (大小比例) S0:S1:E = 1:1:8 , 老年代:新生代=1:1
- E区满了,则通过复制清理方式,拷贝剩存对象到S0区(Survivor)。E区再次满了,把S0区和E区通过复制清理,把幸存对象拷贝到S1区(S0和S1交替使用)
- 在新生代中(S0,S1,E),每次清理发生时,存活下来的对象age+1,age到达6时拷贝到老年代。(该对象存活率高,转移到老年代,减少拷贝开销)
- 老年代不仅存幸存率高的,也存体积大的,减少拷贝开销。
- 老年代满的时候触发,STW(Stop the World),java程序暂停,全力做清理。
Spring
@Transactional原理
@Transactional 是 Spring 框架中用于声明事务性操作的注解。当一个方法被标记为 @Transactional 时,Spring 将使用AOP动态代理在方法执行前后开启和提交事务,保证操作的原子性。
反射的优点
- 灵活性和动态性:反射提供了一种在运行时而非编译时对 Java 程序进行操作和查询的能力。这意味着你可以编写更加灵活和动态的代码,可以在运行时加载、探索和使用完全未知的类。
- 框架开发:反射是很多 Java 框架背后的核心机制,包括 Spring 和 Hibernate。这些框架通过反射来实现依赖注入、ORM、事务管理等功能,极大地简化了应用程序的开发。
- 调试和测试工具:反射可以用来开发 IDE 插件、测试工具等,这些工具可以在运行时检查对象的状态,帮助开发者调试和测试代码。
反射的缺点
- 性能开销:反射操作相对于直接代码调用来说,要慢很多。因为反射涉及到类型解析、动态调度等操作,这些都需要在运行时执行,增加了额外的开销。
- 安全问题:使用反射可以访问类的私有成员和方法,这可能会破坏封装,增加安全风险。
- 代码复杂度:过度使用反射可能会使代码难以理解和维护,特别是对于不熟悉反射的开发者来说。
反射在框架中的应用:
- 依赖注入:框架可以在运行时动态地将依赖对象注入到其他对象中,无需显式地在代码中创建或配置它们。
- 动态代理:反射允许动态创建代理对象,用于实现面向切面的编程(AOP),例如,自动事务管理、日志记录等。
- 注解处理:框架通过反射读取注解,根据注解自动配置组件或执行相应的逻辑,这简化了配置和引导过程。
数据库
索引失效的情况
- 联合索引abc三列, where中未用或者跳过左列索引。如ac,bc。
- 查询列中没有使用索引,如建立的是列b的索引,但是查询条件中没有列b。
- like模糊查询以%开头
- 对索引使用了函数表达式,abs()
- or 的左边或右边无索引(or连接的查询条件必须全部建立索引)
- 避免对索引进行范围查询(大于、小于:效率不高)
- 发生了类型转换,字符索引未加双引号。
什么是聚族索引,非聚族索引和覆盖索引?
- 聚族索引通常是主键建立的索引,叶子节点保存的是所有列,可能包含不需要的列数据,不一定是覆盖索引。
- 非聚族索引(二级索引)的叶子节点保存的是主键。
- 覆盖索引指该索引返回的列就是查询的列,不需要回表,且不包含不需要查询的列。
什么是回表查询
1.二级索引的查找结果指向主键,需要其他列的数据时,需要通过覆盖索引(也就是主键索引)再次进行查询,这个过程叫回表查询。
事务有哪些原则
- Atomicity 原子性 (失败需要回滚,同时成功,同时失败)
- Consistency 一致性 (数据库的改变是合理的)
- Isolation 隔离性 (通过隔离级别控制)
- Durability 持久性 (事务完成后,结果持久保存)
脏读、不可重复读和幻读
- 脏读:读到其他事务未提交的数据(读未提交引起,用读已提交解决)
- 不可重复度:一个事务中两次读取的结果不同。(用可重复读解决)
- 幻读:读取时数据不存在,插入时数据存在。(可重复读引起,串行化解决)
集合
fail-fast机制
- 单线程的fail-fast发生在遍历时候调用集合的增加或删除等影响集合个数的修改操作。
- 多线程的fail-fast发生在一个线程遍历集合的时候,另一个线程影响了集合的元素个数。
- 集合内部为了一个一个mod值,当遍历中对其进行增加或删除时,会导致mod值的改变,出现异常。
- 应该使用迭代器的remove方式而不是集合的remove方式,因为迭代器的remove方式在删除元素后,对mod值进行了一个复原。
LinkedHashMap和HashMap
- 在遍历HashMap时无法得到确定的顺序,而LinkedHashMap遍历时是根据插入时间的先后顺序实现的。利用这点可以实现LRU缓存。
- treeMap 是红黑树实现的一个存储键值对数据结构,可以根据自然顺序或者构造器顺序进行排序。
- 从顺序来讲,treeMap(构造器顺序/自然顺序)->linkedhashmap(插入顺序)->hashmap(无序)
- 从增删改查速度来讲,hashmap、linkedhashmap是O(1),treeMap是O(logn)
为什么用cocurrentHashMap代替HashTable
- HashTable在增删改的时候锁住了整个对象,影响了效率。当两个线程要插入的元素key一样时,需要加锁。如果不一样,则不必加锁,加锁影响了效率。
ConcurrentHashMap中变量使用final和volatile修饰有什么用呢?
- final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。
- 使用volatile来保证某个变量内存的改变对其他线程即时可见,在配合CAS可以实现不加锁对并发操作的支持。
- get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。
ConcurrentHashMap的value为什么不能是null;
- 如果有一个key不存在于哈希表中,A线程get(key)得到null,不知道是value为null还是key不存在。此时通过containsKey去进一步判断,预期应该是返回false。而线程b如果此时有put(key,null)的行为,则containsKey可能返回true;
- Key也不能为null,这是因为作者觉得key为null不合理。
HashMap
- 容量需为2的指数倍,方便使用按位与来代替取余操作。
- 按位与的好处是可以在扩容的时候, 使得原来的元素均匀的分布在新的哈希表中。
容器的线程安全问题
- ArrayList线程不安全,Vector线程安全但是被弃用了,JUC下的CopyOnWriteArrayList用来实现线程安全。
- HashMap线程不安全,HashTable线程安全但是被弃用了,JUC下的coucrruntHashMap用来实现线程安全。
- JUC容器实现线程安全的原理,在遍历时拷贝一份数据进行,从而不受原数据被改变带来的影响。
java基础
重载和重写的区别
-
发生范围:
- 重载:发生在同一个类中,方法名相同但参数列表不同(参数类型、参数个数、参数顺序)。
- 重写:发生在子类和父类之间,子类重写(覆盖)了父类的方法,方法名、参数列表和返回类型必须相同。
-
参数列表:
- 重载:参数列表必须不同,可以通过参数的个数、类型或顺序进行区分。
- 重写:参数列表必须完全相同,包括参数类型、参数个数和参数顺序.
-
返回值类型:
- 重载:返回值类型可以相同也可以不同,但不能仅仅依靠返回值类型来区分方法.
- 重写:返回值类型必须相同,否则会编译错误.
-
异常:
- 重载:可以抛出不同的异常,但不能仅仅依靠异常来区分方法.
- 重写:子类方法抛出的异常不能超出父类方法抛出的异常范围,或者抛出更具体的异常.
-
访问修饰符:
- 重载:访问修饰符可以相同也可以不同.
- 重写:访问修饰符不能降低方法的可见性,可以提高方法的可见性(例如,父类方法是protected,子类方法可以是public).
-
发生阶段:
- 重载:发生在编译阶段,根据方法签名(方法名和参数列表)来确定调用哪个方法.
- 重写:发生在运行时,通过对象的实际类型来确定调用哪个方法.
总的来说,重载是在同一个类中方法名相同但参数列表不同,而重写是子类重写父类的方法,方法名、参数列表和返回类型必顇相同。重载是编译时多态,而重写是运行时多态.
String为什么不可变的
- String源码维护的是final修饰的char数组
StringBuffer StringBuilder
- StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
- StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
- 操作少量的数据: 适用 String (不需要频繁修改数据的时候用String)
- 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder(不加锁性能高)
- 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer (加锁保证安全)
java语言的特点
- 简单易学(没有指针,垃圾回收机制)
- 面向对象(封装继承多态)
- 跨平台(一套代码可以多个平台运行)
- 强大的生态
"=="和equals方法究竟有什么区别?
== 的作用:
基本类型:比较值是否相等
引用类型:比较内存地址值是否相等
equals 的作用:
引用类型:默认情况下,比较内存地址值是否相等。可以按照需求逻辑,重写对象的equals方法。)
Object类的底层equals就是==, 重写时可以先判断==,如果==不成立再进行其他判断。(==成立表示内存地址相同)
String a=new String("foo");
String b=new String("foo");
System.out.println(a==b);// false
String aa = "ab";
String bb = "ab";
System.out.println(aa==bb);// true
表达式a==b将返回false,a和b存放的地址不同,而这两个对象中的内容是相同的,所以,表达式a.equals(b)将返回true。
aa和bb指向的是字符串常量池同一块内存,aa和bb的值(内存地址)也是相同的。
equals() 是Object类的方法,如果没有像包装类一样重写equals()方法,默认也是比较内存地址。
对于基本数据类型来说,== 比较的是值。对于包装数据类型来说,== 比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 equals() 方法(对equals进行了重写,比较的是对象值而不是内存地址)。
String s1 = new String(“abc”);这句话创建了几个字符串对象?
- 会创建 1 或 2 个字符串对象。
- 如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。
避免多个字符串对象拼接
String a="aaa";
String b="bbb";
String c = a+b;
String不可变,a+b会创建一个Stringbuilder对象进行拼接再toString给c。需要大量改变字符串时应该直接使用Stringbuilder类。
自动装箱和拆箱
- 自动装箱通过调用对应包装类型的valueOf()方法将基本类型转换为包装类型对象。
- 自动拆箱通过调用包装类型对象的【如intValue()】方法将包装类型对象转换为基本类型值。
- 自动装箱时,如果需要创建的数据在缓存之内,则直接取缓存中的数据,而不会new新对象。
.
如何选用集合?
- 我们需要根据键值获取到元素值时就选用 Map 接口下的集合,需要排序时选择 TreeMap,不需要排序时就选择 HashMap,需要保证线程安全就选用 ConcurrentHashMap。
- 我们只需要存放元素值时,就选择实现Collection 接口的集合,需要保证元素唯一时选择实现 Set 接口的集合比如 TreeSet 或 HashSet,不需要就选择实现 List 接口的比如 ArrayList 或 LinkedList,然后再根据实现这些接口的集合的特点来选用。
为什么构造函数不能重写
- 构造函数要和类名一样,子类的类名跟父类的类名显然不能一样
Integer与int的区别
- Integer是int的封装类
- Integer默认未赋值,int默认0.(要想表达出没有参加考试和考试成绩为0的区别,则只能使用Integer。)
- Integer提供了相关静态方法,整数和字符串之间的转换(toString()、pasrInt()),比较两个Integer类型的大小(compare)等。.
ceil、floor、round
1.向上取整,向下取整,四舍五入。ps: round(-11.5)==-11
强引用、弱引用
- 强引用 new 出来的对象默认是强引用,对象有强引用就不会被GC清理。
- 弱引用,对象只剩下弱引用就会被清理。ThreadLoaclMap中的Key是弱引用,通过继承WeakReference实现
- 软引用,内存溢出(不够)的时候会被清理
- 虚引用,不会对内存造成影响,仅在内存被回收的时候会发出通知。
static class ThreadLocalMap {
// 定义一个Entry类,key是一个弱引用的ThreadLocal对象
// value是任意对象
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 省略其他
}
JVM、JRE、JDK
- JVM是运行 Java 字节码的虚拟机
- JRE是运行时环境,包括JVM和核心类库
- JDK是JAVA开发工具,包括JRE和编译器、调试器等开发工具。
- JIT是JVM上的运行时编译器,他会记录热点字节码的机器码,避免不断重复这个过程。
java和c++的区别
- 单继承和多继承
- 垃圾回收
- 指针
- 操作符重载(java有特殊情况 字符串拼接时候的+号是重载)
java基本数据类型
- 整数型 byte short int long 对应字节大小 1 2 4 8。
- 浮点型 float double 4 8
- 字符型 char 2
- 布尔型 boolean 只占1比特位
- 这八种基本类型都有对应的包装类分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean
- 基本数据类型的局部变量存在栈中,基本数据类型的成员变量存在堆中,包装类型属于对象,对象都在堆中。
为什么要有包装类型.
- 基本类型有默认值,不方便判断前端是否传入了这个数据
- 泛型参数不能是基本数据类型
- 包装类型实现了常量池技术
自动拆箱引起的NPE问题
- 数据库查询结果有可能是null,如果用基本数据类型接收,会发生空指针异常。
- 三目运算符使用不当会触发空指针异常。因为冒号两边数据类型不一致,编译器会先尝试将他们数据类型统一。
public static void quickTest1() {
Integer a = null;
Integer b = true ? a : 12;
System.out.println("b : " + b);
}
因为: Integer b = Integer.valueOf( true ? a.intValue() : 12 );
为什么说 Java 语言“编译与解释并存”?
- 编译型语言c++、go 编译之后就是机器能识别的语言
- 解释型语言通过一句句代码解析成机器指令交由机器执行,如python、javascript
- java是先编译成字节码,再由虚拟机进行解释。
引用拷贝、浅拷贝、深拷贝
为什么重写equals同时要重写hashcode
- 据Java规范,如果两个对象根据equals方法是相等的,那么它们的hashCode值必须相等。如果不重写hashCode方法,就有可能违反这个约定。
- hashCode方法用来提高基于哈希的集合的性能。如果hashCode方法不被正确重写,就不能正确的提高查询效率。
- 在使用基于哈希的集合(如HashMap、HashSet等)时,对象的hashCode值被用来确定对象在集合中的存储位置。如果你在重写equals方法后不重写hashCode方法,就会导致对象在集合中无法正确被定位,甚至会导致集合操作出现意外行为。
- 默认的hashcode是内存地址的哈希值,重写之后是值的哈希值。
Timer、TimeTask
是java的定时工具类,TimeTask是函数式接口,通过重写run方法定义定时任务的执行内容,Timer负责执行TimeTask实例。
public class MyScheduler {
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
// 在这里编写需要定时执行的任务
System.out.println("定时任务执行啦!");
}
};
// 设定定时任务,延迟0毫秒后执行,每隔1000毫秒执行一次
timer.schedule(task, 0, 1000);
}
}
遇到过哪些异常
1.空指针异常
2.java.lang.ClassNotFoundException
用过哪些注解
Java8新特性
函数式接口
- 有且仅有一个抽象方法的接口,可以用lamda表达式or匿名内部类作为该抽象方法的实现。
// before Java8
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello java8 without lambda");
}
}).start();
//Java8
new Thread(() -> System.out.println("hello java8 lambda")).start()
函数式接口 | 函数描述符 |
---|---|
Predicate | T->boolean |
Consumer | T->void |
Function<T,R> | T->R |
Supplier | () -> T |
UnaryOperator | T -> T |
BinaryOperator | (T,T)->T |
BiPredicate<L,R> | (L,R)->boolean |
BiConsumer<T,U> | (T,U)->void |
BiFunction<T,U,R> | (T,U)->R |
方法引用
方法引用主要有三类:
-
静态方法的方法引用
- valueOf是String类的静态方法,方法引用写为 String::valueOf,对应lambda表达式:a -> String.valueOf(a)
-
任意类型实例方法的方法引用
- length是String类的实例方法,方法引用写为 String::length,对应lambda表达式: (str) -> str.length()
// before students.sort((s1, s2) -> s1.getAge.compareTo(s2.getAge())))); // after 使用方法引用 students.sort(Comparator.comparing(Student::getAge()))));
-
现有对象的实例方法的方法引用
- 第三种容易与第二种混淆,现有对象指的是在lambda表达式中调用外部对象(不是入参对象)的实例方法,比如:
String str = "hello java8"; () -> str.length();
对应方法引用写为 str::length,注意不是 String::length
最后我们将三类方法引用归纳如下:
lambda表达式 | 方法引用 | 描述 |
---|---|---|
(args) -> ClassName.staticMethod(args) | ClassName::staticMethod | 静态方法方法引用 |
(arg0, params) -> arg0.instanceMethod(params) | ClassName::instanceMethod | 内部实例方法引用 |
(params) -> arg0.instanceMethod(params) | arg0.instanceMethod | 外部实例方法引用 |
个人技能参考
项目格式参考