原文链接:b站java实习一面7.25_牛客网 (nowcoder.com)
本文仅作为个人学习使用。
线程池
参考文章:Java并发常见面试题总结(下) | JavaGuide
什么是线程池
线程池是一种线程管理机制,它预先创建了一定数量的线程,这些线程可以重复使用,避免了频繁创建和销毁线程的开销。
线程池主要优点
-
提高响应速度:
- 线程池预先创建了一定数量的线程,避免了频繁创建和销毁线程的开销,从而提高了系统的响应速度。
-
合理控制线程:
- 线程池能够对线程进行简单的管理和控制,避免了无限制线程的创建,防止资源耗尽。
-
复用线程:
- 线程池能够重复利用已创建的线程,减少了频繁创建和销毁线程的内存开销。
-
设置参数:
- 线程池能够设置核心线程数和最大线程数,对线程的数量进行有效地控制。
线程池参数
ThreadPoolExecutor
3 个最重要的参数:
-
corePoolSize
- 核心线程数量,即线程池中永远存在的最小线程数量。
- 即使这些线程处于空闲状态,也不会被回收。
-
maximumPoolSize
- 线程池中允许的最大线程数量。
- 当队列满了,并且已创建的线程数小于最大线程数时,线程池会创建新线程来处理请求。
-
workQueue
- 任务队列,用于存放等待执行的任务。
- 当所有核心线程都处于工作状态时,新任务会被添加到队列中。
ThreadPoolExecutor
其他常见参数 :
-
keepAliveTime
- 线程池中超过核心线程数的线程最大存活时间。
- 当线程空闲时间超过这个时间,多余的线程会被终止。
-
TimeUnit
keepAliveTime
参数的时间单位。
-
threadFactory
- 线程工厂,用于创建新线程。
- 可以自定义线程的名称、优先级等属性。
-
rejectedExecutionHandler
- 拒绝策略,当线程池无法执行新任务时,应该采取的饱和策略。
- 如
AbortPolicy
(默认)、DiscardPolicy
、DiscardOldestPolicy
、CallerRunsPolicy
等。
线程池的拒绝策略
线程池的拒绝策略是指当任务无法被线程池执行时,应该采取的饱和处理策略。常见的拒绝策略有以下几种:
-
AbortPolicy(默认策略):
- 直接抛出
RejectedExecutionException
异常。
- 直接抛出
-
DiscardPolicy:
- 默默地丢弃当前被拒绝的任务。
-
DiscardOldestPolicy:
- 丢弃线程池里最老的一个任务,然后重新提交当前任务。
-
CallerRunsPolicy:
- 将任务返回给当前线程执行。
-
自定义拒绝策略:
- 实现
RejectedExecutionHandler
接口,提供自定义的拒绝策略。
- 实现
这些拒绝策略各有优缺点:
AbortPolicy
直接抛出异常,适合对错误处理要求较高的场景。DiscardPolicy
和DiscardOldestPolicy
会直接丢弃任务,适合对结果不是太敏感的场景。CallerRunsPolicy
会将任务返回给当前线程执行,适合对响应时间要求不高的场景。- 自定义拒绝策略可以根据实际需求进行更细粒度的控制。
Java线程池构建
-
使用Executors工厂类创建线程池
- Java提供了Executors工厂类,可以通过该类的静态方法创建不同类型的线程池。
- 常用的方法有:
Executors.newFixedThreadPool(int nThreads)
: 创建固定大小的线程池。Executors.newCachedThreadPool()
: 创建一个可缓存的线程池。Executors.newSingleThreadExecutor()
: 创建一个只有一个线程的线程池。Executors.newScheduledThreadPool(int corePoolSize)
: 创建一个定长的线程池,支持定时及周期性任务执行。
-
手动创建ThreadPoolExecutor
-
通过直接创建
ThreadPoolExecutor
类的实例来构建线程池。 -
通过构造方法可以设置各种参数,如核心线程数、最大线程数、任务队列等。
-
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60L, TimeUnit.SECONDS, // 线程存活时间
new LinkedBlockingQueue<>(100), // 任务队列
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
-
使用Spring提供的线程池配置
-
Spring提供了
ThreadPoolTaskExecutor
类,可以更加方便地配置线程池。 -
可以通过Spring Bean的形式进行声明式配置,无需手动创建
ThreadPoolExecutor。
-
@Configuration
public class ThreadPoolConfig {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
return executor;
}
}
注意事项
虽然Executors
提供的工厂方法使用起来很方便,但它们使用了默认的拒绝策略和工作队列,这在某些情况下可能不是最佳选择。例如,newCachedThreadPool()
使用了SynchronousQueue
作为工作队列,这意味着它实际上是一个无界线程池,可能会导致系统资源耗尽。因此,在生产环境中,推荐使用ThreadPoolExecutor
来更细致地控制线程池的配置。
Java中常见的线程池实现
FixedThreadPool
: 固定大小的线程池。CachedThreadPool
: 可缓存线程池,线程池的线程数量根据需求动态调整。SingleThreadExecutor
: 单线程的线程池,所有任务按照顺序执行。ScheduledThreadPool
: 定时任务的线程池。ForkJoinPool
: 支持工作窃取算法的线程池。
线程池的生命周期(5种状态以及转换)
参考文章:线程池学习笔记(3)——线程池的生命周期_线程池生命周期-CSDN博客
-
RUNNING:
- 线程池处于正常工作状态,可以接受新任务并处理队列中的任务。
- 这是线程池的初始状态。
-
SHUTDOWN:
- 调用
shutdown()
方法后,线程池进入此状态。 - 不再接受新任务,但会继续处理队列中的任务。
- 当队列中的任务全部处理完毕,线程池就会进入TERMINATED状态。
- 调用
-
STOP:
- 调用
shutdownNow()
方法后,线程池进入此状态。 - 不再接受新任务,并且会尝试中断正在执行的任务。
- 当所有任务终止后,线程池就会进入TERMINATED状态。
- 调用
-
TIDYING:
- 当所有任务都已终止,并且所有工作线程都已经退出时,线程池进入此状态。
- 此时
runState
为TIDYING
,并且workCount
为0。 - 线程池会执行
terminated()
钩子方法,然后进入TERMINATED状态。
-
TERMINATED:
- 线程池彻底终止,进入此状态。
- 线程池的生命周期结束。
状态转换流程如下:
RUNNING
->SHUTDOWN
: 执行shutdown()
方法RUNNING
/SHUTDOWN
->STOP
: 执行shutdownNow()
方法SHUTDOWN
->TIDYING
: 当线程池和任务队列都为空时STOP
->TIDYING
: 当线程池为空时TIDYING
->TERMINATED
: 当terminated()
方法执行完成时
Java中的数据结构
在Java中,数据结构主要通过集合框架(Collection Framework)来实现,它提供了一系列的接口和类来存储、检索和操作数据。以下是一些主要的数据结构以及它们的底层实现细节:
-
List
ArrayList
: 实现了List
接口,内部使用动态数组来存储元素。当数组空间不足时,会创建一个更大的新数组,并将旧数组中的所有元素复制到新数组中。LinkedList
: 实现了List
和Queue
接口,使用双向链表来存储元素。每个元素包含一个指向下一个元素的引用和一个指向前一个元素的引用。
-
Set
HashSet
: 实现了Set
接口,内部使用哈希表来存储元素,不允许重复元素。它依赖于对象的hashCode()
和equals()
方法来确定元素的唯一性。TreeSet
: 实现了SortedSet
接口,使用红黑树来存储元素,可以按照自然顺序或自定义比较器排序。
-
Map
HashMap
: 实现了Map
接口,内部使用哈希表(拉链式解决冲突,当链表过长时转为红黑树)来存储键值对,不允许有重复的键。TreeMap
: 实现了SortedMap
接口,使用红黑树来存储键值对,键可以按照自然顺序或自定义比较器排序。LinkedHashMap
: 继承自HashMap
,但维护了一个双链表,可以保持元素的插入顺序或访问顺序。
-
Queue
ArrayDeque
: 实现了Deque
接口,使用循环数组作为底层数据结构,支持双端队列的操作。LinkedList
: 也可以作为队列使用,因为它实现了Queue
接口。
-
Stack
Vector
: 虽然Vector
类不是专门用于实现栈的,但它可以用作线程安全的栈,因为它的方法是同步的。Stack
: 是一个过时的类,现在通常推荐使用Deque
接口的实现类如ArrayDeque
来替代。
链表 vs 红黑树
链表
- 优点:
- 实现简单。
- 插入和删除操作快,不需要移动元素。
- 缺点:
- 查找效率低。对于链表,最坏情况下查找一个元素的时间复杂度是O(n),其中n是链表的长度。
- 当哈希冲突较多时,链表的长度增加,查找效率显著下降。
红黑树
- 优点:
- 自平衡的二叉查找树,保证了任何操作(查找、插入、删除)的平均和最坏情况时间复杂度都是O(log n),其中n是树中节点的数量。
- 在数据量大且哈希冲突严重时,相比于链表,红黑树能提供更稳定的性能。
- 缺点:
- 实现复杂,需要额外的维护来保持树的平衡。
- 占用的空间比简单的链表要多,因为每个节点需要额外的指针和颜色信息。
红黑树的优势
-
查找效率高:红黑树的查找效率比链表高,特别是在处理大量数据和频繁查找的情况下。由于其自平衡性质,红黑树能够保持树的高度相对较小,从而减少了查找路径的长度。
-
插入和删除效率:红黑树的插入和删除操作虽然比链表复杂,但由于其自平衡机制,仍然能够保持O(log n)的时间复杂度,这在数据量大的情况下优于链表的O(n)。
-
空间效率:尽管红黑树需要额外的存储空间来保存平衡信息(如颜色标记),但考虑到它带来的性能提升,这种额外的空间消耗是值得的。
-
有序性:红黑树能够保持键的自然排序,这是链表所不具备的。虽然
HashMap
不关心键的排序,但在类似TreeMap
的数据结构中,红黑树的有序性是非常重要的。 -
稳定性:红黑树提供的O(log n)性能在各种情况下都是稳定的,而链表的性能随着链表长度的增加而下降。
如何减少哈希冲突
-
良好的哈希函数
- 设计一个良好的哈希函数至关重要。哈希函数应该尽可能均匀地分布键,使得不同键产生不同的哈希码。理想情况下,哈希函数应接近均匀随机地分配哈希值,避免聚集。
- 使用Java的
Object.hashCode()
方法是一个起点,但你可能需要根据键的具体类型进行优化,例如对于字符串,可以考虑使用更复杂的算法,如MurmurHash。
-
足够的容量
- 增加哈希表的大小可以减少冲突的可能性。较大的容量意味着每个桶(bucket)的平均元素数量减少,从而降低冲突率。
- Java的
HashMap
会在负载因子(load factor)超过设定值时自动扩容,但这可以通过初始化时设置一个较大的容量来延后或减少。
-
负载因子调整
- 负载因子是哈希表中元素数量与桶数量的比率。默认情况下,
HashMap
的负载因子为0.75,这意味着当元素数量达到桶数量的75%时,HashMap
会自动扩容。调整负载因子可以在内存使用和冲突率之间找到平衡。
- 负载因子是哈希表中元素数量与桶数量的比率。默认情况下,
-
二次哈希
- 使用二次哈希方法可以进一步减少冲突。这种方法使用第二个独立的哈希函数来解决冲突,即使第一个哈希函数产生了冲突,第二个哈希函数也可能产生不同的结果。
-
开放寻址法
- 开放寻址是一种冲突解决策略,它在哈希表中寻找下一个空闲位置,而不是使用链表或红黑树。常见的开放寻址策略包括线性探测、二次探测和双重哈希。这种方法可以避免链表或树的开销,但可能会导致更多的探查次数。
-
动态扩容策略
- 实现一个更智能的动态扩容策略,比如根据历史数据的增长模式预测何时需要扩容,或者在每次扩容时都增加一定的比例,而不是固定倍数。
-
键的预处理
- 对键进行预处理,如规范化字符串或使用更简单的数据类型,可以减少哈希计算的差异性和冲突。
-
使用更高级的数据结构
- 如果哈希冲突非常严重,考虑使用其他数据结构,如跳表(Skip List)或布隆过滤器(Bloom Filter),它们在某些场景下可以提供更好的性能。
在实际应用中,通常需要结合以上方法并根据具体需求和场景进行权衡和调整。例如,对于实时系统或高并发环境,可能需要更重视性能和响应时间,而在存储空间有限的设备上,则可能需要更关注内存使用效率。
数据库索引
-
索引使用场景
- 频繁出现在 WHERE、ORDER BY、JOIN 条件中的列
- 区分度高的列(包含较少重复值)
- 数据量大的表
-
索引数据结构
- B-Tree: 多叉树数据结构,查找效率高
- Hash: 基于哈希表的数据结构,等值查询效率高
R-Tree: 用于空间数据索引,适用于范围查询Bitmap: 位图索引,适用于基数较小的列
-
索引创建原则
- 针对经常出现在查询条件、排序条件、连接条件中的列创建索引
- 选择独立性好、离散度高的列作为索引
- 尽量缩短索引长度,仅包含查询中需要的列
- 避免对频繁更新的列创建索引,更新会降低性能
-
最左匹配原则
- 联合索引中,查询条件要匹配索引最左边的列
- 索引 (a, b, c) 可以支持 WHERE a=1, WHERE a=1 and b=2, WHERE a=1 and b=2 and c=3
- 不支持 WHERE b=2, WHERE c=3
总的来说,MySQL 索引是提高查询性能的关键。合理利用索引需要根据具体的业务场景和查询需求来设计。索引的创建应遵循一定的原则,尤其要注意最左匹配原则。合理设计索引可以大幅提升 MySQL 的查询效率
redis的作用和实现
-
作用
- 缓存: Redis可以用作高速缓存,存储常用的数据并快速访问,降低数据库的负载。
- 消息队列: Redis支持发布/订阅、流等消息队列功能,可实现消息的异步处理。
- 数据库: Redis支持丰富的数据结构,如字符串、哈希、列表、集合等,可用作轻量级数据库。
- 分布式锁: Redis可以实现分布式环境下的互斥锁,解决多进程/线程间的资源竞争问题。
-
实现特点
- 内存数据库: Redis将数据存储在内存中,读写速度非常快,但需要定期将数据持久化到磁盘。
- 单线程架构: Redis采用单线程模型,通过高效的事件循环机制来处理并发请求。
- 丰富的数据结构: Redis支持字符串、哈希、列表、集合、有序集合等多种数据结构。
- 灵活的数据持久化: Redis提供AOF(Append-Only File)和RDB(Redis Database)两种持久化方式。
- 高可用性: Redis支持主从复制和哨兵机制,可实现数据的高可用和故障转移。
- 集群方案: Redis Cluster提供水平扩展能力,可以通过添加节点来提高整体性能。
-
使用场景
- 缓存系统: 常见的web应用、游戏应用等都可以使用Redis作为缓存层,提高响应速度。
- 计数器: Redis的数据结构可以方便地实现各种计数功能,如点赞数、访问量等。
- 排行榜: 利用有序集合(Sorted Set)可以方便地实现各种排行榜功能。
- 实时系统: Redis的发布/订阅模式可用于构建实时的消息推送系统。
- 分布式锁: Redis提供的原子性操作可以方便地实现分布式锁功能。
redis和数据库的数据一致性
(不知道这里问的是redis集群主从复制还是问的redis作为缓存如何保证与数据库数据一致)
redis集群主从复制
Redis 集群主从复制在保证数据一致性方面有一些特点和限制,主要体现在以下几个方面:
-
数据复制模式
- Redis 集群支持异步复制模式,这意味着主节点更新数据后,并不会等待从节点完成数据同步才返回成功,而是立即返回。这种方式可以提高性能,但也会带来一些数据一致性的问题。
-
复制延迟
- 由于异步复制的特性,主节点和从节点之间会存在一定的数据复制延迟。在主节点发生故障切换到从节点时,可能会出现数据丢失或不一致的情况。
-
故障切换
- Redis 集群支持自动故障切换,当主节点发生故障时,从节点会被自动提升为新的主节点。但在切换过程中,可能会出现数据丢失或不一致的情况。
-
部分失败
- 在 Redis 集群中,某些跨分片的操作(如事务)可能会出现部分成功部分失败的情况。这也会导致数据不一致。
为了提高 Redis 集群主从复制的数据一致性,可以采取以下措施:
-
同步复制
- 配置 Redis 集群使用同步复制模式,主节点必须等待从节点完成数据同步才返回成功。这可以提高数据一致性,但会降低性能。
-
读写分离
- 将读操作和写操作分离,读操作优先从从节点读取,写操作则发送到主节点。这可以降低数据不一致的概率。
-
数据备份和恢复
- 定期备份 Redis 集群的数据,并在必要时进行数据恢复。这可以帮助补救数据不一致的情况。
-
Redis Sentinel
- 使用 Redis Sentinel 监控主从节点的状态,在主节点故障时自动完成主从切换,并通知客户端新的主节点地址。这可以提高可用性和一致性。
-
两阶段提交
- 对于跨分片的复杂操作,可以采用两阶段提交的方式,先在所有节点上执行预提交,然后再统一提交。这可以提高数据一致性。
redis作为缓存如何保证与数据库数据一致
-
缓存更新策略:
- 主动更新:当数据库中的数据发生变更时,主动更新 Redis 缓存中的数据。这种方式可以确保缓存与数据库的数据一致,但需要在应用程序中实现缓存更新逻辑。
- 延迟更新:当数据库中的数据发生变更时,先更新数据库,然后异步更新 Redis 缓存。这种方式可以提高性能,但可能会出现短暂的数据不一致。
- 双写模式:在执行数据库操作的同时,也执行相应的 Redis 缓存操作。这种方式可以确保数据一致性,但需要额外的开销,可能会影响性能。
-
失效策略:
- 时间失效:为 Redis 缓存设置合理的过期时间,使缓存自动失效。这样可以保证缓存数据不会一直滞后于数据库。
- 主动失效:当数据库中的数据发生变更时,主动让相关的 Redis 缓存失效。这样可以确保下次访问时能从数据库中获取最新数据。
-
读写分离:
- 将读操作和写操作分离,读操作优先从 Redis 缓存读取,写操作则发送到数据库。这可以降低数据不一致的概率。
-
缓存回收策略:
- 采用 LRU、LFU 等缓存回收策略,定期清理最近最少使用或最不常使用的缓存数据。这可以确保缓存中保留的数据是比较新鲜的。
-
缓存命中率监控:
- 监控 Redis 缓存的命中率,适当调整缓存策略和过期时间,以维持较高的缓存命中率,减少对数据库的访问。
-
数据库与缓存一致性检查:
- 定期检查 Redis 缓存中的数据和数据库中的数据是否一致,如果发现不一致则进行修复。这种方式可以发现数据不一致的问题,但无法主动防止数据不一致的发生。
-
事务性操作:
- 将数据库和 Redis 缓存的更新操作包裹在同一个事务中执行。这样可以确保数据库和 Redis 缓存的数据保持一致性。但是,如果事务执行过程中出现异常,可能会导致部分操作失败。
redis加锁
-
基于 SETNX 的分布式锁
- 实现:
- 这种方式利用 Redis 的原子性操作
SETNX
来实现分布式锁。 - 客户端首先使用
SETNX
命令尝试获取锁,如果成功则表示获取到了锁。 - 客户端在使用完锁后需要使用
DEL
命令删除锁。
- 这种方式利用 Redis 的原子性操作
- 优点:
- 实现简单,性能较好。
- 原子性操作可以保证锁的获取和释放是线程安全的。
- 缺点:
- 无法解决死锁问题,如果客户端在获取到锁后意外崩溃,则无法主动释放锁。
- 无法实现锁的自动续期,长时间持有锁可能会导致其他客户端无法获取到锁。
- 实现:
-
基于 Redlock 算法的分布式锁
- 实现:
- Redlock 是由 Redis 的作者提出的一种分布式锁算法,可以更可靠地解决死锁问题。
- 客户端需要在多个 Redis 实例上尝试获取锁,只有在大多数实例上成功获取锁,才算真正获取到了锁。
- 客户端在使用完锁后,需要在所有实例上都释放锁。
- 优点:
- 可以更可靠地解决死锁问题,提高了分布式锁的可靠性。
- 可以通过在多个 Redis 实例上获取锁来提高可用性。
- 缺点:
- 需要部署多个 Redis 实例,增加了部署和维护的复杂度。
- 获取锁的过程需要在多个实例上进行,性能会略有下降。
- 实现:
-
基于 Redis 的 Lua 脚本的分布式锁
- 实现:
- 这种方式使用 Lua 脚本来实现分布式锁的获取和释放。
- 客户端首先使用
SET
命令带有过期时间来获取锁,如果成功则表示获取到了锁。 - 客户端在使用完锁后,需要使用 Lua 脚本来删除锁,确保删除时只能删除自己的锁。
- 优点:
- 可以解决死锁问题,并且可以确保锁的删除操作是原子性的。
- 可以通过 Lua 脚本实现更复杂的锁定逻辑,如锁的自动续期等。
- 缺点:
- 需要客户端支持 Lua 脚本的执行,增加了客户端的复杂度。
- 如果 Lua 脚本执行过程中出现异常,可能会导致锁无法正确释放。
- 实现:
-
基于 Redis Stream 的分布式锁
- 实现:
- 这种方式利用 Redis 5.0 引入的 Stream 数据结构来实现分布式锁。
- 客户端首先使用
XADD
命令在 Stream 中添加一个消息来获取锁,如果成功则表示获取到了锁。 - 客户端在使用完锁后,需要使用
XDEL
命令删除自己的消息来释放锁。
- 优点:
- 可以解决死锁问题,并且可以确保锁的删除操作是原子性的。
- Redis Stream 具有较好的可靠性和持久性,可以提高分布式锁的可靠性。
- 缺点:
- 需要 Redis 5.0 及以上版本,对于旧版本的 Redis 不适用。
- 对客户端的 Redis 客户端库支持要求较高,需要支持 Stream 相关的命令。
- 实现:
如果对性能要求较高,且不需要特别高的可靠性,可以选择基于 SETNX
的方式;如果需要更高的可靠性,可以选择基于 Redlock 算法的方式;如果需要更强的原子性保证,可以选择基于 Lua 脚本或 Redis Stream 的方式。
怎么保证一个场景下的多线程对数据库的访问不冲突
-
事务管理
- 使用数据库事务确保数据的一致性。事务具有ACID特性(原子性、一致性、隔离性、持久性),确保一系列操作要么全部成功,要么全部失败。在多线程环境中,确保每个线程内的操作都在事务中完成,可以防止数据的不一致状态。
-
乐观锁
- 乐观锁通常使用版本号或时间戳来检查数据是否被其他事务修改。在更新数据前,线程会检查版本号是否与获取时相同,如果不同则重试操作。
-
悲观锁
- 悲观锁假设会发生冲突,因此在操作数据前先锁定相关资源。这可以通过数据库级别的锁(如行级锁、表级锁)或应用程序级别的锁(如Java的
synchronized
关键字或ReentrantLock
)来实现。
- 悲观锁假设会发生冲突,因此在操作数据前先锁定相关资源。这可以通过数据库级别的锁(如行级锁、表级锁)或应用程序级别的锁(如Java的
-
读写锁
- 使用读写锁可以允许多个线程同时读取数据,但只允许一个线程写入数据。这可以减少读操作的等待时间,提高并发性能。
-
数据库连接池
- 使用连接池可以有效地管理数据库连接,避免频繁的连接和断开操作,同时确保连接的正确释放,减少资源竞争。
-
隔离级别
- 数据库支持不同的事务隔离级别,如读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。较高的隔离级别可以减少幻读和脏读,但可能会影响性能。
-
并发控制算法
- 数据库通常支持多种并发控制算法,如多版本并发控制(MVCC)或两阶段锁(2PL),这些算法旨在减少锁的使用,提高并发性。
-
批处理和事务批量提交
- 将多个数据库操作组合在一个事务中进行批量提交,可以减少网络延迟和数据库的日志记录,提高性能。
-
异步处理和消息队列
- 使用异步处理和消息队列可以将任务排队,避免直接在主线程中阻塞数据库访问,从而提高系统的响应速度和吞吐量。
-
数据库设计和索引优化
- 合理的数据库设计和索引策略可以减少查询时间,避免热点数据的竞争。
SpringBoot框架的理解
底层技术
Spring Boot 主要建立在 Spring Framework 之上,利用了 Spring 框架的核心特性,如 IoC(控制反转)、AOP(面向切面编程)、MVC(模型-视图-控制器)等。此外,Spring Boot 还整合了其他的开源技术和标准,比如:
- JPA/Hibernate: 数据持久化框架。
- Thymeleaf/FreeMarker: 视图模板引擎。
- Spring Data: 提供对各种数据库的统一访问方式。
- Spring Security: 安全框架。
- Spring MVC/WebFlux: Web 层的支持。
主要特点
-
自动配置:SpringBoot会根据添加的依赖自动配置Spring,减少了大量的手动配置。比如添加了Spring Data JPA依赖,它就会自动配置数据源、事务管理等。
-
起步依赖:SpringBoot提供了一系列的"起步依赖",比如
spring-boot-starter-web
包含了Web开发所需的依赖。使用它可以快速构建Web应用。 -
Embedded容器:SpringBoot可以内嵌Tomcat、Jetty等Servlet容器,不需要部署war包。
-
Production-ready特性:SpringBoot提供了众多生产环境下的特性,如指标收集、健康检查、外部化配置等。
-
简化部署:SpringBoot应用可以打包成一个可执行的jar包,方便部署和管理。
-
可扩展性:SpringBoot应用可以很容易地接入其他Spring项目,如Spring Security、Spring Data等。
框架的好处
- 简化配置: Spring Boot 的自动化配置大大减少了样板代码和 XML 配置文件的数量。
- 快速开发: 开箱即用的特性允许开发者专注于业务逻辑而非基础设施配置。
- 易于部署: 支持打包为可执行的 JAR 或 WAR 文件,简化部署过程。
- 生产就绪: 提供了监控、健康检查、日志管理和外部配置管理等生产级特性。
- 社区支持: 由于基于 Spring Framework,Spring Boot 受益于庞大的 Spring 社区和丰富的第三方库。
- 云原生: 支持云平台,如 Kubernetes 和 Docker,易于实现微服务架构。
代码题:全排列
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
List<Integer> output = new ArrayList<Integer>();
for (int num : nums) {
output.add(num);
}
int n = nums.length;
backtrack(n, output, res, 0);
return res;
}
public void backtrack(int n, List<Integer> output, List<List<Integer>> res, int first) {
// 所有数都填完了
if (first == n) {
res.add(new ArrayList<Integer>(output));
}
for (int i = first; i < n; i++) {
// 动态维护数组
Collections.swap(output, first, i);
// 继续递归填下一个数
backtrack(n, output, res, first + 1);
// 撤销操作
Collections.swap(output, first, i);
}
}
}