Java学习整理
2023-02-23 15:43:50 0 举报
AI智能生成
登录查看完整内容
初中级Java学习整理
作者其他创作
大纲/内容
MVC
DDD
架构模式
异步处理
应用解耦
流量削峰
日志处理
消息通讯
使用场景
支持跨数据中心的消息复制
单机吞吐量:十万级,最大的优点,就是吞吐量高
topic数量都吞吐量的影响:topic从几十个到几百个的时候,吞吐量会大幅度下降。所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源
时效性:ms级
可用性:非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
消息可靠性:经过参数优化配置,消息可以做到0丢失
功能支持:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用
外框
优点
由于是批量发送,数据并非真正的实时; 仅支持统一分区内消息有序,无法实现全局消息有序
有可能消息重复消费
依赖zookeeper进行元数据管理
缺点
优缺点
顺序读写
零拷贝
分区
批量发送
数据压缩
特点
在队列中,一组用户可以从服务器中读取消息,每条消息都发送给其中一个人
排队
在这个模型中,消息被广播给所有的用户
发布-订阅
传统消息传递方式
快速:单一的Kafka代理可以处理成千上万的客户端,每秒处理数兆字节的读写操作
可伸缩:在一组机器上对数据进行分区
和简化,以支持更大的数据
持久:消息是持久性的,并在集群中进
行复制,以防止数据丢失
设计:它提供了容错保证和持久性
kafka相对于传统消息传递方式
消息传递方式
Zookeeper是一个开放源码的、高性能的协调服务,它用于Kafka的分布式应用
Zookeeper是什么
不,不可能越过Zookeeper,直接联系Kafka broker。一旦Zookeeper停止工作,它就不能服务客户端请求
可以在没有Zookeeper的情况下使用Kafka吗
Zookeeper主要用于在集群中不同节点之间进行通信
在Kafka中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取
除此之外,它还执行其他活动,如: leader检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等
Zookeeper在Kafka中的作用
Zookeeper
kafka 的伸缩性也可以通过Partition来体现,因为我们可以对Topic的分区进行动态扩容,从而提高整个系统的吞吐
如果指定了partition就直接发送到该分区
如果没有指定分区但是指定了key,就按照key的hash值选择分区
如果partition和key都没有指定就使用轮询策略
也称 Randomness 策略。所谓随机就是我们随意地将消息放置到任意一个分区上
随机策略
分区策略
我们知道Kafka是要等全部的follower同步完成后,才可以发送ack,这时候就有可能出现这这种情况:(1)leader收到数据后,后面的follower就开始同步数据了,但是如果有一个follower出现了故障,就会许久的不可以与leader来进行同步,这样leader就会一直等待下去了,直到它同步完成才会把ack发送出去。但是的话这种情况会大大的让Kafka效率降低,所以ISR就出现了
为什么需要ISR
什么是ISR
ISR(in-sync replica set)同步副本集
OSR(Outof-Sync Replicas)未能保持同步的副本集
领导者副本对外提供服务,这里的对外指的是与客户端程序进行交互
当领导者副本挂掉了,或者说领导者副本所在的 Broker 宕机时,Kafka 依托于 ZooKeeper 提供的监控功能能够实时感知到,并立即开启新一轮的领导者选举,从追随者副本中选一个作为新的领导者。老 Leader 副本重启回来后,只能作为追随者副本加入到集群中。Kafka 把所有不在 ISR 中的存活副本都称为非同步副本。通常来说,非同步副本落后 Leader 太多,因此,如果选择这些副本作为新 Leader,就可能出现数据的丢失。毕竟,这些副本中保存的消息远远落后于老 Leader 中的消息。在 Kafka 中,选举这种副本的过程称为 Unclean 领导者选举。Broker 端参数 unclean.leader.election.enable 控制是否允许 Unclean 领导者选举。开启 Unclean 领导者选举可能会造成数据丢失,但好处是,它使得分区 Leader 副本一直存在,不至于停止对外提供服务,因此提升了高可用性。反之,禁止 Unclean 领导者选举的好处在于维护了数据的一致性,避免了消息丢失,但牺牲了高可用性当ISR中的副本的LEO不一致时,如果此时leader挂掉,选举新的leader时并不是按照LEO的高低进行选举,而是按照ISR中的顺序选举
领导者副本的选举
Leader发生故障后,会从ISR中选出一个新的leader,为了保证多个副本之间的数据一致性,其余的follower会先将各自的log文件高于hw的部分截掉(新leader自己不会截掉),然后从新的leader同步数据。
Leader故障
领导者副本(Leader Replica)
追随者副本不处理客户端请求,它唯一的任务就是从领导者副本异步拉取消息,并写入到自己的提交日志中,从而实现与领导者副本的同步
Follower发生故障后会被临时踢出ISR(动态变化),待该follower恢复后,follower会读取本地的磁盘记录的上次的HW,并将该log文件高于HW的部分截取掉,从HW开始向leader进行同步,等该follower的LEO大于等于该Partition的hw,即follower追上leader后,就可以重新加入ISR
Follower故障
flower副本
AR(Assigned Replicas)
保证消费数据的一致性和副本数据的一致性
HighWatermark(HW)的作用
基于【时间删除】 日志说明
基于【大小超过阈值】 删除日志说明
日志清理
零拷贝不是指不需要拷贝,而是减少那些不必要的拷贝,从而减少额外的开销。
解决的是网络数据落盘。它将磁盘文件映射到内存中,之后通过修改内存来修改文件内容。
mmap
解决的是磁盘到网络数据的传输。操作系统读取磁盘数据到内存缓存后,直接发送给网卡缓存,然后发送网络数据。
sendfile
kafka在写入数据时,只允许向后追加数据,不允许修改已有数据。所以在每个partition当中,数据的顺序是能保证的。
如果topic或partition过多时,可能会导致kafka频繁切换partition,顺序读写不能保障。
PageCache是系统级别的缓存,他可以在数据写到PageCache时就返回,大大增加的写入效率。同时kafka也支持通过参数控制数据是写到PageCache时返回还是落盘时返回。
PageCache
kafka允许批量的接收、发送消息,从而增加读写效率
批量操作
kafka支持接收、发送压缩之后的消息,以增加网络传输效率。需要注意的是解压的时候会消耗CPU资源。
高性能原理
相同key值的消息写入同一个partition(partition内的消息是有序的),一个partition的消息只会被一个消费者消费
那么我们对这个消息管道中的数据进行hash处理分发到每一个线程中
如果消费者用多线程进行接收消息
kafka顺序消息
Kafka对数据的可靠性要求不是非常的高,就是可以说是容忍那么一丢丢的数据丢失,所以就不用等待ISR里的Follower全部接收成功。所以的话Kafka提供了三种可靠性的级别
producer不等待broker的ack,这一个操作是提供了最低的一个延迟,使得broker一接收到还没来得及写入磁盘的时候就已经返回了,当broker出现故障的时候就会可能丢失数据
acks=0
producer等待broker的ack,partition的leader落盘成功以后返回了ack,如果follwer它在同步成功之前发生了故障的话,那么就会把数据丢失掉
acks=1
producer等待broker的ack,partition的leader和follower需要全部落盘成功之后才会返回ack,但是如果在follower同步完成之后,在broker在发送ack前,leader发生了故障,那么这刻会造成数据的重复
acks=-1
ACK应答机制
可靠性
KafKa
热点数据的缓存
限时业务的运用
计数器相关问题
分布式锁
是一种将数据快照保存到磁盘的技术
SAVE
阻塞 Redis 直到完成快照(不推荐)
BGSAVE
fork 一个子线程,异步进行快照(fork 过程是阻塞的)
手动触发的命令
手动触发
配置自动触发
# 默认执行快照的生效条件# 900 秒内有 1 条 key 变化save 900 1 # 300 秒内有 10 条 key 变化save 300 10# 60 秒内有 10000 条 key 变化save 60 10000# 关闭 RDB 快照# save \"\"# 文件名称dbfilename dump.rdb# 文件保存路径dir ~/redis/data/# 如果持久化出错,主进程是否停止写入stop-writes-on-bgsave-error yes# 是否压缩rdbcompression yes# 导入时是否检查(损失性能)rdbchecksum yes
自动触发
触发方式
文件体积小
恢复数据快
实时性不足
fork 线程成本高
快照生成慢
Redis Database
RDB
以文本形式记录执行命令的日志,每次执行先写内存,后写日志
实时性高
不会大批量丢失数据
写后日志可以避免语句检查开销,但存在潜在风险
Append Only File
AOF
4.0之后Redis新增了RDB和AOF混合使用的方式来保证数据恢复
持久化
服务器不会主动删除数据,只有当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除
惰性删除
服务器执行定时任务删除过期数据,但是考虑到内存和CPU的折中(删除会释放内存,但是频繁的删除操作对CPU不友好),该删除的频率和执行时间都受到了限制
定期删除
过期键的删除策略
noeviction
不淘汰(4.0后默认的)
volatile-random
随机
volatile-ttl
ttl
volatile-lru
lru
volatile-lfu
lfu
对设置了过期时间的数据中进行淘汰
allkeys-random
allkeys-lru
allkeys-lfu
全部数据进行淘汰
LRU 算法的全称是 Least Recently Used,按照最近最少使用的原则来筛选数据。这种模式下会使用 LRU 算法筛选设置了过期时间的键值对
LRU算法
LFU 缓存策略是在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存
LFU 算法
内存淘汰算法
先更新 db然后直接删除 cache
写
从 cache 中读取数据,读取到就直接返回cache 中读取不到的话,就从 db 中读取数据返回再把数据放到 cache 中。
读
缺陷1:首次请求数据一定不在cache中解决方案:可以将热点数据可以提前放入 cache 中
缺陷2:写操作比较频繁的话导致 cache 中的数据会被频繁被删除,这样会影响缓存命中率解决方案:数据库和缓存数据强一致场景 :更新 db 的时候同样更新 cache,不过我们需要加一个锁/分布式锁来保证更新 cache 的时候不存在线程安全问题可以短暂地允许数据库和缓存数据不一致的场景 :更新 db 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小
缺陷
Cache Aside Pattern(旁路缓存模式)Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。Cache Aside Pattern 中服务端需要同时维系 db 和 cache,并且是以 db 的结果为准。
先查 cache,cache 中不存在,直接更新 db。cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache 和 db)。
从 cache 中读取数据,读取到就直接返回 。读取不到的话,先从 db 加载,写入到 cache 后返回响应。
Read/Write Through Pattern(读写穿透)Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。这种缓存读写策略小伙伴们应该也发现了在平时在开发过程中非常少见。抛去性能方面的影响,大概率是因为我们经常使用的分布式缓存 Redis 并没有提供 cache 将数据写入 db 的功能。
Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。
不同点
cache 数据可能还没异步更新 db 的话,cache 服务可能就就挂掉了
Write Behind Pattern 下 db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量
优势
Write Behind Pattern(异步缓存写入)Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写
3种常用的缓存读写策略
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
问题来源
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小
布隆过滤器
解决方案
缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
设置热点数据永远不过期
接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些 服务 不可用时候,进行熔断,失败快速返回机制
缓存穿透
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中
缓存雪崩
缓存中一些只会被访问一次或者几次的的数据,被访问完后,再也不会被访问到,但这部分数据依然留存在缓存中,消耗缓存空间
缓存污染
文章摘取
缓存数据一致解决方案
问题
Redis
中间件
分布式环境中,一致性是指多个副本之间,在同一时刻能否有同样的值
C 一致性
系统提供的服务必须一直处于可用的状态。即使集群中一部分节点故障
A 可用性
系统在遇到节点故障,或者网络分区时,任然能对外提供一致性和可用性的服务。以实际效果而言,分区相当于通信的时限要求。系统如果不能在一定实现内达成数据一致性,也就意味着发生了分区的情况。必须就当前操作在 C 和 A 之前作出选择
P 分区容错性
N1和N2当中各自有一个应用程序AB和数据库,当系统满足一致性的时候,我们认为N1和N2数据库中的数据保持一致。在满足可用性的时候,我们认为无论用户访问N1还是N2,都可以获得正确的结果,在满足分区容错性的时候,我们认为无论N1还是N2宕机或者是两者的通信中断,都不影响系统的运行
我们假设一种极端情况,假设某个时刻N1和N2之间的网络通信突然中断了。如果系统满足分区容错性,那么显然可以支持这种异常。问题是在此前提下,一致性和可用性是否可以做到不受影响呢?
有用户向N1发送了请求更改了数据,将数据库从V0更新成了V1。由于网络断开,所以N2数据库依然是V0,如果这个时候有一个请求发给了N2,但是N2并没有办法可以直接给出最新的结果V1,这个时候该怎么办呢?
牺牲了一致性
一种是将错就错,将错误的V0数据返回给用户
牺牲了可用性
第二种是阻塞等待,等待网络通信恢复,N2中的数据更新之后再返回给用户
两种方法
CAP不能同时满足的原因
一个系统保证了一致性和分区容错性,舍弃可用性。也就是说在极端情况下,允许出现系统无法访问的情况出现,这个时候往往会牺牲用户体验,让用户保持等待,一直到系统数据一致了之后,再恢复服务
舍弃A,保留CP
这种是大部分的分布式系统的设计,保证高可用和分区容错,但是会牺牲一致性。比如淘宝购物以及12306购票等等,前面说过淘宝可以做到全年可用性5个9的超高级别,但是此时就无法保证数据一致性了。
虽然舍弃C,但不完全不需要一致性,这里会保证最终一致性
分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用
Basically Available(基本可用)
允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是CAP中的不一致
Soft state(软状态)
最终一致是指经过一段时间后,所有节点数据都将会达到一致
Eventually consistent (最终一致性)
BASE解决了CAP中理论没有网络延迟,在BASE中用软状态和最终一致,保证了延迟后的一致性。BASE和 ACID 是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态
BASE
舍弃C,保留AP
很遗憾,这种情况几乎不存在。因为分布式系统,网络分区是必然的。如果要舍弃P,那么就是要舍弃分布式系统,CAP也就无从谈起了。可以说P是分布式系统的前提,所以这种情况是不存在的。
舍弃P,保留CA
三种选择
CAP
MQ事务消息方案
本地消息表方案
通知型
TCC
SAGA
补偿性
AP(基于BASE实现的最终一致性)
2PC
JTA
JTS
基于DTP模型
CP(强一致性)
Seata
分布式事务的选择
分布式事务
分布式
InnoDB 支持行级别的锁粒度,MyISAM 不支持,只支持表级别的锁粒度。
MyISAM 不提供事务支持。InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别。
MyISAM 不支持外键,而 InnoDB 支持。
MyISAM 不支持 MVVC,而 InnoDB 支持。
MyISAM 不支持数据库异常崩溃后的安全恢复,而 InnoDB 支持。
InnoDB和MyISAM的区别
通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
索引需要使用物理文件存储,也会耗费一定空间。
B 树的所有节点既存放键(key) 也存放 数据(data),而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。
B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
B 树& B+树两者有何异同呢?
B树 & B+树
哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。
为何能够通过 key 快速取出 value 呢? 原因在于 哈希算法(也叫散列算法)。通过哈希算法,我们可以快速找到 key 对应的 index,找到了 index 也就找到了对应的 value。
hash = hashfunc(key)index = hash % array_size
但是!哈希算法有个 Hash 冲突 问题,也就是说多个不同的 key 最后得到的 index 相同。
链地址法
开放地址法
hash冲突的解决方案
既然哈希表这么快,为什么 MySQL 没有使用其作为索引的数据结构呢? 主要是因为 Hash 索引不支持顺序和范围查询。
Hash
红黑树
索引结构
数据表的主键列使用的就是主键索引。
一张数据表有只能有一个主键,并且主键不能为 null,不能重复。
在 MySQL 的 InnoDB 的表中,当没有显示的指定表的主键时,InnoDB 会自动先检查表中是否有唯一索引且不允许存在 null 值的字段,如果有,则选择该字段为默认的主键,否则 InnoDB 将会自动创建一个 6Byte 的自增主键。
主键索引(Primary Key)一级索引
二级索引又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。
唯一索引也是一种约束。唯一索引的属性列不能出现重复的数据,但是允许数据为 NULL,一张表允许创建多个唯一索引。 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。
唯一索引(Unique Key)
普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和 NULL。
普通索引(Index)
前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小, 因为只取前几个字符。
前缀索引(Prefix)
全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。Mysql5.6 之前只有 MYISAM 引擎支持全文索引,5.6 之后 InnoDB 也支持了全文索引。
全文索引(Full Text)
二级索引(辅助索引)
索引类型
聚簇索引即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引。
聚簇索引的查询速度非常的快,因为整个 B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。相比于非聚簇索引, 聚簇索引少了一次读取数据的 IO 操作。
查询速度非常快
聚簇索引对于主键的排序查找和范围查找速度非常快。
对排序查找和范围查找优化
因为 B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。
依赖于有序的数据
如果对索引列的数据被修改时,那么对应的索引也将会被修改,而且聚簇索引的叶子节点还存放着数据,修改代价肯定是较大的,所以对于主键索引来说,主键一般都是不可被修改的。
更新代价大
聚簇索引(聚集索引)
非聚簇索引即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引
非聚簇索引的叶子节点并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。
更新代价比聚簇索引要小 。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的
跟聚簇索引一样,非聚簇索引也依赖于有序的数据
这应该是非聚簇索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。
可能会二次查询(回表)
非聚簇索引(非聚集索引)
非聚簇索引不一定回表查询。
那么这个索引的 key 本身就是 name,查到对应的 name 直接返回就行了,无需回表查询。
即使是 MYISAM 也是这样,虽然 MYISAM 的主键索引确实需要回表,因为它的主键索引的叶子节点存放的是指针。但是!如果 SQL 查的就是主键呢?
非聚簇索引一定回表查询吗(覆盖索引)?
聚簇索引与非聚簇索引
覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了,而无需回表查询。
如主键索引,如果一条 SQL 需要查询主键,那么正好根据主键索引就可以查到主键。
再如普通索引,如果一条 SQL 需要查询 name,name 字段正好有索引, 那么直接根据这个索引就可以查到数据,也无需回表。
覆盖索引
使用表中的多个字段创建索引,就是 联合索引,也叫 组合索引 或 复合索引。
联合索引
最左前缀匹配原则指的是,在使用联合索引时,MySQL 会根据联合索引中的字段顺序,从左到右依次到查询条件中去匹配,如果查询条件中存在与联合索引中最左侧字段相匹配的字段,则就会使用该字段过滤一批数据,直至联合索引中全部字段匹配完成,或者在执行过程中遇到范围查询,如 >、<、between 和 以%开头的like查询 等条件,才会停止匹配。
所以,我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。
最左前缀匹配原则
覆盖索引和联合索引
索引下推(Index Condition Pushdown) 是 MySQL 5.6 版本中提供的一项索引优化功能,可以在非聚簇索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数。
索引下推
不为 NULL 的字段
我们创建索引的字段应该是查询操作非常频繁的字段。
被频繁查询的字段
被作为 WHERE 条件查询的字段,应该被考虑建立索引。
被作为条件查询的字段
索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。
频繁需要排序的字段
经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。
被经常频繁用于连接的字段
选择合适的字段创建索引
虽然索引能带来查询上的效率,但是维护索引的成本也是不小的。 如果一个字段不被经常查询,反而被经常修改,那么就更不应该在这种字段上建立索引了。
被频繁更新的字段应该慎重建立索引
因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。
尽可能的考虑建立联合索引而不是单列索引
注意避免冗余索引
前缀索引仅限于字符串类型,较普通索引会占用更小的空间,所以可以考虑使用前缀索引带替普通索引。
考虑在字符串类型的字段上使用前缀索引代替普通索引
使用 SELECT * 进行查询
创建了组合索引,但查询条件未准守最左匹配原则
在索引列上进行计算、函数、类型转换等操作
以 % 开头的 LIKE 查询比如 like '%abc'
查询条件中使用 or,且 or 的前后条件中有一个列没有索引,涉及的索引都不会被使用到
发生隐式转换
避免索引失效
删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 schema_unused_indexes 视图来查询哪些索引从未被使用
删除长期未使用的索引
正确使用索引的方式
索引
redo log是InnoDB存储引擎独有的,它让MySQL拥有了崩溃恢复能力。
比如 MySQL 实例挂了或宕机了,重启时,InnoDB存储引擎会使用redo log恢复数据,保证数据的持久性与完整性。
MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 Buffer Pool 中。
后续的查询都是先从 Buffer Pool 中找,没有命中再去硬盘加载,减少硬盘 IO 开销,提升性能。
更新表数据的时候,也是如此,发现 Buffer Pool 里存在要更新的数据,就直接在 Buffer Pool 里更新。
然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(redo log buffer)里,接着刷盘到 redo log 文件里。
理想情况,事务一提交就会进行刷盘操作,但实际上,刷盘的时机是根据策略来进行的。
Buffer Pool
InnoDB 存储引擎为 redo log 的刷盘策略提供了 innodb_flush_log_at_trx_commit 参数。
设置为 0 的时候,表示每次事务提交时不进行刷盘操作
0
设置为 1 的时候,表示每次事务提交时都将进行刷盘操作(默认值)
1
设置为 2 的时候,表示每次事务提交时都只把 redo log buffer 内容写入 page cache
2
它支持三种策略
刷盘时机
redo log(重做日志)
redo log 它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎。而 binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于MySQL Server 层。不管用什么存储引擎,只要发生了表数据更新,都会产生 binlog 日志。
数据备份
主备
主主
主从
需要依靠binlog来同步数据,保证数据一致性。
主要用处
可以通过binlog_format参数指定
statement
row
mixed
三种格式
记录格式
binlog的写入时机也非常简单,事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中。因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。
写入机制
bin log(归档日志)
redo log(重做日志)让InnoDB存储引擎拥有了崩溃恢复能力。binlog(归档日志)保证了MySQL集群架构的数据一致性。虽然它们都属于持久化的保证,但是侧重点不同。在执行更新语句过程,会记录redo log与binlog两块日志,以基本的事务为单位,redo log在事务执行过程中可以不断写入,而binlog只有在提交事务时才写入,所以redo log与binlog的写入时机不一样。
回到正题,redo log与binlog两份日志之间的逻辑不一致,会出现什么问题?我们以update语句为例,假设id=2的记录,字段c值是0,把字段c值更新成1,SQL语句为update T set c=1 where id=2。假设执行过程中写完redo log日志后,binlog日志写期间发生了异常,会出现什么情况呢?由于binlog没写完就异常,这时候binlog里面没有对应的修改记录。因此,之后用binlog日志恢复数据时,就会少这一次更新,恢复出来的这一行c值是0,而原库因为redo log日志恢复,这一行c值是1,最终数据不一致。为了解决两份日志之间的逻辑一致问题,InnoDB存储引擎使用两阶段提交方案。
原理很简单,将redo log的写入拆成了两个步骤prepare和commit,这就是两阶段提交。使用两阶段提交后,写入binlog时发生异常也不会有影响,因为MySQL根据redo log日志恢复数据时,发现redo log还处于prepare阶段,并且没有对应binlog日志,就会回滚该事务。
方案
二阶段提交
我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚,在 MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用 回滚日志 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。
undo log(回滚日志)
另外,MVCC 的实现依赖于:隐藏字段、Read View、undo log。在内部实现中,InnoDB 通过数据行的 DB_TRX_ID 和 Read View 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改
三大日志
事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用
原子性(Atomicity)
执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的
一致性(Consistency)
并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的
隔离性(Isolation)
一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响
持久性(Durability)
只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!
额外补充一点
ACID 特性
一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据,这也就是脏读的由来。
脏读(Dirty read)
在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
丢失修改(Lost to modify)
指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
不可重复读(Unrepeatable read)
幻读与不可重复读类似。它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
幻读(Phantom read)
MVCC在快照读的情况解决了幻读的问题
当前读的情况只有使用临键锁(行锁)才能解决幻读的问题
innoDB是通过MVCC+临键锁解决幻读
不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改
幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了
不可重复读和幻读有什么区别?
并发事务带来了哪些问题
最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
READ-UNCOMMITTED(读取未提交)
允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
READ-COMMITTED(读取已提交)
对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
REPEATABLE-READ(可重复读)
最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
SERIALIZABLE(可串行化)
事务隔离级别
MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。
SERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。
MySQL 的隔离级别是基于锁实现的吗
事务
又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)
共享锁(S 锁)
又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)
排它锁(X 锁)
按照方式
对整个数据库实例加锁
当需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句
命令:Flush tables with read lock
如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆
如果在从库上备份,那么在备份期间从库不能执行主库同步过来的binlog,会导致主从延迟
使用场景:全库逻辑备份
全局锁
MySQL 中锁定粒度最大的一种锁(全局锁除外),是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。表级锁和存储引擎无关,MyISAM 和 InnoDB 引擎都支持表级锁。
lock table 表名 read
其它线程可以查询sql
表共享读锁(Table Read Lock)
lock table 表名 write
其它线程直接阻塞
表独占写锁(Table Write Lock)
如果需要用到表锁的话,如何判断表中的记录没有行锁呢,一行一行遍历肯定是不行,性能太差。我们需要用到一个叫做意向锁的东东来快速判断是否可以对某个表使用表锁。
事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
意向共享锁(Intention Shared Lock,IS 锁)
事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。
意向排他锁(Intention Exclusive Lock,IX 锁)
意向锁
表锁
读锁之间不互斥,因此可以有多个线程同时对一张表增删改查
当对一个表做增删改查操作的时候,加MDL读锁
读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行
当要对表做结构变更操作的时候,加MDL写锁
MDL (meta data lock 元数据锁)
也被称为记录锁,属于单个行记录上的锁
记录锁(Record Lock)
锁定一个范围,不包括记录本身。
间隙锁(Gap Lock)
锁定一个范围,包括记录本身
临键锁(Next-Key Lock)
在 InnoDB 默认的隔离级别 REPEATABLE-READ 下,行锁默认使用的是 Next-Key Lock。但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。
行锁
按照锁的粒度
在读取的时候生成一个”版本号”,等到其他事务commit了之后,才会读取最新已commit的”版本号”数据。
read commit (读已提交)read commit 生成的是语句级的快照
快照读MVCC影响下,已经解决了幻读的问题(因为它是读历史版本的数据)
而如果是当前读(指的是 select * from table for update),则需要配合间隙锁来解决幻读的问题。
每次读取的都是「当前事务的版本」,即使当前数据被其他事务修改了(commit),也只会读取当前事务版本的数据。
repeatable read (可重复读)repeatable read 生成的是事务级的快照
MVCC通过生成数据快捷,并用快照来提供一定级别的一致性读取目前MVCC只支持read commit (读已提交) 和repeatable read (可重复读)
MVCC
关系型数据库设计表的时候,通常会有一列作为自增主键。InnoDB 中的自增主键会涉及一种比较特殊的表级锁— 自增锁(AUTO-INC Locks) 。更准确点来说,不仅仅是自增主键,AUTO_INCREMENT的列都会涉及到自增锁,毕竟非主键也可以设置自增长。
传统模式
连续模式(MySQL 8.0 之前默认)
交错模式(MySQL 8.0 之后默认)
如果一个事务正在插入数据到有自增列的表时,会先获取自增锁,拿不到就可能会被阻塞住。这里的阻塞行为只是自增锁行为的其中一种,可以理解为自增锁就是一个接口,其具体的实现有多种。具体的配置项为 innodb_autoinc_lock_mode (MySQL 5.1.22 引入),可以选择的值如下:
自增锁
锁
快照读(一致性非锁定读)就是单纯的 SELECT 语句。
SELECT ... FOR UPDATE
SELECT ... LOCK IN SHARE MODE
但不包括下面这两类 SELECT 语句:
快照即记录的历史版本,每行记录可能存在多个历史版本(多版本技术)。
快照读的情况下,如果读取的记录正在执行 UPDATE/DELETE 操作,读取操作不会因此去等待记录上 X 锁的释放,而是会去读取行的一个快照。
在 RC 级别下,对于快照数据,一致性非锁定读总是读取被锁定行的最新一份快照数据。
在 RR 级别下,对于快照数据,一致性非锁定读总是读取本事务开始时的行数据版本。
只有在事务隔离级别 RC(读取已提交) 和 RR(可重读)下,InnoDB 才会使用一致性非锁定读:
快照读比较适合对于数据一致性要求不是特别高且追求极致性能的业务场景。
快照读
当前读 (一致性锁定读)就是给行记录加 X 锁或 S 锁。
# 对读的记录加一个X锁SELECT...FOR UPDATE
# 对读的记录加一个S锁SELECT...LOCK IN SHARE MODE
# 对修改的记录加一个X锁INSERT...UPDATE...DELETE...
当前读的一些常见 SQL 语句类型如下:
当前读
快照读和当前读
读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。小幅提升写性能,大幅提升读性能。
主库将数据库中数据的变化写入到 binlog从库连接主库从库会创建一个 I/O 线程向主库请求更新的 binlog主库会创建一个 binlog dump 线程来发送 binlog ,从库中的 I/O 线程负责接收从库的 I/O 线程将接收的 binlog 写入到 relay log 中。从库的 SQL 线程读取 relay log 同步数据本地(也就是再执行一遍 SQL )。
主从复制
写完主库之后,主库的数据同步到从库是需要时间的,这个时间差就导致了主库和从库的数据不一致性问题
既然你从库的数据过期了,那我就直接从主库读取
会增加主库的压力
强制将读请求路由到主库处理
对于一些对数据比较敏感的场景,你可以在完成写请求之后,避免立即进行请求操作
延迟读取
主从同步延迟
读写分离
就是把单一数据库按照业务进行划分,不同的业务使用不同的数据库,进而将一个数据库的压力分担到多个数据库
垂直分库
把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,这样就实现了水平扩展,解决了单表的存储和性能瓶颈的问题。
水平分库
分库
对数据表列的拆分,把一张列比较多的表拆分为多张表
子主题
垂直分表
对数据表行的拆分,把一张行比较多的表拆分为多张表,可以解决单一表数据量过大的问题
水平分表
分表
单表的数据达到千万级别以上,数据库读写速度比较缓慢
数据库中的数据占用的空间越来越大,备份时间越来越长
应用的并发量太大
什么情况分库分表
求指定 key(比如 id) 的哈希,然后根据哈希值确定数据应被放置在哪个表中。哈希分片比较适合随机读写的场景,不太适合经常需要范围查询的场景。
哈希分片
按照特性的范围区间(比如时间区间、ID区间)来分配数据,比如 将 id 为 1~299999 的记录分到第一个库, 300000~599999 的分到第二个库。范围分片适合需要经常进行范围查找的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。
范围分片
很多 NewSQL 数据库都支持地理位置分片算法,也就是根据地理位置(如城市、地域)来分配数据。
地理位置分片
灵活组合多种分片算法,比如将哈希分片和范围分片组合。
融合算法
分片算法
同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。这样就导致我们需要手动进行数据的封装,比如你在一个数据库中查询到一个数据之后,再根据这个数据去另外一个数据库中找对应的数据。
join 操作
同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。
事务问题
分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式 id 了。
分布式 id
带来的问题
分库分表
停机迁移
我们对老库的更新操作(增删改),同时也要写入新库(双写)。如果操作的数据不存在于新库的话,需要插入到新库中。 这样就能保证,咱们新库里的数据是最新的。
在迁移过程,双写只会让被更新操作过的老库中的数据同步到新库,我们还需要自己写脚本将老库中的数据和新库的数据做比对。如果新库中没有,那咱们就把数据插入到新库。如果新库有,旧库没有,就把新库对应的数据删除(冗余数据清理)。
重复上一步的操作,直到老库和新库的数据一致为止。
双写方案
分库分表后怎么迁移数据
Mysql
数据库
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
封装
继承实现了 IS-A 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal,从而获得 Animal 非 private 的属性和方法。继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 向上转型 。
继承
编译时多态主要指方法的重载
编译时多态
运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定
覆盖(重写)
向上转型
三个条件
运行时多态
多态
三大特征
代表最终、不可改变的
当前这个类不能有任何的子类
一个类如果是final的,那么其中所有的成员方法都无法进行覆盖重写(因为没有子类)
修饰类
修饰一个方法的时候,这个方法就是最终方法,也就是不能被覆盖重写
对于类、方法来说,abstract关键字和final关键字不能同时使用,因为矛盾。有抽象方法的abstract类被继承时,其中的方法必须被子类Override,而final不能被Override。
修饰方法
对于基本类型来说,不可变说的是变量当中的数据不可改变
对于引用类型来说,不可变说的是变量当中的地址值不可改变
修饰局部变量
对于成员变量来说,如果使用final关键字修饰,那么这个变量也照样是不可变
由于成员变量具有默认值,所以用了final之后必须手动赋值,不会再给默认值
对于final的成员变量,要么使用直接赋值,要么通过构造方法赋值。二者选其一
必须保证类当中所有重载的构造方法,都最终会对final的成员变量进行赋值
如果选择在构造方法中赋值,则要把set函数取消掉
修饰成员变量
final关键字
抽象类可以有静态方法;接口在JDK8之前不能有静态方法,在JDK8中可以有静态方法,且只能被接口类直接调用(不能被实现类的对象调用)
抽象类中的方法可以是public、protected; 接口方法在JDK8之前只有public abstract,在JDK8可以有default方法
抽象类和接口的区别
本质this指向本对象的指针。super是一个关键字
this和super
泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除
类型擦除
可以接受任何继承自T的类型
<? extends T>
可以接受任何T的父类构成
<? super T>
限定通配符
可以用任意类型来替代
<?>
非限定通配符
泛型
IndexOutOfBoundsException
NullPointerException
IIIegalArgumentException
ClassCastExcetpion
运行时异常(RuntimeException)
IOException
SQLException
ClassNotFoundException
非运行时异常(NonRunTimeException)
异常(Exception)
IOError
ThreadDeathError
AssertionError
错误(Error)
是 Java 语言中所有错误与异常的超类
throw = 异常的抛出
throws = 异常的声明
throw和throws
Throwable
在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性
User.class
根据类名
new User().getClass()
根据对象
Class.forName(\"com.test.User\")
根据全限定类名
获取Class对象的三种方式
反射
下标寻址十分迅速,但计算机的内存是有限的,故数组的长度也是有限的
无序数组的查找最坏情况需要遍历整个数组
可以通过二分查找提快查找的速度
任何一种数组无法解决的问题就是插入、删除操作比较复杂
因此,在一个增删查改比较频繁的数据结构中,数组不会被优先考虑
数组
它的结构特点被证明根本不适合进行查找
但链表在增删改的速度上面很快
普通链表
数组和链表的折中,同时它的设计依赖散列函数的设计
数组不能无限长、链表也不适合查找,所以也不适合大规模的查找
哈希表
因为可能退化成链表,同样不适合进行查找
二叉查找树
为了解决二叉查找树可能退化成链表问题
AVL树是严格的平衡二叉树
不管我们是执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡,而旋转是非常耗时的,由此我们可以知道AVL树适合用于插入与删除次数比较少,但查找多的情况
AVL树(平衡二叉树)
二叉查找树和AVL树的折中
红黑树是一种弱平衡二叉树
但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)
通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍
它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,我们就用红黑树
是大规模数据存储中,实现索引查询这样一个实际背景下,树节点存储的元素数量是有限的(如果元素数量非常多的话,查找就退化成节点内部的线性查找了),这样导致二叉查找树结构由于树的深度过大而造成磁盘I/O读写过于频繁,进而导致查询效率低下
多路查找树
自平衡二叉查找树不同,B树适用于读写相对大的数据块的存储系统,例如磁盘。它的应用是文件系统及部分非关系型数据库索引
B树
B树基础上,为叶子结点增加链表指针(B树+叶子有序链表),所有关键字都在叶子结点 中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中。通常用于关系型数据库(如Mysql)和操作系统的文件系统中
B+树
B*树
用来做空间数据存储的树状数据结构。例如给地理位置,矩形和多边形这类多维数据建立索引
R树
自然语言处理中最常用的数据结构,很多字符串处理任务都会用到。Trie树本身是一种有限状态自动机,还有很多变体。什么模式匹配、正则表达式,都与这有关
Trie树
数据结构
分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解
分治算法
通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解
和分治算法最大的差别:适用于动态规划算法求解的问题经过分解后得到的子问题往往不是相互独立的,而是下一个子阶段的求解是建立在上一个子阶段的解的基础上的
动态规划算法
保证每次操作都是局部最优的,并且最后得到的结果是全局最优的
贪心算法
比如重要的二分法,比如二分查找;二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列
二分法
BFS(广度优先搜索)
Backtracking(回溯)
DFS(深度优先搜索)
搜索算法
常见的算法思想
常见的排序算法
它是一种较简单的排序算法。它会遍历若干次要排序的数列,每次遍历时,它都会从前往后依次的比较相邻两个数的大小;如果前者比后者大,则交换它们的位置。这样,一次遍历之后,最大的元素就在数列的末尾! 采用相同的方法再次遍历时,第二大的元素就被排列在最大元素之前。重复此操作,直到整个数列都有序为止
冒泡排序(Bubble Sort)
它的基本思想是: 选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分;其中一部分的所有数据都比另外一部分的所有数据都要小。然后,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
快速排序(Quick Sort)
直接插入排序(Straight Insertion Sort)的基本思想是: 把n个待排序的元素看成为一个有序表和一个无序表。开始时有序表中只包含1个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,将它插入到有序表中的适当位置,使之成为新的有序表,重复n-1次可完成排序过程
插入排序(Insertion Sort)
希尔排序实质上是一种分组插入方法。它的基本思想是: 对于n个待排序的数列,取一个小于n的整数gap(gap被称为步长)将待排序元素分成若干个组子序列,所有距离为gap的倍数的记录放在同一个组中;然后,对各组内的元素进行直接插入排序。 这一趟排序完成之后,每一个组的元素都是有序的。然后减小gap的值,并重复执行上述的分组和排序。重复这样的操作,当gap=1时,整个数列就是有序的
Shell排序(Shell Sort)
它的基本思想是: 首先在未排序的数列中找到最小(or最大)元素,然后将其存放到数列的起始位置;接着,再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕
选择排序(Selection sort)
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点
堆排序(Heap Sort)
将两个的有序数列合并成一个有序数列,我们称之为\"归并\"。归并排序(Merge Sort)就是利用归并思想对数列进行排序
归并排序(Merge Sort)
桶排序的原理很简单,将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)
桶排序(Bucket Sort)
基数排序(Radix Sort)
算法
基础
可以直接根据下标定位位置
查询和更新数据比较快
可以设置值为null
默认扩容因子是1.5
初始长度为10
动态数组
删除时需要把后面的数据往前前移一位
指定位置的新增,同样,会把后面的数据往后后移一位
删除和指定位置的新增会比较慢
线程不安全
ArrayList
因为它是链表形式,在删除和指定新增时,只需要讲后面的和前面的连起来/断开
删除和新增会比较快
双向链表
因为它是链表形式,在查询是需要逐渐查找
查询和新增会比较慢
LinkedList
线程安全
在每个方法里面使用了Synchronized块
效率慢
Collections.synchronizedList
效率相比其它线程安全比较快
使用ReentrantLock来实现
每一次增删改对数组进行了copy
使用的安全失败,其它用的都是快速失败
它的写性能很慢,因为每一次写都需要对数组进行copy
CopyOnWriteArrayList
和synchronizedList大致一样
Vector
Vector是直接在方法上面写了synchronized
Collections.synchronizedList是在方法里面使用了synchronized块
Vector和Collections.synchronizedList的区别
List
有序
插入和删除比较快
基于红黑树来实现TreeSet
不存在重复的key
查询和更新会比较慢
不能写入空数据
TreeSet
查询和更新会比较快
可以写入空数据
底层是HashMap
key不重复
插入和删除比较慢
无序
HashSet
基本和HashSet一样,只不过,它是一个有序的HashSet
LinkedHashSet
基本和CopyOnWriteArrayList一样,都是基于安全失败,对数据在写时进行copy
CopyOnWriteArraySet
和Collections.synchronizedList()一样,都是在方法里面对方法加了synchronized块
Collections.synchronizedSet()
Set<String> setFromMap = Collections.newSetFromMap(new ConcurrentHashMap<>());
封装了ConcurrentHashMap,利用ConcurrentHashMap的性质确保生成的Set是线程安全的。
Collections.newSetFromMap()
Set
入列(添加元素)时,如果元素数量超过队列总数,会进行等待(阻塞),待队列的中的元素出列后,元素数量未超过队列总数时,就会解除阻塞状态,进而可以继续入列;出列(删除元素)时,如果队列为空的情况下,也会进行等待(阻塞),待队列有值的时候即会解除阻塞状态,进而继续出列;阻塞队列的好处是可以防止队列容器溢出;只要满了就会进行阻塞等待;也就不存在溢出的情况;只要是阻塞队列,都是线程安全的;
一个支持延时获取元素的无界阻塞队列
DelayQueue
一个由链表结构组成的无界阻塞队列
LinkedTransferQueue
ArrayBlockingQueue
最多只能存储一个元素,每一个put操作必须等待一个take操作,否则不能继续添加元素
SynchronousQueue
一个带优先级的队列,而不是先进先出队列。元素按优先级顺序被移除,而且它也是无界的,也就是没有容量上限,虽然此队列逻辑上是无界的,但是由于资源被耗尽,所以试图执行添加操作可能会导致 OutOfMemoryError 错误
PriorityBlockingQueue
阻塞队列
不管出列还是入列,都不会进行阻塞,入列时,如果元素数量超过队列总数,则会抛出异常,出列时,如果队列为空,则取出空值;
ConcurrentLinkedQueue
ConcurrentLinkedDeque
内部基于数组实现,线程不安全的队列
PriorityQueue
非阻塞队列
阻塞
有界限,大小长度受限制
有界
无限大小,其实说是无限大小,其实是有界限的,只不过超过界限时就会进行扩容,就行ArrayList 一样,在内部动态扩容
无界
界限
每个元素中除了元素本身之外,还存储一个指针,这个指针指向下一个元素
单向链表
除了元素本身之外,还有两个指针,一个指针指向前一个元素的地址,另一个指针指向后一个元素的地址
链表
Queue
Collection
不存在hash冲突
基于红黑树来实现的
key不能为null
NavigableMap父类是SoredMap
它是基于NavigableMap接口来实现有序的
TreeMap相比于HashMap,它是有序的
相比于HashMap来说,在增删改上,没有HashMap性能高
TreeMap
效率快
基于动态数组,链表,(1.8之后多了红黑树)
key,value可以为null
会出现Hash冲突
HashMap
能保证插入顺序和访问顺序
key,value允许为空
key不允许重复,value允许重复
添加 比较快
底层是基于双向链表实现
查询比较慢
LinkedHashMap
数组加链表
动态数组扩容
默认长度是11,每次扩容是当前容量*2+1
有点类似vector,在每个方法上添加锁
增删改查慢
为什么hashmap允许,是因为hashmap在hash方法时对null做了特殊处理,而为什么hashtable的value不能为null,是代码做了限制,如果设置为null会报错
不允许key和value为null
需要重写hsahcode和equals方法
HashTable
1.7 使用分段锁和ReentrantLock 来保证线程安全
1.8 使用 CAS + Synchronized来保证线程安全
ConCurrentHashMap使用乐观锁的形式来保证安全
HashTable使用悲观锁的形式来保证安全
相比于HashTable 效率比它高
不允许KEY和Value为空
ConCurrentHashMap
map
快速失败(fail—fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
快速失败(fail-fast)
安全失败(fail—safe)大家也可以了解下,java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
安全失败(fail-safe)
快速失败和安全失败
集合
在线程安全实现中会细节讲解Synchronized
synchronized
通过内存屏障来防止指令重排
防重排序
将被修改的变量,在被修改后可以立即同步到主内存,被修改的变量在每次被使用之前都从主内存刷新,其实本质也是通过内存屏障来实现可见性
可见性
不能完全保证,只能保证单次的读/写操作具有原子性
保证原子性:单次读/写
通过happens-before规则来实现有序性
有序性
volatile
当final关键字修饰一个类,则该类会成为最终类,即该类不能被继承,但是该类可以有父类
当final关键字修饰了成员方法,则意味着这个方法不能被重写,但是可以被继承
该成员变量必须在其所在类对象创建之前被初始化(且只能被初始化一次)
成员变量
该变量必须在使用之前赋值,且只能被赋值一次
局部变量
修饰变量
final
三个关键字
对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行
原子性
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性
可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。当然JMM是通过Happens-Before 规则来保证有序性的
三个特性
是通过对象头部的Mark Word中的锁标识+monitor实现的
实现原理
标记字段-运行时数据,如哈希码、GC信息以及锁信息
Mark Word(运行时数据)
对象锁代表的类的元数据指针
Klass(元数据指针)
ArrayLength(数组长度,数组对象才有该部分)
java对象头
synchronized通过Monitor来实现线程同步和协作
依赖的是操作系统的Mutex(互斥锁量)只有拥有互斥量的线程才能进入临界区,不拥有的只能阻塞等待,会维护一个阻塞的队列
同步
依赖的是synchronized持有的对象,对象可以让多个线程方便同步,还可以通过对象调用wait方法释放锁让线程进入等待队列,等其他线程调用对象的notify和notifyAll方法进行唤醒可以重新获取锁
协作
Monitor用来进行监听锁的持有和调度锁的持有的。持有的对象可以理解为锁的一个媒介,可以使用它方便操作同步和协作
这套监听锁和调度锁包括使用的互斥量其实都是比较消耗资源的,所以使用它的成为“重量级锁”
monitor
synchronized void method() { //业务代码}
作用于当前对象实例加锁,这样锁的是当前对象,this
修饰当前实例方法
synchronized void staic method() { //业务代码}
给当前类加锁,锁的是这个类对象,Object.class
修饰静态方法
synchronized(this) { //业务代码}
指定加锁对象,对给定对象/类加锁。synchronized(this / object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得当前 class 的锁
修饰代码块
synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类(Objcet.class)上锁
synchronized 关键字加到实例方法上是给对象实例(this)上锁
总的来说
三种用法
通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间
重量型锁
当对象头中锁标志位为01,是否偏向锁为0时表示使无锁的状态。想要在无锁的时候实现同步可以使用上面乐观锁中实现-CAS。
无锁
在对象头中进行标记,表示有线程进入了临界区,在只有一个线程访问的时候既不用使用CAS也不用引入较重的monitor
线程不会主动释放偏向锁,只有遇到其他线程进尝试竞争偏向锁时,需要等待全局安全点(在这个时间点上没有执行的字节码),它会首先暂停拥有偏向锁的的线程,判断它是否还活着,如果死亡了就恢复到无锁状态其他线程就可以占用,如果还在临界区就对锁进行升级成\"轻量锁\"
每个线程进入临界区的时候都会查看对象头锁标识是否是偏向锁,是偏向锁的话,会判断当前线程和对象头中的线程id是同一个线程则直接进入,如果不是则进行CAS看是不是能比较替换成功(防止马上就释放了),如果没成功就会暂停持有偏向锁的线程,看线程是否已经不再用锁了,如果没用就释放,给新进来的线程占用,如果在用就进行锁升级生成轻量锁\"
JVM参数关闭偏向锁:-XX:-UseBiasedLocking,那么程序会默认进入轻量级锁状态
关闭偏向锁
偏量锁是一种锁的优化,它本质上不是锁,只是对象头中进行了标记,如果没有多线程并发访问临界区的时候可以减少开销,如果出现多并发的时候会进行升级
偏向锁
一般用于两个线程在交替使用锁的时候,由于没有同时抢锁,属于一种比较和谐的状态,就可以使用轻量级锁
线程在执行同步代码块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁
轻量级锁加锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁
轻量级锁解锁
轻量级锁在加锁过程中,用到了自旋锁。所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的
因为绝大多数情况下线程获得锁和释放锁的过程都是非常短暂的,自旋一定次数之后极有可能碰到获得锁的线程释放锁,所以,轻量级锁适用于那些同步代码块执行很快的场景,这样,线程原地等待很短的时间就能够获得锁了
为什么使用自旋锁
锁在原地循环等待的时候,是会消耗CPU资源的。所以自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么等待锁的线程会不断的循环反而会消耗CPU资源
可以使用-XX:PreBlockSpin参数来设置自旋锁等待的次数
默认情况下锁自旋的次数是 10 次
设置自旋次数
自旋锁
在 JDK1.7 开始,引入了自适应自旋锁,修改自旋锁次数的JVM参数被取消,虚拟机不再支持由用户配置自旋锁次数,而是由虚拟机自动调整。自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源
自适应自旋
轻量级锁
锁的类型
也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁
锁粗化
通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护,通过逃逸分析也可以在线程本地Stack上进行对象空间的分配(同时还可以减少Heap上的垃圾收集开销)
锁消除
这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒
是为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟
当线程在获取轻量级锁的过程中执行CAS操作失败时,在进入与monitor相关联的操作系统重量级锁(mutex semaphore)前会进入忙等待(Spinning)然后再次尝试,当尝试一定的次数后如果仍然没有成功则调用与该monitor关联的semaphore(即互斥锁)进入到阻塞状态
锁优化
重量级锁
轻量级锁(包括自旋锁)->
当新线程进来后,会先通过自旋,判断上一个线程是否释放轻量锁,如果一直没有释放,到了自旋阈值,会自动升级为重量级锁
偏向锁->
当现在有另一个线程进入时,会先判断之前拥有偏向锁的线程是否存活,如果存活,升级轻量级锁,如果没有存活,释放偏向锁,让其它线程占用
无锁->
当有一个独占线程进入时,会在这个线程的对象头上添加Mark Word 状态,此时升级到偏向锁
锁升级
当某个线程已经获得了该锁时,再次调用lock()方法可以再次立即获得该锁
某个线程在执行methodA()时,假设已经获得了锁,这是当它执行到methodB()时可以立即获得methodB里面的锁,因为两个方法是调用的同一把锁
public static void methodA(){ try{ lock.lock(); //dosomething methodB(); }finally{ lock.unlock(); } } public static void methodB(){ try{ lock.lock(); //dosomthing }finally{ lock.unlock(); } }
synchronized也是可重入锁
可重入锁
ReentrantLock 支持公平锁,但默认是非公平锁
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();}
lock方法
创建一个公平锁
public ReentrantLock() { sync = new NonfairSync();}
final void lock() { acquire(1);}
创建一个非公平锁
从两个lock()方法我们可以看到,它们的不同点是在调用非公平锁的lock()方法时,当前线程会尝试去获取锁,如果获取失败了则调用acquire(1)方法入队列等待;而调用公平锁的lock()方法当前线程会直接入队列等待
公平锁
AQS方法一中如果当前线程被中断则抛出InterruptedException,否则尝试去获取锁,获取成功则返回,获取失败则调用aqs方法二doAcquireInterruptibly()
AQS方法二中在for循环线程自旋中也会判断当前线程是否被标记为中断,如果是也会抛出InterruptedException。
ReentrantLock有一个lockInterruptibly()方法,它会最终调用AQS的两个方法
可中断
ReentrantLock提供了超时机制,当调用tryLock()方法,当前线程如果获取锁失败会立刻返回;而当调用带参tryLock()方法是,当前线程如果在设置的timeout时间内未获得锁,也会立刻返回
可定时
ReentrantLock类具有完全互斥排他的效果,同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务,这样做虽然保证了同时写实例变量的线程安全性,但效率是非常低下的
ReentrantReadWriteLock 类,使用它可以在进行读操作时不需要同步执行,提升运行速度,加快运行效率。读写锁有两个锁:一个是读操作相关的锁,也称共享锁;另一个是写操作相关的锁,也称排他锁。读锁之间不互斥,读锁和写锁互斥,写锁与写锁互斥,因此只要出现写锁,就会出现互斥同步的效果。读操作是指读取实例变量的值,写操作是指向实例变量写入值
使用的是ReentrantReadWriteLock的读锁,所以要使readLock.readLock().lock()和readLock.readLock().unlock()进行上锁和解锁
ReentrantReadWriteLock 读读共享
使用了写锁的效果就变成同一时间只允许一个线程执行lock()之后的方法的代码
ReentrantReadWriteLock 写写互斥
ReentrantReadWriteLock锁
ReentrantLock
互斥同步
Compare-And-Swap (对比和交换)
CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换
使用版本号或者时间戳可以解决
ABA问题
循环时间长开销大
只能保证一个共享变量的原子操作
CAS存在的问题
CAS
原子更新布尔类型
AtomicBoolean
原子更新整型
AtomicInteger
原子更新长整型
AtomicLong
原子更新基本类型
原子更新整型数组里的元素
AtomicIntegerArray
原子更新长整型数组里的元素
AtomicLongArray
原子更新引用类型数组里的元素
AtomicReferenceArray
原子更新数组
原子更新整型的字段的更新器
AtomicIntegerFieldUpdater
原子更新长整型字段的更新器
AtomicLongFieldUpdater
原子更新带有版本号的引用类型
AtomicStampedFieldUpdater
上面已经说过此处不在赘述
AtomicReferenceFieldUpdater
原子更新引用类型
AtomicReference
AtomicStampedReference
原子更新带有标记位的引用类型
AtomicMarkableReferce
原子更新字段类
CAS+volatile
volatile保证线程的可见性,多线程并发时,一个线程修改数据,可以保证其它线程立马看到修改后的值CAS 保证数据更新的原子性
原子类
非阻塞同步
要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的
栈封闭
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
线程本地存储(Thread Local Storage)
具体实现
无同步方案
线程安全具体实现
并发是指一个处理器同时处理多个任务
并发
并行是指多个处理器或者是多核的处理器同时处理多个不同的任务
并行
并发和并行
继承 Thread 类
实现 Runnable 接口
实现 Callable 接口
创建线程的几种方式
创建后尚未启动
新建(New)
可能正在运行,也可能正在等待 CPU 时间片
可运行(Runnable)
等待获取一个排它锁,如果其线程释放了锁就会结束此状态
阻塞(Blocking)
等待其它线程显式地唤醒,否则不会被分配 CPU 时间片
无限期等待(Waiting)
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒
限期等待(Timed Waiting)
可以是线程结束任务之后自己结束,或者产生了异常而结束
死亡(Terminated)
线程状态
线程状态转变流程
管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作
一个任务创建一个线程
CachedThreadPool
所有任务只能使用固定大小的线程
FixedThreadPool
相当于大小为 1 的 FixedThreadPool
SingleThreadExecutor
Executor
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程
main() 属于非守护线程。使用 setDaemon() 方法将一个线程设置为守护线程。
Daemon
Thread.sleep(millisec) 方法会休眠当前正在执行的线程
sleep()
对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
yield()
基础线程机制
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
interrupt()
如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
interrupted()
调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。
Executor.shutdown()
线程中断的方式
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
join()
调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。
wait() notify() notifyAll()
线程间协作的方式
每个Thread维护了一个ThreadLocalMap,这个映射表的Key是ThreadLocal,Value是要存的值,同时Key是作为弱引用使用,弱引用对象会在GC时被回收。ThreadLocal对象作为key使用,而ThreadLocalMap和Thread生命周期一致,当ThreadLocal对象被回收时如果当前线程未结束,就会存在key已经为null,value访问不到的情况而导致内存泄漏。这里弱引用的使用会在ThreadLocal的get或者set方法在某些时候被调用时会调用expungeStaleEntry方法用来清除Entry中Key为null的Value,但是这是不及时的,也不一定每次都能执行,所以需要在使用之后调用remove()方法来显示调用expungeStaleEntry方法进行回收。其本质问题是ThreadLocalMap的生命周期和Thread一样长,没及时remove时线程没有结束就会导致内存泄漏。弱引用的使用增加了回收的机会,一点程度上避免了泄漏。当使用线程池和ThreadLocal时要注意线程是不断重复的,不手动删除会导致value的积累
内存泄漏原因
用来提供线程内部的局部变量
这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量
用途
每个Thread维护一个ThreadLocalMap哈希表,这个哈希表的key是ThreadLocal实例本身,value才是真正要存储的值Object。
实现
1.获取当前线程2.获取当前线程维护的ThreadLocalMap3.判断这个Map是否为空4.如果不为空,则将当前ThreadLocal作为Key,添加进map中or4.如果为空,则调用构造方法生成默认大小的map,当前ThreadLocal作为Key,添加进新map中
set
1.获取当前线程2.获取当前线程维护的ThreadLocalMap3.判断这个Map是否为空4.如果不为空,则调用ThreadLocalMap的getEntry方法传入当前ThreadLocal获取值,判断值是否存在,如果存在,则返回or4.如果map不存在或者值不存在,则获取初始化值,和当前ThreadLocal一起set进map(map不存在则创建新的map)中,并放回初始value
get
1.获取当前线程2.获取当前线程维护的ThreadLocalMap3.判断这个Map是否为空4.如果不为空,则调用ThreadLocalMap中的remove方法,删除当前ThreadLocal作为Key对应Map中的值
remove
常规操作底层实现
ThreadLocals依赖于附加的每线程线性探测哈希映射
threadLocalHashCode = nextHashCode()
下一个要给出的哈希代码
nextHashCode = new AtomicInteger()
连续生成的哈希代码之间的差异-圈数
HASH_INCREMENT = 0x61c88647
threshold = len * 2 / 3
扩容阈值
INITIAL_CAPACITY = 16
初始容量
ThreadLocalMap中的变量
变量
ThreadLocal
管理线程,避免增加创建线程和销毁线程的资源损耗
提高响应速度
重复利用
线程池核心线程数最大值
corePoolSize
线程池最大线程数大小
maximumPoolSize
线程池中非核心线程空闲的存活时间大小
keepAliveTime
线程空闲存活时间单位
unit
存放任务的阻塞队列
workQueue
用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题
threadFactory
线城池的饱和策略事件,主要有四种类型
handler
核心参数
执行流程
抛出一个异常,默认的
AbortPolicy
直接丢弃任务
DiscardPolicy
丢弃队列里最老的任务,将当前这个任务继续提交给线程池
DiscardOldestPolicy
交给线程池调用所在的线程进行处理
CallerRunsPolicy
拒绝策略
一个用数组实现的有界阻塞队列,按FIFO排序量
ArrayBlockingQueue(有界队列)
基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列
LinkedBlockingQueue(可设置容量队列)
是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
DelayQueue(延迟队列)
是具有优先级的无界阻塞队列
PriorityBlockingQueue(优先级队列)
一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene。newCachedThreadPool线程池使用了这个队列。
SynchronousQueue(同步队列)
工作队列
核心线程数和最大线程数大小一样
没有所谓的非空闲时间,即keepAliveTime为0
阻塞队列为无界队列LinkedBlockingQueue
工作流程
newFixedThreadPool(固定数目线程的线程池)
核心线程数为0
最大线程数为Integer.MAX_VALUE
阻塞队列是SynchronousQueue
非核心线程空闲存活时间为60秒
newCachedThreadPool(可缓存线程的线程池)
核心线程数为1
最大线程数也为1
阻塞队列是LinkedBlockingQueue
keepAliveTime为0
newSingleThreadExecutor(单线程的线程池)
阻塞队列是DelayedWorkQueue
scheduleAtFixedRate() :按某种速率周期执行
scheduleWithFixedDelay():在某个延迟后执行
newScheduledThreadPool(定时及周期执行的线程池)
常用线程池
该状态的线程池会接收新任务,并处理阻塞队列中的任务
调用线程池的shutdown()方法,可以切换到SHUTDOWN状态
调用线程池的shutdownNow()方法,可以切换到STOP状态
RUNNING
该状态的线程池不会接收新任务,但会处理阻塞队列中的任务
SHUTDOWN
该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务
STOP
该状态表明所有的任务已经运行终止,记录的任务数量为0
terminated()执行完毕,进入TERMINATED状态
TIDYING
该状态表示线程池彻底终止
TERMINATED
状态切换图
线程池各个状态
线程池
线程
一个Valatile 的 int 变量 state
一个双向队列(FIFO) 取名CLH,根据创始人名而来
在得到一个独占锁之前,会先尝试获取独占锁,如果获取到了返回true,如果没有获取到,将这个线程加入队列中(尾插法的形式,如果队列为空,则会添加一个空的头结点)
添加一个独占锁
尝试获取独占锁,可获取返回true,否则false
tryAcquire(int)
acquire
尝试释放成功后,即从头结点开始唤醒其后继节点,如后继节点被取消,则转为从尾部开始找阻塞的节点将其唤醒。阻塞节点被唤醒后,即进入acquireQueued中的for(;;)循环开始新一轮的资源竞争。
释放一个独占锁
尝试释放独占锁,可释放返回true,否则false
tryRelease(int)
release
独占
与独占锁不同的是,由于共享锁是可以被多个线程获取的,因此在首个阻塞节点被唤醒后,会通过setHeadAndPropagate传递唤醒后续的阻塞节点
一直会传递到下一个需要独占锁的线程前
添加一个共享锁
尝试以共享方式获取锁,失败返回负数,只能获取一次返回0,否则返回个数
tryAcquireShared(int)
acquireShared
和独占锁流程一样
释放一个共享锁
尝试释放共享锁,可获取返回true,否则false
tryReleaseShared(int)
releaseShared
共享
提供的模式
节点引用线程由于等待超时或被打断时的状态
CANCELLED = 1
后继节点线程需要被唤醒时的当前节点状态。当队列中加入后继节点被挂起(block)时,其前驱节点会被设置为SIGNAL状态,表示该节点需要被唤醒
SIGNAL = -1
当节点线程进入condition队列时的状态
CONDITION = -2
仅在释放共享锁releaseShared时对头节点使用
PROPAGATE = -3
节点初始化时的状态
waitStatus(当前节点的状态)
前驱节点
prev
后继节点
next
引用线程,头节点不包含线程
thread
condition条件队列
nextWaiter
Node
使用了模板设计模式
AQS
LockSupport提供的park/unpark是以线程的角度来设计,真正解耦了线程之间的同步
对当前线程执行阻塞操作,直到获取到可用许可后才解除阻塞,也就相当于当前线程进入阻塞状态
park()
对当前线程执行阻塞操作,等待获取到可用许可后才解除阻塞,最大的等待时间由传入的参数来指定,一旦超过最大时间它也会解除阻塞
parkNanos(long)
对当前线程执行阻塞操作,等待获取到可用许可后才解除阻塞,最大的等待时间为参数所指定的最后期限时间
parkUntil(long)
参数不含阻塞对象
与park()方法同义,但它多传入的参数为阻塞对象
park(Object)
与parkNanos(long)同义,但指定了阻塞对象
与parkUntil(long)同义,但指定了阻塞对象
参数包含阻塞对象
park开头的方法用于执行阻塞操作
将指定线程的许可置为可用,也就相当于唤醒了该线程
unpark(Thread)
unpark开头的方法用于执行唤醒操作
核心方法
对于LockSupport使用的许可可看成是一种二元信号,该信号分有许可和无许可两种状态。每个线程都对应一个信号变量,当线程调用park时其实就是去获取许可,如果能成功获取到许可则能够往下执行,否则则阻塞直到成功获取许可为止。而当线程调用unpark时则是释放许可,供线程去获取。park/unpark方式的执行顺序不影响唤醒,不会造成死锁
许可机制
park方法支持中断,也就是说一个线程调用park方法进入阻塞后,如果该线程被中断则能够解除阻塞立即返回。但需要注意的是,它不会抛出中断异常,所以我们不必去捕获InterruptedException
支持中断
LockSupport
ByteArrayInputStream
PipedInputStream
BufferedInputStream
DataInputStream
FilterInputStream
FileInputStream
InputStream
ByteArrayOutputStream
PipedOutputStream
BufferedOutputStream
DataOutputStream
PrintStream
FilterOutputStream
FileOutputStream
ObjectOutputStream
OutputStream
字节流(给计算机看的)
CharArrayReader
PipedReader
FilterReader
BufferedReader
FileReader
InputStreamReader
Reader
CharArrayWriter
PipedWriter
FilterWriter
BufferedWriter
FileWriter
OutputStreamWriter
PrintWriter
Writer
字符流(给人看的)
传输方式
文件(File)
数组(Array)
管道(Piped)
基本数据类型
缓冲(Buffered)
PintWriter
打印(Print)
ObjectInputStream
对象序列化反序列化
转换
数据操作
应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回
你早上去买炸油条,你点单,之后一直等店家做好,期间你啥其它事也做不了。(你就是应用级别,店家就是操作系统级别, 应用被阻塞了不能做其它事)
举例
同步阻塞IO(bloking IO)
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)
你早上去买现炸油条,你点单,点完后每隔一段时间询问店家有没有做好,期间你可以做点其它事情。(你就是应用级别,店家就是操作系统级别,应用可以做其它事情并通过轮询来看操作系统是否完成)
同步非阻塞IO(non-blocking IO)
系统调用可能是由多个任务组成的,所以可以拆成多个任务,这就是多路复用
你早上去买现炸油条,点单收钱和炸油条原来都是由一个人完成的,现在他成了瓶颈,所以专门找了个收银员下单收钱,他则专注在炸油条。(本质上炸油条是耗时的瓶颈,将他职责分离出不是瓶颈的部分,比如下单收银,对应到系统级别也时一样的意思)
多路复用IO(multiplexing IO)
应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
你早上去买现炸油条,门口排队的人多,现在引入了一个叫号系统,点完单后你就可以做自己的事情了,然后等叫号就去拿就可以了。(所以不用再去自己频繁跑去问有没有做好了)
信号驱动式IO(signal-driven IO)
相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。
异步IO(asynchronous IO)
五种IO模式
IO
查找并加载类的二进制数据
加载
确保被加载的类的正确性
验证
为类的静态变量分配内存,并将其初始化为默认值
准备
把类中的符号引用转换为直接引用
解析
为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化
初始化
生命周期
负责加载存放在JDK\\jre\\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的
启动类加载器: Bootstrap ClassLoader
该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\\jre\\lib\\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器
扩展类加载器: Extension ClassLoader
该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器
应用程序类加载器: Application ClassLoader
因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader
自定义类加载器:User ClassLoader
层次
当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
全盘负责
先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
父类委托
缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
缓存机制
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类
当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成
当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成
果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载
若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException
过程
实现自定义ClassLoad,实现其中的loadClass()方法,就不会往上找,这样就打破了双亲委派机制
打破双亲委派机制
双亲委派机制
机制
类加载机制
Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次 Java 方法调用,是线程私有的,生命周期和线程一致
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
JVM 直接对虚拟机栈的操作只有两个:每个方法执行,伴随着入栈(进栈/压栈),方法执行结束出栈
栈不存在垃圾回收问题
可以通过参数-Xss来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
局部变量表(Local Variables)
操作数栈(Operand Stack)(或称为表达式栈)
动态链接(Dynamic Linking):指向运行时常量池的方法引用
方法返回地址(Return Address):方法正常退出或异常退出的地址
内部结构
虚拟机栈
一个 Native Method 就是一个 Java 调用非 Java 代码的接口。我们知道的 Unsafe 类就有很多本地方法
本地方法接口
Java 虚拟机栈用于管理 Java 方法的调用,而本地方法栈用于管理本地方法的调用
本地方法栈
程序计数器是用来记录线程的代码执行到哪个位置,因为cpu是线程调度的,会在多个线程之间来回切换,所以需要一个程序计数器
程序计数器
线程私有
新对象和没达到一定年龄的对象都在新生代
Eden Memory (伊甸园)
两个幸存区(Survivor Memory,被称为from/to或s0/s1)
三个部分
大多数新创建的对象都位于 Eden 内存空间中
当 Eden 空间被对象填充时,执行Minor GC,并将所有幸存者对象移动到一个幸存者空间中
Minor GC 检查幸存者对象,并将它们移动到另一个幸存者空间。所以每次,一个幸存者空间总是空的
经过多次 GC 循环后存活下来的对象被移动到老年代。通常,这是通过设置年轻一代对象的年龄阈值来实现的,然后他们才有资格提升到老一代
流程
新生代
被长时间使用的对象,老年代的内存空间应该要比年轻代更大
老年代
1.7永久代
1.8 元空间
在 JVM 内存模型的堆中,堆被划分为新生代和老年代
此时 JVM 会给对象定义一个对象年轻计数器(-XX:MaxTenuringThreshold)
当创建一个对象时,对象会被优先分配到新生代的 Eden 区
JVM 会把存活的对象转移到 Survivor 中,并且对象年龄 +1
对象在 Survivor 中同样也会经历 Minor GC,每经历一次 Minor GC,对象年龄都会+1
当 Eden 空间不足时,JVM 将执行新生代的垃圾回收(Minor GC)
如果分配的对象超过了-XX:PetenureSizeThreshold,对象会直接被分配到老年代
new 的对象先放在伊甸园区,此区有大小限制
当伊甸园的空间填满时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
然后将伊甸园中的剩余对象移动到幸存者 0 区
如果再次触发垃圾回收,此时上次幸存下来的放到幸存者 0 区,如果没有回收,就会放到幸存者 1 区
如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区
什么时候才会去养老区呢? 默认是 15 次回收标记
在养老区,相对悠闲。当养老区内存不足时,再次触发 Major GC,进行养老区的内存清理
若养老区执行了 Major GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常
对象的分配过程
堆
只是 JVM 规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据
永久代和元空间都可以理解为方法区的落地实现
方法区
线程共享
从内存模型而不是垃圾回收的角度,对 Eden 区域继续进行划分,JVM 为每个线程分配了一个私有缓存区域,它包含在 Eden 空间内
多线程同时分配内存时,使用 TLAB 可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为快速分配策略
堆区是线程共享的,任何线程都可以访问到堆区中的共享数据
由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
为什么要有TLAB
TLAB(Thread Local Allocation Buffer) 线程本地分配缓冲区
内存结构
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收
两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收
正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法
引用计数算法
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收
虚拟机栈中引用的对象
本地方法栈中引用的对象
方法区中类静态属性引用的对象
方法区中的常量引用的对象
包含那些内容
可达性分析算法
引用对象是否可以回收
被强引用关联的对象不会被回收
Object obj = new Object()
使用 new 一个新对象的方式来创建强引用
强引用
被软引用关联的对象只有在内存不够的情况下才会被回收
Object obj = new Object();SoftReference<Object> sf = new SoftReference<Object>(obj);obj = null; // 使对象只被软引用关联
使用 SoftReference 类来创建软引用
软引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前
Object obj = new Object();WeakReference<Object> wf = new WeakReference<Object>(obj);obj = null;
使用 WeakReference 类来实现弱引用
弱引用
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象
Object obj = new Object();PhantomReference<Object> pf = new PhantomReference<Object>(obj);obj = null;
使用 PhantomReference 来实现虚引用
虚引用
引用类型
将存活的对象进行标记,然后清理掉未被标记的对象
标记和清除过程效率都不高
会产生大量不连续的内存碎片,导致无法给大对象分配内存
不足
标记 - 清除
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
标记 - 整理
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理
只使用了内存的一半
复制
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法
复制算法
标记 - 清除 或者 标记 - 整理 算法
一款多线程的收集器,采用复制算法,主要工作在 Young 区,可以通过 -XX:ParallelGCThreads 参数来控制收集的线程数,整个过程都是 STW 的,常与 CMS 组合使用。
ParNew
以获取最短回收停顿时间为目标,采用“标记-清除”算法,分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW ,多数应用于互联网站或者 B/S 系统的服务器端上,JDK9 被标记弃用,JDK14 被删除
CMS
分代收集
一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求
G1
JDK11 中推出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收,SPECjbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS
ZGC
分区收集算法
基本垃圾回收算法
只是新生代的垃圾收集
新生代收集(Minor GC/Young GC)
目前,只有 CMS GC 会有单独收集老年代的行为
很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收
只是老年代的垃圾收集
老年代收集(Major GC/Old GC)
目前只有 G1 GC 会有这种行为
收集整个新生代以及部分老年代的垃圾收集
混合收集(Mixed GC)
部分收集(Partial GC)
收集整个 Java 堆和方法区的垃圾
整堆收集(Full GC)
回收区域
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC
对象优先在 Eden 分配
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制
大对象直接进入老年代
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中
-XX:MaxTenuringThreshold 用来定义年龄的阈值
长期存活的对象进入老年代
虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄
动态对象年龄判定
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的
如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC
空间分配担保
内存分配策略
当 Eden 空间满时,就将触发一次 Minor GC
触发Minor GC(Young GC)
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存
调用 System.gc()
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间
老年代空间不足
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC
空间分配担保失败
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC
Concurrent Mode Failure
触发Full GC (Old GC)
什么情况触发GC
单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程
单线程与多线程
串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并形指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行
串行与并行
回收方式
Serial 翻译为串行,也就是说它以串行的方式执行
它是单线程的收集器,只会使用一个线程进行垃圾收集工作
它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率
它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的
Serial 收集器
它是 Serial 收集器的多线程版本
是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作
默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数
ParNew 收集器
与 ParNew 一样是多线程收集器
其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的: 新生代空间变小,垃圾回收变得频繁,导致吞吐量下降
可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手动指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量
Parallel Scavenge 收集器
是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用
在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用
作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用
如果用在 Server 模式下,它有两大用途
Serial Old 收集器
是 Parallel Scavenge 收集器的老年代版本
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器
Parallel Old 收集器
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿
并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿
重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿
并发清除: 不需要停顿
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿
吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高
浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS
无法处理浮动垃圾,可能出现 Concurrent Mode Failure
标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC
CMS 收集器
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收
这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描
初始标记
并发标记
为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行
最终标记
首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率
筛选回收
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片
空间整合
能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒
可预测的停顿
G1 收集器
7个垃圾回收器
Hotspot有哪些垃圾回收器
GC垃圾回收
将字节码指令解释或者编译为对应平台上的本地机器指令
简单来说,执行引擎充当了将高级语言翻译为机器语言的翻译者
作用
解释器所承担的角色就是一个运行时翻译者,将字节码文件中的内容翻译为对应平台的本地机器码指令
当一条字节码指令被解释执行后,接着再根据pc寄存器中记录的下一条需要被执行的字节码指令执行解释操作
字节码解释器在执行过程中通过纯软件代码模拟字节码执行,效率非常低
字节码解释器
模板解释器将每一条字节码和一个模板函数关联,模板函数中直接产生这条字节码指令执行时的机器码,从而提高了解释器的性能
在常用的HotSpot VM中,解释器主要由Interpreter模板和code模块构成
实现了解释器的核心功能
Interpreter模板
用于管理HotSpot VM在运行时生成的本地机器码指令
code模块
模板解释器
两套解释器
解释器
即时编译器的目的是避免函数被解释执行,而是将整个函数体编译成机器码指令,每次函数执行时,只执行编译后的机器码即可,这种方式可以大大的提高效率
是否需要JIT编译器将字节码直接编译成对应平台的机器码,需要根据代码被调用的 执行频率 而定。需要被JIT编译器编译成机器码的字节码,也称为 热点代码 ,JIT编译器会对热点代码做出 深度优化 ,将其从字节码编译成机器码,并 缓存到方法区 ,提高代码的执行效率
方法调用计数器用于统计方法调用次数,它的默认阈值是client模式下是1500次,在server模式下是10000次。超过这个阈值,就会触发JIT编译
修改虚拟机参数-XX:CompileThreshold来手动指定
当一个方法被调用的时候,会优先检查该方法是否被JIT编译过,如果存在,则优先使用编译过的本地代码来执行,如果不存在,则将此方法的调用计数器加一,然后再判断计数器的值是否超过配置的阈值。如果已经超过了,就会向JIT编译器提交一个该方法的编译请求。
方法调用计数器执行的流程图
关于方法调用计数器,如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对执行的频率。当超过一定的时间限度,如果方法的调用次数仍然达不到阈值,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器的热度衰减,而这段时间被称作为该方法的半衰周期。进行热度衰减的过程是虚拟机进行垃圾回收的时候顺便进行的,举手之劳而已。可以使用虚拟机参数-XX:-UseCounterDecay来关闭热度衰减。这样的话,只要运行时间足够长,绝大部分方法都会被编译成本地代码。最后,还可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位为秒
方法调用计数器
统计一个方法中循环体代码执行次数,在字节码中遇到控制流向后,跳转的指令称为“回边”
回边计数器执行的流程图
回边计数器
热点探测方式
热点代码
client compiler
指定Java虚拟机运行在client模式下,使用C1编译器
C1编译器会对字节码进行简单和可靠的优化,耗时短
以达到更快的编译速度,但是编译后的代码执行速度相对慢
将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程
方法内联
对唯一实现的类进行内联
去虚拟化
在运行期间把一些不会执行的代码叠掉
冗余消除
主要方法
c1编译器
server compiler
指定Java虚拟机运行在server模式下,使用C2编译器
C2编译器对代码优化时间长,编译时间也长
但是编译后的代码执行速度比较快
用标量值代替聚合对象的属性值
标量替换
对于未逃逸的对象分配在栈上而不是堆上
栈上分配
清楚同步操作,通常指synchronized
同步消除
c2编译器
分类
JIT编译器(即时编译器)
当程序启动的时候,解释器可以马上发挥作用,省去编译的时间
编译器想要执行,需要把字节码编译成本地机器码,并且缓存编译后的机器码,编译需要一定的时间
编译后的本地机器码,执行效率高。所以,在两种并存的模式下,解释器首先发挥作用,而不必等到即时编译器全部编译完在执行,这样可以省去不必要的编译时间
随着程序继续不断运行,编译器发挥作用,根据热点探测功能,把越来越多的字节码编译成本地机器码,获得更高的执行效率
解释器和JIT并存
完全采用解释器模式执行程序
-Xint
完全采用即时编译器模式执行程序。如果即时编译器出现问题,解释器会介入执行
-XComp
采用解释器+即时编译器的混合模式共同执行程序,HotStop VM默认就是这个模式
-Xmixed
执行引擎执行程序的方式
Hotspot虚拟机
执行引擎
Java内存模型(Java Memory Model)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范(定义了程序中各个变量的访问方式)。 JVM运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存(栈空间),用于存储线程私有的数据,而Java 内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问, 但线程对变量的操作(读取赋值等)必须在工作内存中进行。所以首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量。工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
happens-before规则
Load指令(读屏障):它将内存存储的数据拷贝到处理器的缓存中
Load指令
Store指令(写屏障):它主要实现让当前线程写入高速缓存中的最新数据更新写入到内存,让其他线程也可见
Store指令
屏障类型
在每个volatile写操作前插入StoreStore屏障,这样就能让其他线程修改A变量后,把修改的值对当前线程可见,在写操作后插入StoreLoad屏障,这样就能让其他线程获取A变量的时候,能够获取到已经被当前线程修改的值
在每个volatile读操作前插入LoadLoad屏障,这样就能让当前线程获取A变量的时候,保证其他线程也都能获取到相同的值,这样所有的线程读取的数据就一样了,在读操作后插入LoadStore屏障;这样就能让当前线程在其他线程修改A变量的值之前,获取到主内存里面A变量的的值。
内存屏障
JMM
JVM
socket
它是JDK内置的一种动态扩展点的实现
就是我们可以定义一个标准的接口,然后第三方的库里面可以实现这个接口
SPI全称是Service Provider Interface
通过SPI在代码中定义好接口,由外部代码来实现接口,在通过自定义ClassLoad来实现热编译,最终实现热部署
通过SPI和ClassLoad 实现动态加载类(热部署)
SPI
进阶
基于Spring开发的应用中的对象可以不依赖于Spring的API
非侵入式
IOC——Inversion of Control,指的是将对象的创建权交给 Spring 去创建。使用 Spring 之前,对象的创建都是由我们自己在代码中new创建。而使用 Spring 之后。对象的创建都是给了 Spring 框架
控制反转
DI——Dependency Injection,是指依赖的对象不需要手动调用 setXX 方法去设置,而是通过配置赋值
依赖注入
Aspect Oriented Programming——AOP
面向切面编程
Spring 是一个容器,因为它包含并且管理应用对象的生命周期
容器
Spring 实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用XML和Java注解组合这些对象
组件化
在 IOC 和 AOP 的基础上可以整合各种企业应用的开源框架和优秀的第三方类库(实际上 Spring 自身也提供了表现层的 SpringMVC 和持久层的 Spring JDBC)
一站式
特性
Spring 可以使开发人员使用 POJOs 开发企业级的应用程序。只使用 POJOs 的好处是你不需要一个 EJB 容器产品,比如一个应用程序服务器,但是你可以选择使用一个健壮的 servlet 容器,比如 Tomcat 或者一些商业产品
Spring 在一个单元模式中是有组织的。即使包和类的数量非常大,你只要担心你需要的,而其它的就可以忽略了
Spring 不会让你白费力气做重复工作,它真正的利用了一些现有的技术,像 ORM 框架、日志框架、JEE、Quartz 和 JDK 计时器,其他视图技术
测试一个用 Spring 编写的应用程序很容易,因为环境相关的代码被移动到这个框架中。此外,通过使用 JavaBean-style POJOs,它在使用依赖注入注入测试数据时变得更容易
Spring 的 web 框架是一个设计良好的 web MVC 框架,它为比如 Structs 或者其他工程上的或者不怎么受欢迎的 web 框架提供了一个很好的供替代的选择。MVC 模式导致应用程序的不同方面(输入逻辑,业务逻辑和UI逻辑)分离,同时提供这些元素之间的松散耦合。模型(Model)封装了应用程序数据,通常它们将由 POJO 类组成。视图(View)负责渲染模型数据,一般来说它生成客户端浏览器可以解释 HTML 输出。控制器(Controller)负责处理用户请求并构建适当的模型,并将其传递给视图进行渲染
Spring 对 JavaEE 开发中非常难用的一些 API(JDBC、JavaMail、远程调用等),都提供了封装,使这些API应用难度大大降低
轻量级的 IOC 容器往往是轻量级的,例如,特别是当与 EJB 容器相比的时候。这有利于在内存和 CPU 资源有限的计算机上开发和部署应用程序
Spring 提供了一致的事务管理接口,可向下扩展到(使用一个单一的数据库,例如)本地事务并扩展到全局事务(例如,使用 JTA)
特性和优势
Spring里面的bean就类似是定义的一个组件,而这个组件的作用就是实现某个功能的,这里所定义的bean就相当于给了你一个更为简便的方法来调用这个组件去实现你要完成的功能
唯一bean实例,Spring中的bean默认都是单例的
singleton
每次请求都会创建一个新的bean实例
prototype
每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效
request
每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效
session
全局session作用域,仅仅在基于Portlet的Web应用中才有意义,Spring5中已经没有了。Portlet是能够生成语义代码(例如HTML)片段的小型Java Web插件。它们基于Portlet容器,可以像Servlet一样处理HTTP请求。但是与Servlet不同,每个Portlet都有不同的会话
global-session
作用域
创建对象的方式有很多,比如 new、反射、clone等等,Spring是怎么创建对象的呢?绝大多数情况下,Spring是通过反射来创建对象的,不过如果我们提供了Supplier或者工厂方法,Spring也会直接使用我们提供的创建方式。
先判断是否提供了Supplier,如果提供,则通过Supplier产生对象。
再判断是否提供工厂方法,如果提供,则使用工厂方法产生对象。
如果仅有一个构造方法,会直接使用该构造方法(如果构造方法有参数,会自动注入依赖参数)
如果没有,Spring默认选择无参构造方法;
如果有,且有@Autowired(required=true)的构造方法,就会选择该构造方法;
如果有,但是没有@Autowired(required=true)的构造方法,Spring会从所有加了@Autowired的构造方法中,根据构造器参数个数、类型匹配程度等综合打分,选择一个匹配参数最多,类型最准确的构造方法。
如果有多个构造参数,会判断有没有加了@Autowired注解的构造参数:
如果都没提供,需要进行构造方法的推断,逻辑为:
具体逻辑
创建Bean(实例化)
通过MergedBeanDefinitionPostProcessor类型的后置处理器,可以对bean对应的BeanDefinition进行修改。Spring自身也充分利用该拓展点,做了很多初始化操作(并没有修改BeanDefinition),比如查找标注了@Autowired、 @Resource、@PostConstruct、@PreDestory 的属性和方法,方便后续进行属性注入和初始化回调。
merged BeanDefinition
本阶段主要是将早期bean对象提前放入到三级缓存singletonFactories中,为循环依赖做支持。在后续进行属性填充时,如果发生循环依赖,可以从三级缓存中通过getObject()获取该bean,完成循环依赖场景下的自动注入。
暴露工厂对象
本阶段完成了Spring的核心功能之一:依赖注入,包括自动注入、@Autowired注入、@Resource注入等。Spring会根据bean的注入模型(默认不自动注入),选择根据名称自动注入还是根据类型自动注入。然后调用InstantiationAwareBeanPostProcessor#postProcessProperties()完成@Autowired和@Resource的属性注入。
属性填充
该阶段主要做bean的初始化操作,包括:回调Aware接口、回调初始化方法、生成代理对象等。
invokeAwareMethods():回调BeanNameAware、BeanClassLoaderAware、BeanFactoryAware感知接口。
ApplicationContextAwareProcessor: 回调EnvironmentAware、ResourceLoaderAware、ApplicationContextAware、ApplicationEventPublisherAware、MessageSourceAware、EmbeddedValueResolverAware感知接口。
InitDestroyAnnotationBeanPostProcessor:调用了标注了@PostConstruct的方法。
回调后置处理器的前置方法,其中:
回调自定义的initMethod,比如通过@Bean(initMethod = \"xxx\")指定的初始化方法。
invokeInitMethods()调用初始化方法:
其中AbstractAutoProxyCreator和 AbstractAdvisingBeanPostProcessor都有可能产生代理对象
回调后置处理器的后置方法,可能返回代理对象。
初始化Bean
在创建bean的时候,会判断如果bean是DisposableBean、AutoCloseable的子类,或者有 destroy-method等,会注册为可销毁的bean,在容器关闭时,调用对应的方法进行bean的销毁。
销毁
创建阶段主要是创建对象,这里我们看到,对象的创建权交由Spring管理了,不再是我们手动new了,这也是IOC的概念。
属性填充阶段主要是进行依赖的注入,将当前对象依赖的bean对象,从Spring容器中找出来,然后填充到对应的属性中去。
初始化bean阶段做的事情相对比较复杂,包括回调各种Aware接口、回调各种初始化方法、生成AOP代理对象也在该阶段进行,该阶段主要是完成初始化回调,后面我们慢慢分析。
使用bean阶段,主要是bean创建完成,在程序运行期间,提供服务的阶段。
销毁bean阶段,主要是容器关闭或停止服务,对bean进行销毁处理。
简略Bean的生命周期
对象A依赖了对象B,而对象B又依赖了对象A
// A -> Bclass A{ public B b;}// B -> Aclass B{ public A a;}
什么是循环依赖?
单例对象缓存池,已经实例化并且属性赋值,这里的对象是成熟对象
第一层缓存(singletonObjects)
单例对象缓存池,已经实例化但尚未属性赋值,这里的对象是半成品对象
第二层缓存(earlySingletonObjects)
单例工厂的缓存
第三层缓存(singletonFactories)
三级缓存
因为一级缓存和二级缓存的区别在于,一级缓存对实例化对象还需要赋值,而二级缓存只是单纯的实例对象而已,所以可以在创建bean的初始化阶段,就可以将实力丢入二级缓存供其它对象依赖,这样通过二级缓存解决了循环依赖
部分通过代理的对象(AOP),这时实例化对象不能满足,这个时候就需要三级缓存,三级缓存是一个ObjectFactory工厂对象,来生成代理对象,这样解决了代理对象时的循环依赖
二级缓存解决不了的循环依赖
单例的setter注入(能解决)
多实例Bean是每次调用一次getBean都会执行一次构造方法并且给属性赋值,根本没有三级缓存,因此不能解决循环依赖
多例的setter注入(不能解决)
这种情况的结果就是两个bean都不能完成初始化,循环依赖难以解决
构造器注入(不能解决)
单例的代理对象setter注入(有可能解决)
DependsOn循环依赖(不能解决)
循环依赖主要场景
文章摘取
循环依赖
Spring Bean
Ioc—Inversion of Control,即“控制反转”,不是什么技术,而是一种设计思想。在Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制实现原理就是工厂模式加反射机制
传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对 象的创建
那就是主要控制了外部资源获取(不只是对象包括比如文件等)
控制什么?
谁控制谁
有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象
因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转
依赖对象的获取被反转了
哪些方面反转了?
为何是反转
传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试
有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是 松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活
实IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了
IoC很好的体现了面向对象设计法则之一—— 好莱坞法则:“别找我们,我们找你”;即由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找
IoC能做什么
控制反转是通过依赖注入实现的
IoC是设计思想,DI是实现方式
DI—Dependency Injection,即依赖注入:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中
依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现
应用程序依赖于IoC容器
谁依赖于谁?
应用程序需要IoC容器来提供对象需要的外部资源
为什么需要依赖?
很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象
谁注入谁?
就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)
注入了什么
setter方式
构造函数
byType
@Autowired如果需要按照名称匹配需要和@Qualifier一起使用
多个bean类型相同时,按名称匹配
@Autowired
byName
@Inject如果需要按照名称匹配需要和@Named一起使用
@Resource
@Inject
注解注入
三种注入方式
DI
IoC和DI是什么关系
顾名思义,就是将bean的信息配置.xml文件里,通过Spring加载文件为我们创建bean。这种方式出现很多早前的SSM项目中,将第三方类库或者一些配置工具类都以这种方式进行配置,主要原因是由于第三方类不支持Spring注解
可以使用于任何场景,结构清晰,通俗易懂
配置繁琐,不易维护,枯燥无味,扩展性差
<?xml version=\"1.0\" encoding=\"UTF-8\"?><beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd\"> <!-- services --> <bean id=\"userService\" class=\"tech.pdai.springframework.service.UserServiceImpl\"> <property name=\"userDao\" ref=\"userDao\"/> <!-- additional collaborators and configuration for this bean go here --> </bean> <!-- more bean definitions for services go here --></beans>
xml 配置
将类的创建交给我们配置的JavcConfig类来完成,Spring只负责维护和管理,采用纯Java创建方式。其本质上就是把在XML上的配置声明转移到Java配置类中
适用于任何场景,配置方便,因为是纯Java代码,扩展性高,十分灵活
由于是采用Java类的方式,声明不明显,如果大量配置,可读性比较差
@Configurationpublic class BeansConfig { /** * @return user dao */ @Bean(\"userDao\") public UserDaoImpl userDao() { return new UserDaoImpl(); } /** * @return user service */ @Bean(\"userService\") public UserServiceImpl userService() { UserServiceImpl userService = new UserServiceImpl(); userService.setUserDao(userDao()); return userService; }}
Java 配置
通过在类上加注解的方式,来声明一个类交给Spring管理,Spring会自动扫描带有@Component,@Controller,@Service,@Repository这四个注解的类,然后帮我们创建并管理,前提是需要先配置Spring的注解扫描器
开发便捷,通俗易懂,方便维护
具有局限性,对于一些第三方资源,无法添加注解。只能采用XML或JavaConfig的方式配置
@Servicepublic class UserServiceImpl { /** * user dao impl. */ @Autowired private UserDaoImpl userDao; /** * find user list. * * @return user list */ public List<User> findUserList() { return userDao.findUserList(); }}
注解配置
三种配置方式
IOC
AOP的本质也是为了解耦,它是一种设计思想
如何理解AOP
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程
AOP是什么
针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分
OOP面向对象编程
针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果
AOP面向切面编程
两种设计思想在目标上有着本质的差异
OOP与AOP
表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等,Spring只支持方法执行连接点,在AOP中表示为在哪里干
连接点(Jointpoint)
选择一组相关连接点的模式,即可以认为连接点的集合,Spring支持perl5正则表达式和AspectJ切入点模式,Spring默认使用AspectJ语法,在AOP中表示为在哪里干的集合
切入点(Pointcut)
在连接点上执行的行为,通知提供了在AOP中需要在切入点所选择的连接点处进行扩展现有行为的手段;包括前置通知(before advice)、后置通知(after advice)、环绕通知(around advice),在Spring中通过代理模式实现AOP,并通过拦截器模式以环绕连接点的拦截器链织入通知;在AOP中表示为干什么
通知(Advice)
横切关注点的模块化,比如上边提到的日志组件。可以认为是通知、引入和切入点的组合;在Spring中可以使用Schema和@AspectJ方式进行组织实现;在AOP中表示为在哪干和干什么集合
方面/切面(Aspect)
引入(inter-type declaration)
需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为被通知对象;由于Spring AOP 通过代理模式实现,从而这个对象永远是被代理对象,在AOP中表示为对谁干
目标对象(Target Object)
把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。在AOP中表示为怎么实现的
织入(Weaving)
AOP框架使用代理模式创建的对象,从而实现在连接点处插入通知(即应用切面),就是通过代理来对目标对象应用切面。在Spring中,AOP代理可以用JDK动态代理或CGLIB代理实现,而通过拦截器模型应用切面。在AOP中表示为怎么实现的一种典型方式
AOP代理(AOP Proxy)
在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)
前置通知(Before advice)
在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回
后置通知(After returning advice)
在方法抛出异常退出时执行的通知
异常通知(After throwing advice)
当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)
最终通知(After (finally) advice)
包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行
环绕通知(Around Advice)
环绕通知是最常用的通知类型。和AspectJ一样,Spring提供所有类型的通知,我们推荐你使用尽可能简单的通知类型来实现需要的功能。例如,如果你只是需要一个方法的返回值来更新缓存,最好使用后置通知而不是环绕通知,尽管环绕通知也能完成同样的事情。用最合适的通知类型可以使得编程模型变得简单,并且能够避免很多潜在的错误。比如,你不需要在JoinPoint上调用用于环绕通知的proceed()方法,就不会有调用的问题
通知类型
我们把这些术语串联到一起,方便理解
AOP术语
AspectJ是一个java实现的AOP框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有AspectJ的AOP功能(当然需要特殊的编译器)
可以这样说AspectJ是目前实现AOP框架中最成熟,功能最丰富的语言,更幸运的是,AspectJ与java程序完全兼容,几乎是无缝关联,因此对于有java编程基础的工程师,上手和使用都非常容易
AspectJ是什么
AspectJ是更强的AOP框架,是实际意义的AOP标准
Spring为何不写类似AspectJ的框架?
Spring AOP 和 AspectJ是什么关系
XML Schema配置方式
基于XML的声明式AspectJ存在一些不足,需要在Spring配置文件配置大量的代码信息,为了解决这个问题,Spring 使用了@AspectJ框架为AOP的实现提供了一套注解
用来定义一个切面
@Aspect
用于定义切入点表达式。在使用时还需要定义一个包含名字和任意参数的方法签名来表示切入点名称,这个方法签名就是一个返回值为void,且方法体为空的普通方法
@pointcut
用于定义前置通知,相当于BeforeAdvice。在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式)
@Before
用于定义后置通知,相当于AfterReturningAdvice。在使用时可以指定pointcut / value和returning属性,其中pointcut / value这两个属性的作用一样,都用于指定切入点表达式
@AfterReturning
用于定义环绕通知,相当于MethodInterceptor。在使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点
@Around
用于定义异常通知来处理程序中未处理的异常,相当于ThrowAdvice。在使用时可指定pointcut / value和throwing属性。其中pointcut/value用于指定切入点表达式,而throwing属性值用于指定-一个形参名来表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常
@After-Throwing
用于定义最终final 通知,不管是否异常,该通知都会执行。使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点
@After
用于定义引介通知,相当于IntroductionInterceptor (不要求掌握)
@DeclareParents
AspectJ注解方式
AOP 配置方式
动态织入,动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的
接口使用JDK动态代理
Java JDK的动态代理(Proxy,底层通过反射实现)
非接口使用CGLIB动态代理
CGLIB的动态代理(底层通过继承实现)
动态代理
Spring AOP的实现方式
AOP
Spring使用工厂模式通过BeanFactory和ApplicationContext创建bean对象
工厂设计模式
Spring AOP功能的实现
代理设计模式
Spring中的bean默认都是单例的
单例设计模式
Spring中的jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,它们就使用到了模板模式
模板方法模式
我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源
包装器设计模式
Spring事件驱动模型就是观察者模式很经典的一个应用
观察者模式
Spring AOP的增强或通知(Advice)使用到了适配器模式
适配器模式
Spring中用到的设计模式
作用对象不同。@Component注解作用于类,而@Bean注解作用于方法
@Component注解通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用@ComponentScan注解定义要扫描的路径)。@Bean注解通常是在标有该注解的方法中定义产生这个bean,告诉Spring这是某个类的实例,当我需要用它的时候还给我
@Bean注解比@Component注解的自定义性更强,而且很多地方只能通过@Bean注解来注册bean。比如当引用第三方库的类需要装配到Spring容器的时候,就只能通过@Bean注解来实现
@Component和@Bean的区别是什么?
通用的注解,可标注任意类为Spring组件。如果一个Bean不知道属于哪一个层,可以使用@Component注解标注
@Component注解
对应持久层,即Dao层,主要用于数据库相关操作
@Repository注解
对应服务层,即Service层,主要涉及一些复杂的逻辑,需要用到Dao层(注入)
@Service注解
对应Spring MVC的控制层,即Controller层,主要用于接受用户请求并调用Service层的方法返回数据给前端页面
@Controller注解
将一个类声明为Spring的bean的注解有哪些?
在代码中硬编码(不推荐使用)
编程式事务
在配置文件中配置(推荐使用),分为基于XML的声明式事务和基于注解的声明式事务
声明式事务
Spring事务管理的方式有几种?
使用后端数据库默认的隔离级别,Mysql默认采用的REPEATABLE_READ隔离级别;Oracle默认采用的READ_COMMITTED隔离级别
ISOLATION_DEFAULT
最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
ISOLATION_READ_UNCOMMITTED
允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
ISOLATION_READ_COMMITTED
对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
ISOLATION_REPEATABLE_READ
最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别
ISOLATION_SERIALIZABLE
Spring事务中的隔离级别有哪几种?
如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
PROPAGATION.REQUIRED
如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行
PROPAGATION.SUPPORTS
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
PROPAGATION.MANDATORY
支持当前事务的情况
创建一个新的事务,如果当前存在事务,则把当前事务挂起
PROPAGATION.REQUIRES_NEW
以非事务方式运行,如果当前存在事务,则把当前事务挂起
PROPAGATION.NOT_SUPPORTED
以非事务方式运行,如果当前存在事务,则抛出异常
PROPAGATION.NEVER
不支持当前事务的情况
如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED
PROPAGATION.NESTED
其他情况
Spring事务中有哪几种事务传播行为?
preHandle目标方法执行前执行
postHandle目标方法执行后执行
afterCompletion请求完成时执行
spring mvc拦截器的顶层接口是:HandlerInterceptor为了方便我们一般情况会用HandlerInterceptor接口的实现类HandlerInterceptorAdapter类
自定义拦截器
实现BeanFactoryAware接口,然后重写setBeanFactory方法,就能从该方法中获取到spring容器对象。
@Servicepublic class PersonService2 implements ApplicationContextAware { private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public void add() { Person person = (Person) applicationContext.getBean(\"person\"); }}
BeanFactoryAware接口
实现ApplicationContextAware接口,然后重写setApplicationContext方法,也能从该方法中获取到spring容器对象。
@Servicepublic class PersonService3 implements ApplicationListener<ContextRefreshedEvent> { private ApplicationContext applicationContext; @Override public void onApplicationEvent(ContextRefreshedEvent event) { applicationContext = event.getApplicationContext(); } public void add() { Person person = (Person) applicationContext.getBean(\"person\"); }}
ApplicationContextAware接口
获取Spring容器对象
只需在handleException方法中处理异常情况,业务接口中可以放心使用,不再需要捕获异常(有人统一处理了)
@RestControllerAdvicepublic class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public String handleException(Exception e) { if (e instanceof ArithmeticException) { return \"数据异常\"; } if (e instanceof Exception) { return \"服务器内部异常\"; } retur nnull; }}
RestControllerAdvice
全局异常处理
将 S 类型对象转为 T 类型对象
将 S 类型对象转为 R 类型及子类对象
它支持多个source和目标类型的转化,同时还提供了source和目标类型的上下文,这个上下文能让你实现基于属性上的注解或信息来进行类型转换
GenericConverter
类型转换器
@Import
selectImports方法返回的是数组,意味着可以同时引入多个类
ImportSelector 接口
能在registerBeanDefinitions方法中获取到BeanDefinitionRegistry容器注册对象,可以手动控制BeanDefinition的创建和注册
ImportBeanDefinitionRegistrar 接口
有时我们需要在某个配置类中引入另外一些类,被引入的类也加到spring容器中。这时可以使用@Import注解完成这个功能。
导入配置
加载一些系统参数、完成初始化、预热本地缓存
CommandLineRunner 接口
@Componentpublic class TestRunner implements ApplicationRunner { @Autowired private LoadDataService loadDataService; public void run(ApplicationArguments args) throws Exception { loadDataService.load(); }}
ApplicationRunner 接口
项目启动时
Spring IOC在实例化Bean对象之前,需要先读取Bean的相关属性,保存到BeanDefinition对象中,然后通过BeanDefinition对象,实例化Bean对象。
@Componentpublic class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableListableBeanFactory; BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(User.class); beanDefinitionBuilder.addPropertyValue(\"id\
实现BeanFactoryPostProcessor接口
在postProcessBeanFactory方法中,可以获取BeanDefinition的相关对象,并且修改该对象的属性
修改BeanDefinition
该在初始化方法之前调用
postProcessBeforeInitialization
该方法再初始化方法之后调用
postProcessAfterInitialization
BeanPostProcessor接口
初始化Bean前后
@Servicepublic class AService { @PostConstruct public void init() { System.out.println(\"===初始化===\"); }}
使用@PostConstruct注解
@Servicepublic class BService implements InitializingBean { @Override public void afterPropertiesSet() throws Exception { System.out.println(\"===初始化===\"); }}
实现InitializingBean接口
初始化方法
们需要在关闭spring容器前,做一些额外的工作,比如:关闭资源文件等。
这时可以实现DisposableBean接口,并且重写它的destroy方法
关闭容器前
每次从spring容器中获取到的bean都是同一个对象
singleton 单例
每次从spring容器中获取到的bean都是不同的对象
prototype 多例
spring默认支持的Scope
同一次请求从spring容器中获取到的bean都是同一个对象
RequestScope
同一个会话从spring容器中获取到的bean都是同一个对象
SessionScope
spring web又对Scope进行了扩展
自定义作用域
常见拓展点
Spring
这个注解是springboot启动类上的一个注解,是一个组合注解,也就是由其他注解组合起来,它的主要作用就是标记说明这个类是springboot的主配置类,springboot应该运行这个类里面的main()方法来启动程序
@Component
这个注解标注在哪个类上,就表示当前这个类是一个配置类,而配置类也是spring容器中的组件
@Configuration
@SpringBootConfiguration
这个注解是开启自动配置的功能,里面包含了两个注解
这个注解的作用说白了就是将主配置类(@SpringBootApplication标注的类)所在包以及子包里面的所有组件扫描并加载到spring的容器中,这也就是为什么我们在利用springboot进行开发的时候,无论是Controller还是Service的路径都是与主配置类同级或者次级的原因
@AutoConfigurationPackage
这个注解就是将需要自动装配的类以全类名的方式返回
在META-INF/spring.factories这个文件里面的数据是以键=值的方式存储,然后解析这些文件,找出以EnableAutoConfiguration为键的所有值,以列表的方式返回
关键就在这个loadSpringFactories()方法里面,在这个方法里,它会查找所有在META-INF路径下的spring.factories文件
loadFactoryNames()方法里面又调用了一个loadSpringFactories()方法
在getCandidateConfigurations()方法里面调用了loadFactoryNames()方法
在selectImport()方法里调用了一个getAutoConfigurationEntry()方法,这个方法里面又调用了一个getCandidateConfigurations()方法
AutoConfigurationImportSelector这个类里面有一个方法selectImports()
@Import(AutoConfigurationImportSelector.class)
@EnableAutoConfiguration
这个注解的作用就是扫描当前包及子包的注解
@ComponentScan
这个注解主要由三个子注解组成
@SpringBootApplication
启动类上注解的作用
在springboot启动的时候会创建一个SpringApplication对象,在对象的构造方法里面会进行一些参数的初始化工作,最主要的是判断当前应用程序的类型以及设置初始化器以及监听器,并在这个过程中会加载整个应用程序的spring.factories文件,将文件中的内容放到缓存当中,方便后续获取
SpringApplication对象创建完成之后会执行run()方法来完成整个应用程序的启动,启动的过程中有两个最主要的方法prepareContext()和refreshContext(),在这两个方法中完成了自动装配的核心功能,在run()方法里还执行了一些包括上下文对象的创建,打印banner图,异常报告期的准备等各个准备工作,方便后续进行调用
在prepareContext()中主要完成的是对上下文对象的初始化操作,包括属性的设置,比如设置环境变量。在整个过程中有一个load()方法,它主要是完成一件事,那就是将启动类作为一个beanDefinition注册到registry,方便后续在进行BeanFactoryPostProcessor调用执行的时候,可以找到对应执行的主类,来完成对@SpringBootApplication、@EnableAutoConfiguration等注解的解析工作
在refreshContext()方法中会进行整个容器的刷新过程,会调用spring中的refresh()方法,refresh()方法中有13个非常关键的方法,来完成整个应用程序的启动。而在自动装配过程中,会调用的关键的一个方法就是invokeBeanFactoryPostProcessors()方法,在这个方法中主要是对ConfigurationClassPostProcessor类的处理,这个类是BFPP(BeanFactoryPostProcessor)的子类,因为实现了BDRPP(BeanDefinitionRegistryPostProcessor)接口,在调用的时候会先调用BDRPP中的postProcessBeanDefinitionRegistry()方法,然后再调用BFPP中的postProcessBeanFactory()方法,在执行postProcessBeanDefinitionRegistry()方法的时候会解析处理各种的注解,包含@PropertySource、@ComponentScan、@Bean、@Import等注解,最主要的是对@Import注解的解析
在解析@Import注解的时候,会有一个getImport()方法,从主类开始递归解析注解,把所有包含@Import的注解都解析到,然后在processImport()方法中对import的类进行分类,例如AutoConfigurationImportSelect归属于ImportSelect的子类,在后续的过程中会调用DeferredImportSelectorHandler类里面的process方法,来完成整个EnableAutoConfiguration的加载
springboot自动装配的流程
自动装配的原理
main方法
Spring Boot
Spring Cloud
使用#{}可以有效的防止SQL注入,提高系统安全性
#{}是预编译处理
${}是字符串替换
#{}和${}的区别是什么
在同一个SqlSession中,执行相同的SQL查询时;第一次会去查询数据库,并写在缓存中,第二次会直接从缓存中取
当执行SQL时候两次查询中间发生了增删改的操作,则SQLSession的缓存会被清空。 每次查询会先去缓存中找,如果找不到,再去数据库查询,然后把结果写到缓存中
SQLSession级别
一级缓存
第一次调用mapper下的SQL去查询用户的信息,查询到的信息会存放该mapper对应的二级缓存区域。 第二次调用namespace下的mapper映射文件中,相同的sql去查询用户信息,会去对应的二级缓存内取结果
mapper级别
二级缓存
一级缓存和二级缓存
mybatis代理模式
MyBatis
框架
Java
0 条评论
回复 删除
下一页