# 美团 Java 后端日常实习面经
# 线程池的核心参数有哪些?
线程池的构造函数有七个参数,分别是核心线程数(corePoolSize)、最大线程数(maximumPoolSize)、空闲线程存活时间(keepAliveTime)、时间单位(TimeUnit)、线程池任务队列(workQueue)、线程工厂(ThreadFactory)和拒绝策略(RejectedExecutionHandler)。
核心线程数是线程池中长期存活的线程数,如果线程池中的线程数量小于核心线程数,这些线程处于空闲状态也不会被销毁;
最大线程数是线程池最多能创建的线程数当核心线程已满,任务队列已满时,如果当前线程小于最大线程数,就会创建新的线程执行此任务,否则触发拒绝策略。
在线程数大于核心线程数时,如果每个线程的空闲时间超过了设置的空闲线程存活时间,这个线程就会被销毁,销毁线程数=当前线程数-核心线程数。空闲线程的存活时间单位是TimeUnit。
线程池的任务队列在没有空闲线程执行新任务时,存储所有待执行任务。
线程池通过创建线程的工厂设置线程的优先级、线程命名规则以及线程类型(用户线程/守护线程)。
线程池的拒绝策略则是当线程池的任务超出线程池队列可以存储的最大值之后,执行的策略。

# 线程池的拒绝策略有哪些?
- AbortPolicy是默认的拒绝策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
- DiscardPolicy是AbortPolicy的slient版本,直接丢掉任务而不产生异常。
- DiscardOldestPolicy是丢弃最老的策略,会将最早进入队列的任务删掉,也就是把队头移除再尝试入队。
- CallerRunsPolicy是添加到线程池失败时调用线程自己去执行任务,不会等待线程池中的线程去执行。
# 怎么设计线程池?
使用线程池时可以通过ThreadPoolExecutor类或者通过Executors类创建线程池,需要考虑线程池参数的合理性,需要重点关注的参数有核心线程数、最大线程数、阻塞队列大小。
需要根据任务类型设置合理的核心线程数,如果是CPU密集型任务核心线程数可以设置为CPU的核心数,充分利用CPU资源;如果是I/O密集型任务,线程大部分时间在等待I/O操作,可以设置大一点的核心线程数,如CPU核心数的2倍;
考虑系统的资源限制设置最大线程数,如果过大可能会耗尽系统资源;
需要根据场景设置合理的阻塞队列大小,避免队列容量过小任务被拒绝或者过大导致任务等待时间过长;
根据实际情况的需求判断任务是否有优先级,可以使用PriorityBlockingQueue作为阻塞队列,实现任务的优先级排序。
在编写线程池代码时还需要确保线程池在正确的时机启动和关闭,可以使用shutdown或shutdownNow方法关闭线程池;
shutdown会等待执行中的任务完成再关闭线程池,shutdownNow则会尝试中断正在执行的任务,关闭线程池的同时返回尚未执行的任务列表;
向线程池提交任务时需要在提交前对任务进行去重,或者在任务本身中增设标志位标识完成情况,避免网络不稳定时浪费线程池资源;如果任务涉及共享资源需要采取同步措施,防止线程池并发执行任务时出现数据不一致或者资源竞争的情况。
# G1相对于CMS有哪些核心提升?
- G1收集器在JDK1.7被引入,后面取代CMS为默认收集器。
- 适配大内存场景:G1收集器将堆内存分为多个区域(Region) ,维护一个优先列表,根据允许的收集时间选择回收价值最大的Region,尽可能提高收集效率;
- 兼顾吞吐量与低延迟:G1可以充分利用CPU资源缩短停顿时间,合理控制并发线程数,CMS大部分阶段会和用户线程并行,抢占CPU资源;
- 解决内存碎片问题:CMS的标记-清除算法,没有进行内存整理,可能会产生大量内存碎片;G1使用标记-整理算法,筛选回收阶段会把存活对象移动到空闲Region中进行内存整理,不会因碎片触发Full GC;
- G1可以管理新生代和老年代的内存,可以对整堆进行垃圾回收,而CMS只能对老年代进行回收。

# 与cms配合的新生代算法是什么?
CMS是老年代的垃圾回收器,一般搭配的新生代收集器是ParNew收集器,基于复制算法实现。
ParNew是新生代的并行收集器,是Serial收集器的并行版本,负责在MinorGC时标记Eden和Survivor From区中存活的对象,然后将存活的对象复制到To区,最后清空Eden和Survivor From区。
ParNew可以将新生代的GC Roots信息快速传递给CMS,保证CMS 初始标记阶段高效执行。
# mysql什么时候会出现死锁?
- 当有多个事务按照不同顺序更新同一批数据行时,会出现死锁,即多个事务交叉持有并等待对方的间隙锁时,就会造成循环等待而产生死锁。
- 如果两个事务都使用了select...for update语句锁定一批数据,当锁定顺序不一致时会产生死锁。
- 如果SQL语句没有走索引而使用非索引字段进行更新时,MySQL会从行锁升级成间隙锁,导致锁范围变大,可能会导致死锁。
- 如果使用外键会自动加锁关联表,多表操作时容易因为循环等待而造成死锁。
# ThreadLocal了解吗?
- ThreadLocal是一个线程的本地变量,可以实现线程隔离,每一个线程独享一个ThreadLocal,在接收请求的时候使用set()设置当前线程中变量的副本,使用get()方法获取ThreadLLocal在当前线程中保存的变量副本,线程可以存储私有数据而互不干扰。
- ThreadLocal通过Thread类的局部变量和哈希表实现,每个Thread中持有一个ThreadLocalMap,ThreadLocalMap中有一个Entry,以ThreadLocal实例为key,当前线程中变量副本为value。
# 介绍一下mysql的索引?
- MySQL索引的核心数据结构是B+树,是适配磁盘存储的多路平衡查找树。B+树采用分层存储,数据存储在叶子节点中,其他节点只有key,叶子节点之间通过双向链表连接。进行BETWEEN、<、>等范围查询操作时效率较高,并且左右子树高度差不超过1,磁盘读写代价低,保证查询效率稳定。
- MySQL还有哈希索引,是基于哈希表实现的,能够通过哈希函数将索引列的值直接映射到一个位置,在进行等值查询时速度很快,时间复杂度仅为O(1),但是不支持范围查询、排序和模糊匹配。
- 除此之外,MySQL的全文索引可以解决大文本内容的关键词搜索问题,适用于CHAR、VARCHAR、TEXT类型的列,支持复杂的自然语言搜索。空间索引可以用于地理空间数据类型,可以进行距离等关系查询。
# mysql隔离级别有哪些?
MySQL有四种隔离级别,按照隔离性从低到高分别是读未提交、读已提交、可重复读或串行化。
读未提交是读取其他事务未提交的修改,存在脏读、幻读、不可重复读的问题,适合对数据一致性要求极低的场景。
读已提交是只能读取其他事务已提交的修改,能够解决脏读问题,但是仍有不可重复读和幻读问题。
可重复读是同一事务内多次读取同一数据时结果保持一致,能够解决脏读和不可重复读问题。它是MySQL InnoDB引擎的默认隔离级别,InnoDB使用MVCC和Next-Key Locks间隙锁机制可以解决幻读。
串行化会给记录加读写锁,能够完全解决脏读、不可重复读和幻读问题 ,但是并发性能差,适用于对数据一致性要求高的场景。
# redis数据结构有哪些?
Redis有String、Hash、List、Set、ZSet五种基础的数据结构,能够满足大多数缓存、计数、队列场景,还有Bitmap、HyperLogLog、Geospatial三种高级结构,适合比较特殊的场景。
String用于存储字符串整数或浮点数,可以支持直接赋值、拼接、自增/减。常用的场景是存储用户token、计数器和简单的键值对缓存。核心命令有set key value设置值、get key获取、incr key自增和append key str拼接。
Hash是类似Java的HashMap,存储键值对集合,适合存储对象型数据,可以单独修改某个字段。核心命令有hset key field value设置字段、hget key field获取字段、hgetall key获取所有字段和hdel key field删除字段。
List是有序可重复的元素集合,底层是双向链表或压缩列表,支持两端插入或删除操作,可以按索引访问。可以实现简单的任务队列,最新列表。核心命令有lpush/rpush key value从左边或右边插入,lpop/rpop从左边或右边弹出和lrange key start可以获取范围元素。
Set是无序不可重复的元素集合,底层是哈希表,支持交集、并集和差集等集合运算。可以使用在需要去重,随机抽取的场景中。核心命令有sadd key value添加元素、smembers key获取所有元素、sinter key1 key2取交集以及sunion key1 key2可以取并集。
ZSet类似Set,额外关联一个分数,会按照分数排序,可以使用在需要排名的场景中。核心命令有zadd key score value添加元素及分数,zrange/zrevrange key start end按分数升序或降序取范围,zrank key value可以获取排名。
Bitmap是位图,按位存储数据0或1,一般使用在统计场景中。核心命令有setbit key offset value设置某一位,getbit key offset获取某一位,bitcount key统计1的个数。
HyperLogLog是基数统计,统计集合中不重复元素的个数,占用空间小,适合做UV统计。核心命令是pfadd key value1 value2添加元素、pfcount key统计基数以及合并两个HyperLogLog使用pfmerge dest key1 key2。
Geospatital是地理空间,存储经纬度坐标,支持距离计算和范围查询,比如geoadd key 经度 纬度 地点可以添加坐标,使用geodist可以计算距离,georadius key 经度 纬度 半径查询范围内的地点。
# 进程和线程有什么区别?
进程和线程最核心的差异是进程是操作系统资源分配的基本单位,线程是任务调度和执行的基本单位。
进程内可以有多个线程,进程的结束会终止所有线程,线程崩溃可能导致进程崩溃,但是进程崩溃不影响其他进程。没有线程的进程可以看做单线程,所以线程也被称为轻量级进程。
在开销方面,进程有独立的代码和数据空间,切换会有较大开销;同一类线程共享代码和数据空间,有独立的运行栈和程序计数器,切换开销小。
# 介绍一下Spring的AOP和IOC?
AOP就是面向切面编程,横向地把日志、事务、权限这些跨所有业务模块的通用逻辑,封装成一个 “切面”,不用在每个业务方法里重复写,直接切入到业务逻辑中,实现通用逻辑和业务逻辑的解耦,业务代码更干净。
AOP使用Java的动态代理机制,Spring会为被切入的对象(目标对象)创建一个代理对象,所有通用逻辑都织入到代理对象里,我们调用业务方法时,实际调用的是代理对象的方法,代理对象先执行通用逻辑,再执行原始的业务方法,最后执行后续的通用逻辑,目标对象完全不用改。
SpringIOC是控制反转的设计思想,原先由程序员控制整个程序的执行,使用IOC思想后将控制权交给了IOC容器,由Spring IOC容器来控制Bean的生命周期,对象的创建、初始化和销毁。在程序中不手动new对象,而是通过@Component或@Autowired注解来配置,容器会自动实例化Bean,注入依赖,降低代码耦合,简化开发。
因此IOC是基础,AOP是基于IOC实现的。IOC使对象之间解耦,管理Bean的生命周期,而AOP让横切逻辑解耦,实现方法增强。
# 双亲委派机制了解吗?
双亲委派机制是Java类加载器的一种类加载策略,该机制的核心思想是当类加载器收到类加载的请求时,会先检查该类是否已经被加载过;
如果已经加载则直接返回该类的引用;否则会委托给父类加载器加载该类,直到顶层的启动类加载器;
父类加载器如果无法加载,当前类加载器才会尝试自行加载,也就是调用自己的findClass()方法。如果当前的类加载器也无法加载这个类,那么它会抛出一个ClassNotFoundException异常。

# 介绍一下jvm内存区域?
JVM内存结构(运行时数据区)可以分为Java虚拟机栈、堆、方法区、本地方法栈和程序计数器五个部分。在JDK8之前,方法区通过永久代实现,JDK8及以后,永久代被元空间取代。其中堆和方法区是线程共享的,虚拟机栈、程序计数器和本地方法栈是线程私有的。
Java虚拟机栈存储方法执行时的栈帧,用来存储局部变量、操作数栈动态链接和返回地址,每个线程的虚拟机栈生命周期和线程相同;
堆存放对象的实例和数组,是垃圾回收的主要区域;
本地方法栈登记本地方法,管理本地方法的调用;
程序计数器记录当前线程执行的字节码指令地址;
方法区保存已经被加载的类信息、静态变量和即时编译后的代码等数据,关闭JVM后释放。

# 堆是怎么划分的?
JVM会把堆分成新生代和老年代,可以提高GC的回收效率,对不同区域采用不同回收算法。默认情况下,新生代与老年代的占比为1:2。
新生代存储刚创建的对象,特点是“对象生命周期短、创建和销毁频繁”。可以进一步分为Eden区(新对象优先分配这里)和两个Survivor区(From/To,用于存放GC后存活的对象),Eden与两个Survivor的大小占比为8:1:1。采用“复制算法”:将Eden和From区存活的对象复制到To区,然后清空Eden和From区,每次GC完成From区和To区会互换角色,循环复用效率极高。
老年代存储在新生代多次GC后仍存活的对象(如长期使用的缓存对象、单例对象),特点是“生命周期长”。采用“标记-清除”或“标记-整理”算法:不需要频繁回收,重点是减少内存碎片。
# HashMap和ConcurrentHashmap有什么区别?
- 二者的核心差异在于HashMap是非线程安全的,在多线程环境下会出现数据不一致或丢失的情况,在JDK7之前还可能会造成死循环,仅适用于单线程场景;ConcurrentHashMap是线程安全的,适合多线程并发场景,能够保证线程安全的同时减少性能损耗。
- HashMap的key和value值可以为空,但是ConcurrentHashMap不能有空值,因为多线程下空值会产生歧义。
- 对HashMap进行遍历时会采用快速失败方式,如果在遍历时修改数据则会抛出ConcurrentModificationException异常;对ConcurrentHashMap进行遍历时则使用弱一致性,会遍历数据的快照,因此遍历时修改数据不会产生异常。
# RDB和AOF分别是什么?
- RDB和AOF是Redis持久化机制,把内存中的数据写到磁盘中,防止服务宕机导致数据丢失。
- RDB(Redis Database) 会按照一定时间间隔把内存的数据以全量快照方式保存到硬盘的二进制文件dump.rdb中,可以通过save配置项定义快照的周期。可以执行SAVE阻塞或BGSAVE后台非阻塞命令进行保存。如果两次快照之间宕机,可能会造成一定的数据丢失。
- AOF(Append Only File) 会将每一条写命令写入AOF缓冲区(server.aof_buf)中,然后将缓存中的数据写入AOF文件并调用fsync()函数将数据写入磁盘appendonly.aof文件。如果Redis重启,会重新执行文件中保存的写命令从而在内存中重建整个数据库的内容。如果日志文件较大,可能恢复速度比较慢。
# synchronized和reentrantlock的区别?
- synchronized是基于JVM的内置隐式锁,线程在进入/退出代码是JVM会自动获取释放锁,且只能用于方法和代码块;ReentrantLock是Java提供的显式锁机制,是Lock接口的一个具体实现类,拥有Lock接口的特性,需要手动调用lock与unlock方法。二者都是可重入锁,同一个线程可以多次获取同一个锁。
- synchronized是非公平锁,且不支持响应中断;ReentrantLock可以进行公平性的设置,线程在等待锁时可以中断。
- synchronized不支持绑定多个条件,只能和wait()和notify()/notifyAll()方法一起使用,实现线程等待与通知;ReentrantLock可以与多个Condition对象结合,实现复杂的线程同步机制。
- ReentrantLock可以提供更细粒度的控制和灵活性,性能高于synchronized。
# 二者的底层实现机制是什么?
synchronized的核心机制是JDK1.6之后引入的锁升级机制,即偏向锁-轻量级锁-重量级锁,其中重量级锁依赖Monitor实现。
JDK1.6默认开启偏向锁,没有开启偏向锁时为无锁,偏向锁可以减少无竞争场景下的锁开销,当有线程获取到偏向锁时会记录线程ID,即JVM在对象头的Mark Word字段中记录当前线程ID,后续相同线程进入或退出同步块时无需CAS操作,仅需检查ID是否相同;有新线程竞争偏向锁时锁会升级为轻量级锁,JDK1.8后默认锁为轻量级锁,能够在无激烈竞争场景下解决多线程交替执行同步块问题,每个线程在自身栈帧中创建锁记录(Lock Record),通过CAS操作尝试将锁对象头的Mark Word设置为自身线程,如果成功设置则说明获取锁成功,自旋次数或者线程数过多则会升级为重量级锁。重量级锁依赖操作系统的互斥量实现,线程会被挂起节省CPU资源,但是存在内核态和用户态切换开销。线程会为对象创建一个监视器锁Monitor,获取锁时线程阻塞在Monitor的等待队列中,持有锁的线程释放锁时操作系统会唤醒等待线程。
ReentrantLock则是在JUC层面基于AQS实现的,同时结合CAS自旋减少阻塞开销。
AQS内部维护一个state变量记录锁状态和一个双向链表(CLH队列)管理等待线程,线程调用lock()时会通过CAS尝试将state从0改为1,成功则获取锁,失败则调用acquire(1),先执行tryAcquire(1)尝试再次CAS,如果当前线程已经持有锁则对state+1实现可重入,否则将线程封装为Node节点加入AQS等待队列,然后通过自旋和CAS尝试获取锁,自旋失败则阻塞线程,在锁释放时会调用unlock()执行release(1)将state-1,若state为0则唤醒队列中的后继线程。
评论
验证登录状态...