面试-java基础
JavaSE
面向对象特性:封装、继承、多态。多态的实现方法:继承、重写、父类引用子类对象。
集合类:继承collection接口的类和继承map接口的类。
hashmap线程不安全的原因:一:没有同步机制。二、put操作无原子性。三、两个线程同时扩容会导致链表成环形结构。四、没有可见性保证手段。
concurrenthashmap:链表超过8转换成红黑树。线程安全实现方式:JDK 1.7 采用 Segment
分段锁来保证安全, Segment
是继承自 ReentrantLock
。JDK1.8 放弃了 Segment
分段锁的设计,采用 CAS + synchronized
保证线程安全,锁粒度更细,synchronized
只锁定当前链表或红黑二叉树的首节点。
异常的分类:
Error(错误)
- 由 JVM 抛出,表示严重问题,一般不捕获也不处理。
Exception(异常)
- Checked Exception(受检异常):编译器强制检查,必须显式捕获或在方法签名中声明抛出。例如
IOException
、SQLException
。 - Unchecked Exception(运行时异常):继承自
RuntimeException
,编译器不强制检查,可根据业务需要捕获。例如NullPointerException
、ArithmeticException
。
juc(java并发编程)
特性:原子性、可用性、有序性(指令重排:会保证程序最终执行结果和代码顺序执行的结果是一致的)
线程和进程:进程是程序的一次执行过程,是系统运行程序的基本单位;一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行。
•线程作为最小调度单位,进程作为资源分配的最小单位。
创建线程四种方式:继承Thread类、实现runnable接口、实现Callable接口、线程池创建线程。Runnable 接口run方法无返回值;Callable接口call方法有返回值,是个泛型。
run和start的区别:start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。run(): 封装了要被线程执行的代码,可以被调用多次。调用 start()
方法方可启动线程并使线程进入就绪状态,直接执行 run()
方法的话不会以多线程的方式执行。
进程通信方式:管道、共享内存、消息队列、信号量、套接字
可中断锁和不可中断锁
- 可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。
ReentrantLock
就属于是可中断锁。 - 不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。
synchronized
就属于是不可中断锁。
synchronized:依赖于JVM级别的Monitor,是悲观锁,一个线程获取到的标志就是在monitor中设置成功了Owner。
重量级锁:底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
轻量级锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性
偏向锁:一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令
synchronized与volatile的区别
volatile
只能使用在变量上;而synchronized
可以在类,变量,方法和代码块上。
volatile
至保证可见性;synchronized
保证原子性与可见性。
volatile
禁用指令重排序;synchronized
不会。
volatile
不会造成阻塞;synchronized
会
volatile:volatile关键字会强制将修改的值立即写入主存。
线程池处理任务的流程:
1,任务在提交的时候,首先判断核心线程数是否已满,如果没有满这时对于一个新提交的任务,线程池会创建一个线程去处理任务。(当线程池里面存活的线程数小于等于核心线程数corePoolSize
时,线程池里面的线程会一直存活着,就算空闲时间超过了keepAliveTime
,线程也不会被销毁,而是一直阻塞在那里一直等待任务队列的任务来执行。)
2,如果核心线程数满了,则判断阻塞队列是否已满,如果没有满,当前任务存入阻塞队列,等待后续线程来执行提交地任务
3,如果阻塞队列也满了,则判断线程数是否小于最大线程数,如果满足条件,则使用临时线程执行任务
4,如果当前的线程数达到了最大线程数目
,并且任务队列也满了,如果还有新的任务过来,那就直接采用拒绝策略进行处理。默认的拒绝策略是抛出一个RejectedExecutionException异常。
线程池中有哪些常见的blockingqueue:ArrayBlockingQueue:基于数组实现的有界阻塞队列。LinkedBlockingQueue。SynchronousQueue:不存储元素的阻塞队列。PriorityBlockingQueue:支持优先级的无界阻塞队列。
虚拟线程:称为轻量级线程或协程,虚拟线程允许一个操作系统线程执行多个虚拟线程。提高系统的吞吐量。更适合处理大量的IO密集型任务。虚拟线程的创建方法:Thread.startVirtualThread()
创、Thread.ofVirtual()
创建 、ThreadFactory
创建。
future:Future类的主要作用是提供一种机制,允许主线程异步执行任务,并在需要时获取任务的结果。
AQS:抽象同步队列:原理是通过维护一个状态变量和一个FIFO队列,结合CAS操作,来实现线程之间的同步和互斥控制
AQS如何实现可重入:通过state。首次获取时 CAS 竞争,重入时无需 CAS,直接累加。释放时递减,直到归零真正放锁。
如何确定线程池参数:
CPU 密集型任务
- 线程数 ≈ CPU 核数 + 1
I/O 密集型任务
- 线程数 ≈ CPU 核数 × (1 + 阻塞系数)
- 阻塞系数=I/O 等待时间 / 计算时间。若等待时间远大于计算时间,可配置更多线程。
- 固定队列容量,调
corePoolSize
和maximumPoolSize
- 记录在不同线程数下的 TPS/RT 和系统资源使用情况。
- 寻找吞吐量拐点:继续增加线程数,吞吐量不再显著提升,甚至下降的点。
- 固定线程数,调队列容量
- 太小会导致拒绝;太大会导致积压太深、响应延迟变高。
- 找到拒绝率为 0 且响应延迟可接受时的最小队列容量。
- 调整
keepAliveTime
- 观察在低峰期线程是否能及时回收,减少资源占用。
JVM调优:
JVM 调优的参数都有哪些?
- Xms 初始堆大小
- Xmx 最大堆大小
- XX:NewSize 年轻代大小
- XX:MaxNewSize 年轻代最大值
- XX:PermSize 永生代初始值
- XX:MaxPermSize 永生代最大值
- XX:NewRatio 新生代与老年代的比例
jps
输出JVM中运行的进程状态信息
jstack
查看java进程内线程的堆栈信息
jmap
用于生成堆转存快照
jstat
监视虚拟机运行时状态信息
G1:
G1的核心特点:
- 并行与并发:G1利用多核处理器的能力,在垃圾回收的不同阶段使用并行和并发的技术,减少对应用程序线程的影响。
- 分区的堆管理:G1将堆空间分割成许多相同大小的区域(Region),每个区域可以作为Eden、Survivor或Old区使用,这种布局有助于更灵活地管理内存。
- 垃圾优先策略:G1采用“垃圾优先”(即那些包含最少存活数据的区域(垃圾最多的区域)的策略,即根据预期的垃圾回收收益来决定哪些区域优先进行回收,从而在给定的暂停时间内获得最大的清理效果。
- 可预测的暂停时间:开发者可以设置期望的GC暂停时间目标,G1会尝试在不超过这个时间限制的情况下进行垃圾回收,这对于交互式应用和服务端应用非常重要。
- 混合回收:G1可以同时进行年轻代和老年代的垃圾回收,这有助于避免长时间的Full GC暂停。