八股总结(java)
-
ArrayList和LinkedList有什么区别
- ArrayList底层是动态数组,LinkedList底层是双向链表;
- 前者利于随机访问,后者利于头尾插入;
- 前者内存连续分配,后者通过指针连接多块不连续的内存;
-
实现多线程的几种方式
- 通过继承Tread并重写run();
- 实现无返回值的Runnable接口;
- 实现有返回值的Callable接口,通常于Future一起使用;
- 使用
ExecutorService
线程池工厂; - 使用
ThreadPoolExecutor
自定义线程池; - 使用
ComletetableFuture
异步链式编程;
-
线程池有哪些核心参数
-
核心线程数:线程池创建时就存在的线程数,并且不会因为任务运行完成而销毁;
-
最大线程数:线程池最大承载的线程数,非核心线程任务运行完成后自动销毁;
-
工作队列:用于存放待执行任务的阻塞队列,有基于数组的有界队列,基于链表的无界队列,不存储元素的队列但每个插入必须等待一个移除,支持优先级排序的无界队列;
-
线程空闲时间:非核心线程任务运行完成后等待新任务的最长时间;
-
时间单位:空闲时间的单位;
-
拒绝策略:当任务数超过最大线程数时的行为,默认抛出异常,也可以由提交任务的线程直接执行任务,也可以丢弃任务不抛出异常,也可以丢弃队列最旧任务并重新提交;
-
-
线程的状态有哪些
-
新建:线程new,但没有start();
-
可运行:调用start(),但还在操作系统分配资源;
-
阻塞:线程因为锁而阻塞;
-
等待:线程无限期等待其他线程的操作;
-
超时等待:线程等待其他线程的操作,但设置过期时间;
-
终止:线程执行完毕或异常终止;
-
-
String、StringBuilder、StringBuffer有什么区别
-
String,是定长字符串,对String的所有操作都会创建新的对象;
-
StringBuilder,可以在不创建新对象的情况下对字符串内容进行修改,线程并不安全,适用于单线程环境下;
-
StringBuffer,可以在不创建新对象的情况下对字符串内容进行修改,线程安全,适用于多线程环境下;
-
-
等于号(==)和equals方法有什么区别?
- ==常用于基本数据类型之间的比较,对于引用数据类型,则对比的是其地址
- equals常用于比较两个对象的内容是否相等,默认情况下Object类的equals方法比较的也是地址,但和许多包装类都重写了equals方法,使其比较的是对象的内容;
-
final关键字有什么作用?修饰类时有什么效果?
- final修饰的变量称为常量,如果修饰的是基本数据类型,则其值不能修改,若修饰的是引用数据类型则其指向的对象不能修改,但可以修改其对象内部状态;
- 修饰方法,不能被子类重写;
- 修饰类,不能被继承;
-
HashMap的底层原理
- 底层是一个默认长为16的动态数组,其存储链表或红黑树;
- 当有键值对存入时,键会通过哈希函数映射当数组对应下标位置,值则会存入链表中;
- 若链表长度超过8则转换为红黑树;若红黑树的数据量小于6,则退化为链表;
- 若数组元素数量超过默认负载因子的0.75就成倍扩容;
-
Hashmap的扩展知识
-
为什么链表长度超过8要转换为红黑树
- 在极端情况下,数据分配不均,导致大量键值对集中在一个元素链表中,会导致hashmap退化为链表影响效率;
- 根据大数据统计,链表长度超过8是因为哈希冲突导致的;
为什么选择红黑树而不是B+树
- 红黑树是自适应平衡二叉树,内存占用小,在hashmap中性能足够;
- B+树的适用情景为io数据流的读取,因为其节点可以有多个子节点,每个节点可以存储多个键,用于减少树的高度,降低磁盘io访问次数,并且所有子节点在同一层,用于平衡io的平均访问时间,叶子节点之间通过指针连接,形成一个有序链表,支持高效返回查询;
-
-
ConcurrentHashMap的底层原理
-
concurrentHashMap的底层就是HashMap的基础上保证线程安全;
-
在jdk8之前,采用分段锁实现线程安全,内部有多个段组成,每个段维护一部分数据,并各自有一把锁。修改某个段的数据时对该段加锁保证线程安全;读取则不用加锁,因为段内的数组使用volatile修饰,保证可见性;
-
jdk8后,采用CAS+synchronized实现线程安全,当要向数组中某一位置插入数据时,若该位置无数据就使用CAS进行写入,反之加锁后进行写入;
-
当数组进行扩容时采用的是并发+CAS+锁的方式,让扩容的同时不影响读取操作;
-
为什么改为CAS+锁,因为原来的分段锁每个段都需要维护一把锁,占用了过多的资源,并且段相比较节点的颗粒度较大;
-
class的生命周期
- class的生命周期指类从JVM虚拟机加载开始,直到卸载的过程,生命周期包括加载、验证、准备、解析、初始化、使用和卸载七个阶段;
- 加载,new创建类的实例、访问类的静态方法或属性、使用反射时会将class文件加载到JVM中,生成字节码class文件;
- 验证,确保字节码class文件的合法;
- 准备,静态变量分配内存并设置默认值;
- 解析,符号引用转为直接引用;
- 初始化,执行static{}代码块和静态变量的显式赋值;
- 使用;
- 卸载,释放内存;
-
JVM是什么
- java的java虚拟机,负责执行java字节码,并提供内存管理、垃圾回收等机制;
- JVM是java“一次编写,随处运行”的核心,将java源码编译为class字节码再解释或编译为机器码交由操作系统执行;
- JVM主要组成有类加载器,运行时数据区,执行引擎等;
-
JVM的工作流程
-
编写java代码;
-
使用javac编译为class字节码;
-
加载字节码;
-
执行字节码,交由JIT编译器与解释器,转换为机器码;
-
内存管理,分为堆、栈、方法区等;
-
程序结束,释放所有资源;
-
-
JVM的内存模型与垃圾回收机制
- 主要分为分为堆、栈、方法区(jdk8前叫永久代)、pc程序计数器、直接内存;
- 方法区,用于存放类信息,常量池,静态变量等;
- 堆,用于存放对象实例和数组,是垃圾回收的主要区域,分为新生代和老年代;
- 栈,用于存放局部变量、方法调用帧;
- 程序计数器,用于记录当前线程执行的字节码指令魏智用于线程切换后回复执行位置;
- 直接内存,手动管理,不受JVM堆内存限制;
- 垃圾回收机制
-
是JVM自动管理内存的核心机制,负责回收不再使用的对象,释放内存空间;
-
垃圾回收的基本原理是,可达性分析,从GC根对象触发,遍历所有可达对象,不可达对象视为垃圾进行回收;
-
垃圾回收算法:
- 标记-清除算法:从根对象出发,标记所有可达对象,清除未被标记的对象;缺点是会产生内存碎片;
- 标记-整理算法:在标记-清理算法的基础上,标记后先将存活的对象移动到另一片区域,之后进行清除;
- 复制算法:将内存分为AB两块,A区存活的对象赋值到B区,清理A区的所有对象;
- 分代收集:将堆分为新生代和老年代,新生代是新对象分配的区域,采用复制算法,老年代是长期存活的对象,使用标记-清除或标记-整理算法;
-
垃圾回收器:
- 单线程回收器,适用于单线程;
- 多线程回收器,适用与多线程;
- CMS回收器,采用三色标记法,并发执行,对象尚未被标记则为白色,对象已被标记但引用的对象还未标记则为灰色,对象及其引用对象都被标记则为黑色,适用于响应时间敏感的应用;工作流程是:
- 初始标记:标记从GC根对象直接可达的对象,标记灰色,需要STW停顿;
- 并发标记:标记所有灰色对象的引用对象;
- 重新标记:重新标记并发标记阶段所有修改引用或者不在引用的对象,需要SWT停顿;
- 并发清除:清除所有白色标记的对象;
- G1回收器,分区回收,适用于低延迟的大内存应用;
- ZGC回收器,超低延迟;
- G1垃圾回收器与CMS垃圾回收器的区别
- G1是在可控的停顿时间内实现高吞吐量,并且适用于大内存。CMS尽量减少停顿时间,适用于中小内存;
- G1堆内存管理基于分区管理,划分为多个大小相同的区域,每个区域可以是年轻代、老年代和永久代。而CMS则将堆内存划分为新生代和老年代;
- G1内存管理采用并发标记整理算法,减少碎片,而CMS采用并发标记清除算法,容易造成FullGC堆溢出;
- 发生了OutOfMemoryError如何解决
- 首先根据报错确认问题在哪;
- 使用jmap生成堆转储文件;
- 分析堆内存;
- 根据原因调整JVM参数;
- 选择合适的垃圾回收机制;
-
CAS的原理
- CAS是一种无锁原子操作,他有三个属性,变量的内存地址,预期值,新值,如果期望值与内存中的值一致则更新为新值;
- CAS存在ABA问题,ABA问题是当一个变量从A变为B再变为A,CAS机制会误认为值没有被修改;
- 常见的解决方案是通过给变量加上时间戳或是版本号,每次修改就会更新,这样修改值时,CAS就能检测到变化;
-
volatile字段的原理,是否能替代锁
- volatile是java的修饰词,用于确保变量的可见性与有序性,主要作用于解决多线程环境下的内存可见性问题;
- 在多线程环境下,每个线程都有自己的工作内存,当线程对变量进行修改时,会先修改变量存放到自己的内存中,不会立即同步给主内存,导致其他线程不能看到最新的值;
- volatile会强制修改后的值立即写会主内存,并强制其他线程重新读取该变量,保证可见性;
- volatile会保证变量的读写操作按照代码的顺序执行;
- volatile的底层原理是依赖于内存屏障,他是一种CPU指令用于控制指令的执行顺序与内存可见性;
- 在写入变量时,会插入写屏障,确保之前操作都完成后,在将新的变量同步给主内存;
- 在插入变量时,会插入读屏障,确保之后的操作都能看到最新的值;
- volatile只能解决可见性与有序性的问题,无法解决复合操作的原子性问题;
-
双亲委派模型
- 是java类的加载机制,采用逐级委派的方式加载类,主要目的是保证java核心类的安全性和避免类的重复加载;
- 按照层次关系分为:
- 启动类加载器:负责加载Java核心类库;
- 扩展类加载器:负责加载扩展类库;
- 应用类加载器:负责还在开发者自己编写的Java代码;
- 自定义类加载器:开发者继承ClassLoader自定义类加载逻辑;
- 工作机制:当类加载器需要加载一个类时,会由当前加载器先向父类加载器发送加载请求,直到启动类加载器,如果找不到则由当前加载器加载;
-
AQS的底层原理
- AQS(抽象队列同步器)是Java并发包中的核心组件,用于实现锁和同步器;
- AQS采用FIFO队列+CAS操作来管理锁的同步和排队,其底层主要通过状态变量(state)和队列管理来实现线程的阻塞、唤醒、等待和竞争锁等功能,通过CAS操作保证线程安全;
- state变量用于表示同步的当前状态,state==0表示锁为被占用,!=0表示已被占用;
- FIFO队列来管理等待同步的线程,当线程尝试获取同步资源失败时会被放入队列,按照先进先出的顺序等待获取同步;
- 队列数据结构是一个双向链表,每个等待的线程都是一个队列的节点,节点包含线程和前后指针;头部是等待同步的线程,尾部是最新加入的线程;
-
Executors线程工厂类有哪些常见的工厂方法
-
newFixedThreadPool(int nThreads),创建一个固定大小的线程池,核心线程数与最大线程数相同,适用于执行长期稳定任务,保证一定并发量的情况;
-
newCachedThreadPool(),创建一个可缓存的线程池,无核心线程,最大线程数为Integer.MAX_VALUE,适用于执行大量短时间任务的情况,能够充分利用系统资源;
-
newSingleThreadExecutor(),创建单线程的线程池,保证所有任务按照顺序执行;
-
-
数组如何转为list
- 使用Arrays.asList(T… a),返回定长list;
- 使用new ArrayList<>(Arrays.asList(array));
- 使用Stream,Arrays.stream(array).collect(Collectors.toList());
- 使用for循环手动添加;
-
如何遍历list
- 增强型for循环;
- 普通循环,访问下标;
- 迭代器;
- Stream流,forEach(System.out::println);
-
任务提交给线程池后是怎么工作的
- 线程池会先检查核心线程数,小于核心线程数,会创建新线程;
- 任务队列未满则任务进入阻塞队列;
- 如果任务队列(ArrayBlockingQueue)已满,且当前线程数小于最大线程数,则会创建新线程执行任务;
- 如果已经达到最大线程数,线程池会执行拒绝策略;
-
wait()和sleep()的区别
- wait和sleep方法都用于使用线程暂停执行,但他们在使用方式、功能和影响上有显著区别;
- sleep是Thread类的静态方法,可以在任何地方调用,无需持有对象锁,调用后线程会暂停执行指定的时间,但不会释放已持有的锁,其他县城无法获取这些锁;
- wait是Object类的方法,必须在同步方法或同步代码块内调用,并且在调用时需要持有该对象的监视器锁,调用后,线程会释放已持有的锁,允许其他线程获取这些锁,线程会进入等待状态,直到被其他线程调用其对象的notify()方法唤醒。如果wait添加了过期时间,若未超过过期时间也可以被notify唤醒,超过时间后自动唤醒;
- sleep常用于让线程暂停执行一段固定的时间,例如实现定时任务或控制线程执行频率;wait常用于线程间的通信;
-
并行与并发的区别:
- 并行(Parallelism): 多个任务在同一时刻同时执行,通常需要多核处理器的支持。
- 并发(Concurrency): 多个任务在同一时间段内交替执行,但不一定同时执行。
-
Java常见的设计模式
- 单例模式:
- 在系统中一个类只存在一个实例,类自身负责创建并维护唯一实例,并提供一个全局方法,允许外部访问该实例;
- 常用于资源管理、配置管理和日志记录;
- 实现单例模式一般通过双重检查锁定,在需要时才创建实例,并通过双重检查和同步块确保线程安全,同时提高性能;
- 普通工厂模式:
- 由一个工厂类根据传入的不同参数创建不同的产品实例,将对象创建逻辑封装在工厂类中;
- 将对象的创建与使用分离,降低了耦合性;
- 缺点是集中力所有对象的创建逻辑,违反了单一职责原则,可能导致工厂类过于复杂。且当需要添加新产品时,需要修改工厂类的代码,不符合开闭原则;
- 工厂方法模式:
- 定义了一个创建对象的接口,由子类决定实例化哪个具体类;
- 相比较普通工厂方法,符合开闭原则,增加新产品时无需修改现有代码;
- 符合单一职责原则,每个具体工厂类只负责创建对应的产品;
- 增加了系统的复杂度,需要对每个具体实现创建工厂类;
- 抽象工厂方法:
- 提供一个创建一系列相关或相互依赖对象的接口,也就是对多组产品抽象其方法,封装隐藏其产品创建的过程;
- 单例模式:
-
threadLocal是什么?
- TreadLocal是一个创建线程局部变量的类,通常情况下,多个线程共享同一变量,可能导致并发问题,使用TreadLocal,存储的变量只能由当前线程使用,保证线程隔离;
- 使用TreadLocal时,可能会导致内存泄露,因为与线程生命周期绑定,如果不及时清理,可能会导致无法回收对象占用内存,所以需要在不需要TreadLocal时调用remove()清理;