Java后端学习路线
2024-03-22 10:43:29 1 举报
AI智能生成
java相关学习笔记
作者其他创作
大纲/内容
应用框架
后端
spring家族框架
spring
IOC
(1)什么是IOC:
IOC,Inversion of Control,控制反转,指将对象的控制权转移给Spring框架,由 Spring 来负责控制对象的生命周期(比如创建、销毁)和对象间的依赖关系。
最直观的表达就是,以前创建对象的时机和主动权都是由自己把控的,如果在一个对象中使用另外的对象,就必须主动通过new指令去创建依赖对象,使用完后还需要销毁(比如Connection等),对象始终会和其他接口或类耦合起来。而 IOC 则是由专门的容器来帮忙创建对象,将所有的类都在 Spring 容器中登记,当需要某个对象时,不再需要自己主动去 new 了,只需告诉 Spring 容器,然后 Spring 就会在系统运行到适当的时机,把你想要的对象主动给你。也就是说,对于某个具体的对象而言,以前是由自己控制它所引用对象的生命周期,而在IOC中,所有的对象都被 Spring 控制,控制对象生命周期的不再是引用它的对象,而是Spring容器,由 Spring 容器帮我们创建、查找及注入依赖对象,而引用对象只是被动的接受依赖对象,所以这叫控制反转。
(2)什么是DI:
IoC 的一个重点就是在程序运行时,动态的向某个对象提供它所需要的其他对象,这一点是通过DI(Dependency Injection,依赖注入)来实现的,即应用程序在运行时依赖 IoC 容器来动态注入对象所需要的外部依赖。而 Spring 的 DI 具体就是通过反射实现注入的,反射允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性
(3)IoC的原理:
Spring 的 IoC 的实现原理就是工厂模式加反射机制
IOC,Inversion of Control,控制反转,指将对象的控制权转移给Spring框架,由 Spring 来负责控制对象的生命周期(比如创建、销毁)和对象间的依赖关系。
最直观的表达就是,以前创建对象的时机和主动权都是由自己把控的,如果在一个对象中使用另外的对象,就必须主动通过new指令去创建依赖对象,使用完后还需要销毁(比如Connection等),对象始终会和其他接口或类耦合起来。而 IOC 则是由专门的容器来帮忙创建对象,将所有的类都在 Spring 容器中登记,当需要某个对象时,不再需要自己主动去 new 了,只需告诉 Spring 容器,然后 Spring 就会在系统运行到适当的时机,把你想要的对象主动给你。也就是说,对于某个具体的对象而言,以前是由自己控制它所引用对象的生命周期,而在IOC中,所有的对象都被 Spring 控制,控制对象生命周期的不再是引用它的对象,而是Spring容器,由 Spring 容器帮我们创建、查找及注入依赖对象,而引用对象只是被动的接受依赖对象,所以这叫控制反转。
(2)什么是DI:
IoC 的一个重点就是在程序运行时,动态的向某个对象提供它所需要的其他对象,这一点是通过DI(Dependency Injection,依赖注入)来实现的,即应用程序在运行时依赖 IoC 容器来动态注入对象所需要的外部依赖。而 Spring 的 DI 具体就是通过反射实现注入的,反射允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性
(3)IoC的原理:
Spring 的 IoC 的实现原理就是工厂模式加反射机制
AOP
OOP面向对象,允许开发者定义纵向的关系,但并不适用于定义横向的关系,会导致大量代码的重复,而不利于各个模块的重用。
AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务处理。
AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。
(1)AspectJ是静态代理,也称为编译时增强,AOP框架会在编译阶段生成AOP代理类,并将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务处理。
AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。
(1)AspectJ是静态代理,也称为编译时增强,AOP框架会在编译阶段生成AOP代理类,并将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
事务
失效
bean对象没有被spring管理
方法修饰符不是public
自身调用问题,如果调用类内部的方法,没有经过spring代理,所以失效
数据源没有配置事务管理
数据库不支持事务
异常被捕获
异常类型错误或者配置错误
spring MVC
springboot
自动配置、开箱即用
整合数据库(事务问题)
集合权限
Shiro
spring security
中间件
消息队列
RPC框架
NIO框架
服务器软件
web服务器
Nginx
应用服务器
Tomcat
Jetty
Undertow
中间件
缓存
redis
五大基础数据类型
https://blog.csdn.net/qq_50596778/article/details/124554777
https://blog.csdn.net/qq_50596778/article/details/124554777
string
可以是字符串、整数、浮点数
对整个字符串或字符串一部分进行操作;对整数或浮点数进行自增或自减操作
经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中
list
一个链表,链表上每一个节点都包含一个字符串
对列表的两端进行push和pop操作;读取单个或多个元素;根据值查找或删除元素
set
包含字符串的无序集合
字符串的集合,包含基础的方法有看是否存在添加、获取、删除;还包含计算交集、并集、差集等
hash
是一个String类型的field和value的映射表
特别适合存储对象
zset
事务
Redis将“多个命令打包, 然后一次性、按顺序地执行”的机制,
并且事务在执行的期间不会主动中断,执行完这所有的命令之
后才会执行其他的命令。可以将Redis事务视为一个队列,在开
启MULTI命令之后,Redis进入事务模式,将接下来的命令放入
到这个队列中,当执行到EXEC时将队列中的命令按照先进先
出(FIFO)依次执行,期间不会执行其他命令,直到全部执行完成
并且事务在执行的期间不会主动中断,执行完这所有的命令之
后才会执行其他的命令。可以将Redis事务视为一个队列,在开
启MULTI命令之后,Redis进入事务模式,将接下来的命令放入
到这个队列中,当执行到EXEC时将队列中的命令按照先进先
出(FIFO)依次执行,期间不会执行其他命令,直到全部执行完成
watch:监视指定的key
watch的实现:每个代表Redis数据库的redis.h/redisDB的文件中,
都保存着一个watched_keys字典,这个字典记录着当前数据库所
有被watch的key的数组,每个key的后面都有一个链表,链表上记
录着当前key被哪些节点watch。(整个结构类型hashmap)。在任何
数据库空间修改key的操作(包括MULTI命令),会触发遍历
watched_keys,查看是否有要被修改的key被客户端监视,如果有,
则会开启监视这个key的客户端的REDIS_DIRTY_CAS选项。当执行
EXEC命令时,会检查REDIS_DIRTY_CAS选项是否打开。如果打开,
则取消事务的执行
都保存着一个watched_keys字典,这个字典记录着当前数据库所
有被watch的key的数组,每个key的后面都有一个链表,链表上记
录着当前key被哪些节点watch。(整个结构类型hashmap)。在任何
数据库空间修改key的操作(包括MULTI命令),会触发遍历
watched_keys,查看是否有要被修改的key被客户端监视,如果有,
则会开启监视这个key的客户端的REDIS_DIRTY_CAS选项。当执行
EXEC命令时,会检查REDIS_DIRTY_CAS选项是否打开。如果打开,
则取消事务的执行
unwatch:取消监视指定的key
DISCARD:取消事务
Redis事务不支持事务的回滚。要么执行成功,要么不执行
管道
https://zhuanlan.zhihu.com/p/64381987
1、使用管道技术可以显著提升Redis处理命令的速度,其原理就是
将多条命令打包,只需要一次网络开销,在服务器端和客户端各一
次read()和write()系统调用,以此来节约时间
2、管道中的命令数量要适当,并不是越多越好。
3、Redis2.6版本以后,脚本在大部分场景中的表现要优于管道。
将多条命令打包,只需要一次网络开销,在服务器端和客户端各一
次read()和write()系统调用,以此来节约时间
2、管道中的命令数量要适当,并不是越多越好。
3、Redis2.6版本以后,脚本在大部分场景中的表现要优于管道。
持久化
AOF
1、AOF日志是持续增量的备份,是基于写命令存储的
可读的文本文件,默认是关闭的,可以通过配置打开
2、因为每一次的持久化都涉及到磁盘IO,同时linux
对磁盘IO有个”延迟读写“机制,所以推荐everysec模式
可读的文本文件,默认是关闭的,可以通过配置打开
2、因为每一次的持久化都涉及到磁盘IO,同时linux
对磁盘IO有个”延迟读写“机制,所以推荐everysec模式
no
不进行持久化
everySec
一秒一次
always
每次写操作都进行持久化
1、AOF重写,随着时间推移,aof文件会越来越大,
所以需要进行重写,对aof文件进行瘦身。
2、AOF Rewrite 虽然是“压缩”AOF文件的过程,但
并非采用“基于原AOF文件”来重写或压缩,而是采
取了类似RDB快照的方式:基于Copy On Write,
全量遍历内存中数据,然后逐个序列到AOF文件中。
因此AOF rewrite能够正确反应当前内存数据的状态。
3、重写过程中,对于新的变更操作将仍然被写入到
原AOF文件中,同时这些新的变更操作也会被Redis
收集起来。当内存中的数据被全部写入到新的AOF
文件之后,收集的新的变更操作也将被一并追加到
新的AOF文件中
4、重写也是有手动触发和自动触发两种方式
所以需要进行重写,对aof文件进行瘦身。
2、AOF Rewrite 虽然是“压缩”AOF文件的过程,但
并非采用“基于原AOF文件”来重写或压缩,而是采
取了类似RDB快照的方式:基于Copy On Write,
全量遍历内存中数据,然后逐个序列到AOF文件中。
因此AOF rewrite能够正确反应当前内存数据的状态。
3、重写过程中,对于新的变更操作将仍然被写入到
原AOF文件中,同时这些新的变更操作也会被Redis
收集起来。当内存中的数据被全部写入到新的AOF
文件之后,收集的新的变更操作也将被一并追加到
新的AOF文件中
4、重写也是有手动触发和自动触发两种方式
RDB
RDB快照是某个时间点的一次全量数据备份
,是二进制文件,在存储上非常紧凑
执行步骤:当需要进行RDB持久化时,首先判断
是否有一个fork子进程,如果有则直接返回。如果
没有则会fork子进程将当前主内存中的数据进行快
速的持久化,并删除上一份快照,在此期间的写操
作会暂时放在一个内存区域当做副本,等fork子进
程完成之后会通知主进程,将新的快照文件同步到
主内存中,把副本中的写操作同步到主内存。
,是二进制文件,在存储上非常紧凑
执行步骤:当需要进行RDB持久化时,首先判断
是否有一个fork子进程,如果有则直接返回。如果
没有则会fork子进程将当前主内存中的数据进行快
速的持久化,并删除上一份快照,在此期间的写操
作会暂时放在一个内存区域当做副本,等fork子进
程完成之后会通知主进程,将新的快照文件同步到
主内存中,把副本中的写操作同步到主内存。
自动触发
1、redis.conf配置,如:save 900 1 ,表示900秒内至少又一次save操作,则会触发
2、当Redis进行shutdown命令时,如果没有开启AOF持久化,则会自动fork子进程进行持久化
3、主从同步,将数据从主节点同步到从节点时
2、当Redis进行shutdown命令时,如果没有开启AOF持久化,则会自动fork子进程进行持久化
3、主从同步,将数据从主节点同步到从节点时
手动触发
save命令,此命令会阻塞Redis主进程,知道持久化完成
bgsave命令(backgroundsave),主进程会fork一个子进程进行持久化,只会在fork子进程时阻塞短暂的时间
bgsave命令(backgroundsave),主进程会fork一个子进程进行持久化,只会在fork子进程时阻塞短暂的时间
缺点
1、RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,如果不采用压缩算法(主进程 fork 出子进程,其实是共享一份真实的内存空间,但是为了能在记录快照的时候,也能让主线程处理写操作,采用的是 Copy-On-Write(写时复制)技术,只有需要修改的内存才会复制一份出来,所以内存膨胀到底有多大,看修改的比例有多大),频繁执行成本过高(影响性能)
2、RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题(版本不兼容)
3、在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改(数据有丢失)
1、RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,如果不采用压缩算法(主进程 fork 出子进程,其实是共享一份真实的内存空间,但是为了能在记录快照的时候,也能让主线程处理写操作,采用的是 Copy-On-Write(写时复制)技术,只有需要修改的内存才会复制一份出来,所以内存膨胀到底有多大,看修改的比例有多大),频繁执行成本过高(影响性能)
2、RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题(版本不兼容)
3、在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改(数据有丢失)
Redis4.0 之后采用的混合持久化的方式
集群
单机模式
最简单的模式,一台机器启动Redis进程
主从模式
一个主节点,多个从节点。主节点负责写数据,从节点负
责读数据。主节点的数据和从节点的数据是完全一致的。
当主节点宕机,会重新选取一个从节点当做master顶替
上来,保证服务的高可用性。
缺点是:写能力受主节点单机限制、主从切换需要人工干预、
主从节点数据冗余
责读数据。主节点的数据和从节点的数据是完全一致的。
当主节点宕机,会重新选取一个从节点当做master顶替
上来,保证服务的高可用性。
缺点是:写能力受主节点单机限制、主从切换需要人工干预、
主从节点数据冗余
当有新的从节点上线时,同步的步骤如下:
1、从节点向主节点发送请求同步的命令
2、主节点进行BGSAVE命令,生成RDB快照,并将快照
文件发送给从节点,期间收到的写命令保存在缓冲区
3、从节点执行快照,
4、主节点向从节点发送保存在缓冲区的写命令
5、从节点执行主节点发送过来的命令
1、从节点向主节点发送请求同步的命令
2、主节点进行BGSAVE命令,生成RDB快照,并将快照
文件发送给从节点,期间收到的写命令保存在缓冲区
3、从节点执行快照,
4、主节点向从节点发送保存在缓冲区的写命令
5、从节点执行主节点发送过来的命令
命令传播:slave已经同步过master了,那么如果后续master
进行了写操作,比如说一个简单的set name redis,那么master
执行过当前命令后,会将当前命令发送给slave执行一遍,达成数据一致性。
进行了写操作,比如说一个简单的set name redis,那么master
执行过当前命令后,会将当前命令发送给slave执行一遍,达成数据一致性。
哨兵模式
哨兵模式在主从模式上多了一群哨兵节点。访问redis集群的
数据都是通过哨兵集群的,哨兵监控整个redis集群哨兵节点
是特殊的Redis节点,不存储数据。其他的主节点和从节点都
会存储数据。Sentinel很好的解决了故障转移,主从切换等问
题,它还可以主节点存活检测、主从运行情况检测。sentinel
每秒中向监控的节点发送ping命令,如果没有响应,则降级,
10秒后还没响应则重新选举新节点。这些对于客户端来说是无感的
数据都是通过哨兵集群的,哨兵监控整个redis集群哨兵节点
是特殊的Redis节点,不存储数据。其他的主节点和从节点都
会存储数据。Sentinel很好的解决了故障转移,主从切换等问
题,它还可以主节点存活检测、主从运行情况检测。sentinel
每秒中向监控的节点发送ping命令,如果没有响应,则降级,
10秒后还没响应则重新选举新节点。这些对于客户端来说是无感的
主从有的优点哨兵模式都有,并且是自动切换主从节点的。
但是当这种模式比较难以扩容,集群和节点容量达
到上限时,在线扩容会很复杂,并且也受到单个节点存储上限的限制
但是当这种模式比较难以扩容,集群和节点容量达
到上限时,在线扩容会很复杂,并且也受到单个节点存储上限的限制
Cluster 集群模式
具有 高可用、可扩展性、分布式、容错 等特性。通过数据分片的方式来进行数据共享问题,同时提供数据复制和故障转移功能。之前的都是将所有的数据存储在单个节点上的,因此存储的容量是有限制的。集群模式就是将所有的数据按照分片存储,当一个分片数量达到上限的时候,就进行分片处理。注意,每一个分片都是单独的主从结构,这样某一个分片的主节点出现故障了,当前分片的从节点可以顶上。集群的键空间被分割为16384个slots(即hash槽),通过hash的方式将数据分到不同的分片上的。
分片之后,读请求经过位运算取模找到对应的分片的从节点,而写请求则找到对应分片的主节点
当某个分片需要扩容时,增加了一个分片之后,槽需要重新分配,数据也需要重新迁移,但是服务不需要下线。redis集群的重新分片由redis内部的管理软件redis-trib负责执行。redis提供了进行重新分片的所有命令,redis-trib通过向节点发送命令来进行重新分片。
分片之后,读请求经过位运算取模找到对应的分片的从节点,而写请求则找到对应分片的主节点
当某个分片需要扩容时,增加了一个分片之后,槽需要重新分配,数据也需要重新迁移,但是服务不需要下线。redis集群的重新分片由redis内部的管理软件redis-trib负责执行。redis提供了进行重新分片的所有命令,redis-trib通过向节点发送命令来进行重新分片。
Redis集群满足AP(可用性和分区容错性)
如何保证Redis原子操作
方法一:单命令操作,Redis提供了INCR/DECR/SETNX 命令
方法二:加锁,加锁主要是将多客户端线程调用相同业务方法转
换为串行化处理,比如多个客户端调用同一个方法对某个键自增
换为串行化处理,比如多个客户端调用同一个方法对某个键自增
方法三:lua脚本。把多个命令写到一个脚本中,Redis会把整个
脚本当做一个命令来执行,且执行过程中不会被打断。这样就保证了原子操作
脚本当做一个命令来执行,且执行过程中不会被打断。这样就保证了原子操作
过期策略
(Redis采用惰性删除和定期删除两种)
(Redis采用惰性删除和定期删除两种)
惰性删除
在get这个key的时候检查是否过期,如果过期就删除,返回null
缺点,内存中存在过期的key占用空间,如果数据量大,会有内存泄漏风险
定时删除
在创建key的时候,设置一个定时器,让定时器在key过期时删除
缺点,占用CPU性能
定期删除
每隔一段时间删除一次过期的key
在内存友好方面不如”定时删除“,在CPU时间友好方面不如”惰性删除“
难点,设置清理时间间隔
缓存雪崩
设置缓存时,大量的key在同一时间失效
缓存穿透
查询一个一定不存在的数据,导致每次请求都进入到存储层
缓存击穿
热点数据,在过期时被大量访问
缓存回收策略
noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
allkeys-random: 回收随机的键使得新添加的数据有空间存放。
volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
allkeys-lfu:从所有键中驱逐使用频率最少的键
allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
allkeys-random: 回收随机的键使得新添加的数据有空间存放。
volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
allkeys-lfu:从所有键中驱逐使用频率最少的键
消息队列
rabbitMQ
https://zhuanlan.zhihu.com/p/63700605
https://zhuanlan.zhihu.com/p/63700605
1、Producer生产消息,消息分为有效载荷(payload)和标签(label)两部分,payload记载了传输的数据,label决定把消息发送给哪个消费者
2、交换机(exchange,rabbitMQ内部对象),可以理解为路由器,按照路由规则对消息进行分发,有三种路由策略(直连<direct>、通配<topic>、广播<fanout>等,交换机将消息按照路由规则发送到queue上就是bind)
3、queue(队列,rabbitMQ内部对象),用于存储消息,一个message可以被拷贝到多个queue中。如果queue不存在,producer和consumer都会尝试创建queue
4、consumer,如果有多个消费者同时订阅同一个queue中的消息,queue中的消息会被平摊给多个消费者。
5、broker,即RabbitMQ Server ,其作用是维护一组从producer到consumer的路线,保证数据能够按照指定的方式传输
2、交换机(exchange,rabbitMQ内部对象),可以理解为路由器,按照路由规则对消息进行分发,有三种路由策略(直连<direct>、通配<topic>、广播<fanout>等,交换机将消息按照路由规则发送到queue上就是bind)
3、queue(队列,rabbitMQ内部对象),用于存储消息,一个message可以被拷贝到多个queue中。如果queue不存在,producer和consumer都会尝试创建queue
4、consumer,如果有多个消费者同时订阅同一个queue中的消息,queue中的消息会被平摊给多个消费者。
5、broker,即RabbitMQ Server ,其作用是维护一组从producer到consumer的路线,保证数据能够按照指定的方式传输
direct模式
直接根据routing key 全匹配,将消息发送给匹配的队列。这种模式下,
一个交换机可以连接多个队列,消息发送到哪个队列根据 routing key 全匹配分发
一个交换机可以连接多个队列,消息发送到哪个队列根据 routing key 全匹配分发
fanout模式
这种模式不需要routing key,直接将消息复制多份分发给绑定到这个交换机上的每个队列
topic模式
是direct的通配符模式,根据routing key的通配符进行匹配,选择符合的队列
如何保证消息的可靠传输
生产者消息丢失
MQ的事务功能
即当发送者发送消息之前开启事务,如果消息没有被RabbitMQ成功接收到,发送者就会接收
到报错,于是就回滚,然后重新发送;如果RabbitMQ成功接收到了消息,那就成功提交事务
即当发送者发送消息之前开启事务,如果消息没有被RabbitMQ成功接收到,发送者就会接收
到报错,于是就回滚,然后重新发送;如果RabbitMQ成功接收到了消息,那就成功提交事务
不推荐使用,事务是同步的,大大影响了MQ的吞吐量
confirm机制
在消息的生产者开启confirm模式,那么没条消息就会分配一个唯一id,然后如果消息成功写入MQ中,就会回传一个ack消息,如果失败则会回传一个nack消息,所以我们可以根据这个机制维护这个消息id的状态,如果长时间没收到ack消息则认为发送失败,则可以重新发送消息
在消息的生产者开启confirm模式,那么没条消息就会分配一个唯一id,然后如果消息成功写入MQ中,就会回传一个ack消息,如果失败则会回传一个nack消息,所以我们可以根据这个机制维护这个消息id的状态,如果长时间没收到ack消息则认为发送失败,则可以重新发送消息
异步处理
MQ消息丢失
MQ消息的持久化在以下两点都要满足才能保证消息的持久化
1、队列queue的持久化,即在创建队列时,设置为持久化的队列,这样队列的元数据就可以写到磁盘中
2、消息的持久化,在发送者端,设置deliveryMode = 2,MQ就会将消息写入磁盘中
1、队列queue的持久化,即在创建队列时,设置为持久化的队列,这样队列的元数据就可以写到磁盘中
2、消息的持久化,在发送者端,设置deliveryMode = 2,MQ就会将消息写入磁盘中
持久化可以跟生产者那边的confirm机制结合起来使用,当消息被写入磁盘了之后
才发送ack消息,这样哪怕消息在写入磁盘前丢失都能通过confirm机制再发一次
才发送ack消息,这样哪怕消息在写入磁盘前丢失都能通过confirm机制再发一次
消费者消息丢失
关闭MQ的自动ack模式,改为手动ack。
如何保证消息的顺序消费
每个消费者都有各自的一个单独的队列
如果一个队列有多个消费者,那么可以在消费者内存中维护一个队列来保证顺序消费
集群环境
集群环境下,考虑到空间的因素,RabbitMQ并不会将消息冗余到所有的节点,但是每个节点都会保存一份队列,交换机,绑定对象和虚拟主机等元数据。而队列的完整数据(包含消息)则是会保存在创建这个队列的那个节点中。如果消息在A节点,但是消费者却订阅在B节点,那么RabbitMQ将会把消息从A节点复制到B节点中。
如果节点故障,消息是否会丢失,则需要看故障节点是内存模式还是磁盘模式。如果是内存模式,那么附着在此节点上的绑定和队列都会消失,并且消费者可以重新连接集群并创建队列;如果是磁盘模式,重新恢复后,这个节点的队列又可以传输数据了,并且在恢复故障的磁盘节点之前,不能再其他节点上让消费者重新连接集群并创建队列。如果消费者在其他节点声明了该队列,则会得到一个404NOT_FOUND错误,这样确保了当故障节点恢复后加入集群,该节点上的队列消息不回丢失,也避免了队列会在一个节点以上出现冗余的问题
如果节点故障,消息是否会丢失,则需要看故障节点是内存模式还是磁盘模式。如果是内存模式,那么附着在此节点上的绑定和队列都会消失,并且消费者可以重新连接集群并创建队列;如果是磁盘模式,重新恢复后,这个节点的队列又可以传输数据了,并且在恢复故障的磁盘节点之前,不能再其他节点上让消费者重新连接集群并创建队列。如果消费者在其他节点声明了该队列,则会得到一个404NOT_FOUND错误,这样确保了当故障节点恢复后加入集群,该节点上的队列消息不回丢失,也避免了队列会在一个节点以上出现冗余的问题
注意:
1、在单节点RabbitMQ上,仅允许该节点是磁盘节点,这样确保了发生故障或重启节点后,所有关于系统的配置和元数据都能恢复
2、在集群中,要求集群中至少有一个磁盘节点,其他所有节点都是内存节点也可以。当集群中有节点加入或离开时,必须将此信息同步到至少一个磁盘节点
3、如果集群中唯一的一个磁盘节点发生故障,集群可以继续路由消息,但是不能创建队列、交换机、用户、绑定、修改权限等。因为这些信息都需要持久化到磁盘中
1、在单节点RabbitMQ上,仅允许该节点是磁盘节点,这样确保了发生故障或重启节点后,所有关于系统的配置和元数据都能恢复
2、在集群中,要求集群中至少有一个磁盘节点,其他所有节点都是内存节点也可以。当集群中有节点加入或离开时,必须将此信息同步到至少一个磁盘节点
3、如果集群中唯一的一个磁盘节点发生故障,集群可以继续路由消息,但是不能创建队列、交换机、用户、绑定、修改权限等。因为这些信息都需要持久化到磁盘中
节点模式
内存模式:非持久化的消息一般只存放在内存中,当内存吃紧的时候才会持久化到磁盘
磁盘模式:持久化的消息再达到队列时就会被持久化到磁盘,如果可以,持久化的消息也会在内存中进行备份以提高吞吐量,如果内存吃紧则会清除内存
节点类型和持久化的关系
内存节点是将元数据存在内存中;
磁盘节点是将元数据存在磁盘中;
对于 队列(queue),消息(message) 等的持久化或非持久化方式和节点类型没有任何关系,持久化就存磁盘,非持久化就存内存;
所以对于在非持久化队列下,内存节点的消息存取会比磁盘节点的块;而持久化队列下,消息存取的速度没有任何区别;
磁盘节点是将元数据存在磁盘中;
对于 队列(queue),消息(message) 等的持久化或非持久化方式和节点类型没有任何关系,持久化就存磁盘,非持久化就存内存;
所以对于在非持久化队列下,内存节点的消息存取会比磁盘节点的块;而持久化队列下,消息存取的速度没有任何区别;
kafka
rocketMQ
https://blog.csdn.net/qq_21040559/article/details/122703715
https://blog.csdn.net/qq_21040559/article/details/122703715
NameServer
相当于集群管理服务(NameServer之间不互联),主要用于管理整个集群元数据以及对集群进行管理
Broker
相当于单个节点负责数据的读写
Producer
Producer负责生产消息后写入broker
Consumer
从broker消费消息
ActiveMQ
RPC框架
dubbo
注册中心
生产者在此注册并发布内容,消费者在此订阅并接收发布的内容
注册中心一般有 zookeeper、nacos等
提供者
服务端,生产内容,生产前需要依赖容器(先启动容器
消费者
服务下线。某个客户端主动下线,会通知eureka server ,服务端会将其从注册表剔除
客户端,从注册中心获取到方法,可以调用生产者中的方法。
容器
监控中心
是dubbo提供的一个jar工程。主要功能是监控服务端和消费端的使用数据。
如:服务端是什么,有多少接口,多少方法,调用次数,压力信息等,客户端有多少,调用过哪些服务端,调用了多少次等
如:服务端是什么,有多少接口,多少方法,调用次数,压力信息等,客户端有多少,调用过哪些服务端,调用了多少次等
gPRC
Thrift
Spring cloud
Netty
数据库框架
ORM层框架
mybatis
Hibernate
JPA
连接池
druid
C3P0
HikariCP
分库分表
mycat
sharding-JDBC
sharding-sphare
搜索引擎
elasticsearch
solr
分布式/微服务
服务注册/发现
eureka
1、服务注册、服务发现和服务检测监控
2、eureka服务端和eureka客户端(分为服务的提供者和服务调用者)
3、每一个erueka server的地位都是相等的,只要有一个节点可用,那么服务就是可用的
2、eureka服务端和eureka客户端(分为服务的提供者和服务调用者)
3、每一个erueka server的地位都是相等的,只要有一个节点可用,那么服务就是可用的
服务注册。当服务提供者(eureka客户端)启动时,会向eureka服务端注册信息,提供自身的元数据,比如IP,端口,运行状况,首页等,eureka服务端有二级缓存来维护这个注册信息
提供注册表。服务消费者在调用服务时,如果 Eureka Client 没有缓存注册表的话,会从 Eureka Server 获取最新的注册表
同步状态。Eureka Client 通过注册、心跳机制和 Eureka Server 同步当前客户端的状态。
服务续约。eureka Client每隔30秒会向 eureka server 发送心跳告诉服务端当前客户端是正常的。默认情况下服务端90秒没收到客户端的心跳,会将当前服务从注册表中剔除
服务剔除。即当客户端不再有心跳发送时,将其从注册表中剔除
注册表拉取。客户端每隔30秒从服务端拉取注册表信息,并缓存在客户端本地。如果从服务端返回的注册表信息和本地不一致,则重新拉取全部注册表信息,数据可以采用XML或者JSON格式传输,默认JSON
远程调用。客户端从注册中心获取到服务提供者信息之后,就可以通过http远程调用服务,如果服务提供者有多个时,会通过Ribbon进行负载均衡
自我保护。为了避免因为网络波动导致服务被错误的剔除的问题。eureka server采取了一个保护机制,就是当正常续约的服务占比不足85%时,不在剔除服务,同时接收新服务的注册和拉取,但不会同步到其他节点,保证当前节点的可用性,等待网络正常再同步到其他节点
作为注册中行,需要保证各个微服务的可用性,满足CAP原理中的AP(可用性和分区容错性)
zookeeper
注册中心时
1、服务的注册与发现
2、zookeeper提供了“心跳检测”功能,
它会定时向各个服务提供者发送一个
请求(实际上建立的是一个 socket 长连接)
2、zookeeper提供了“心跳检测”功能,
它会定时向各个服务提供者发送一个
请求(实际上建立的是一个 socket 长连接)
1、服务提供者向注册中心注册自己的元信息,实际上就是在ZK中生成一个Znode节点,该节点保存了服务提供者的IP、端口、调用协议等信息
2、服务调用方第一次调用时会向注册中心请求,得到服务提供方的信息并缓存在本地,后续的调用直接读取本地缓存信息进行直接调用
3、当某个服务的某台机下线或者宕机时或上线时,注册中心会向服务调用方同步信息,让调用方缓存在本地
4、服务提供方根据调用的数量来决定是否下线
2、服务调用方第一次调用时会向注册中心请求,得到服务提供方的信息并缓存在本地,后续的调用直接读取本地缓存信息进行直接调用
3、当某个服务的某台机下线或者宕机时或上线时,注册中心会向服务调用方同步信息,让调用方缓存在本地
4、服务提供方根据调用的数量来决定是否下线
分布式环境下的选举(多台ZK机器时,一般2n+1台)
角色区分
leader
集群中的领导者,所有的写请求都必须经过它;负责投票的发起和决议,更新系统状态
follower
接收客户端请求并响应,在选举中进行投票
observer
观察者,同步leader的所有数据,不参与投票;可以接收请求并转发给leader;目的是为了扩展,增加读写新能
状态区分
loading,正在搜寻领导者,正在选举中
leadering,当前节点即是领导者
following,leader已经选举出来,当前Server与之同步
observing,观察状态,同步leader状态,不参与投票。
ZAB(Zookeeper Atomic BrodCast)协议
选举(恢复)模式
当服务启动或者leader崩溃时就进入选举模式,选举完成后则进入同步模式
选举过程:
1、选举线程由当前 Server 发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的 Server
2、选举线程首先向所有 Server 发起一次询问(包括自己)
3、选举线程收到回复后,验证是否是自己发起的询问(验证zxid是否一致,递增的事务id),然后获取对方 的 serverid(myid),并存储到当前询问对象列表中,最后获取对方提议的 leader 相关信息 (serverid,zxid),并将这些信息存储到当次选举的投票记录表中
4、收到所有 Server 回复以后,就计算出 id 最大的那个 Server,并将这个 Server 相关信息设 置成下一次要投票的 Server
5、线程将当前 id 最大的 Server 设置为当前 Server 要推荐的 Leader,如果此时获胜的 Server 获得 n/2 + 1 的 Server 票数, 设置当前推荐的 leader 为获胜的 Server,将根据获胜的 Server 相关信息设置自己的状态,否则,继续这个过程,直到 leader 被选举出来。
1、选举线程由当前 Server 发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的 Server
2、选举线程首先向所有 Server 发起一次询问(包括自己)
3、选举线程收到回复后,验证是否是自己发起的询问(验证zxid是否一致,递增的事务id),然后获取对方 的 serverid(myid),并存储到当前询问对象列表中,最后获取对方提议的 leader 相关信息 (serverid,zxid),并将这些信息存储到当次选举的投票记录表中
4、收到所有 Server 回复以后,就计算出 id 最大的那个 Server,并将这个 Server 相关信息设 置成下一次要投票的 Server
5、线程将当前 id 最大的 Server 设置为当前 Server 要推荐的 Leader,如果此时获胜的 Server 获得 n/2 + 1 的 Server 票数, 设置当前推荐的 leader 为获胜的 Server,将根据获胜的 Server 相关信息设置自己的状态,否则,继续这个过程,直到 leader 被选举出来。
选举规则:
1、逻辑时钟小的选举结果被忽略,重新投票
2、统一逻辑时钟后,数据 version 大的胜出
3、数据 version 相同的情况下,server id 大的胜出
1、逻辑时钟小的选举结果被忽略,重新投票
2、统一逻辑时钟后,数据 version 大的胜出
3、数据 version 相同的情况下,server id 大的胜出
同步(广播)模式
选举模式退出后,将leader的状态信息广播个所有的跟随者,使得所有节点的数据保持一致
分布式锁
临时节点
永久节点
顺序节点
临时顺序节点
(一) ZooKeeper的每一个节点,都是一个天然的顺序发号器。
在每一个节点下面创建临时顺序节点(EPHEMERAL_SEQUENTIAL)类型,新的子节点后面,会加上一个次序编号,而这个生成的次序编号,是上一个生成的次序编号加一。
(二) ZooKeeper节点的递增有序性,可以确保锁的公平
一个ZooKeeper分布式锁,首先需要创建一个父节点,尽量是持久节点(PERSISTENT类型),然后每个要获得锁的线程,都在这个节点下创建个临时顺序节点。由于ZK节点,是按照创建的次序,依次递增的。为了确保公平,可以简单的规定:编号最小的那个节点,表示获得了锁。所以,每个线程在尝试占用锁之前,首先判断自己是排号是不是当前最小,如果是,则获取锁
(三)ZooKeeper的节点监听机制,可以保障占有锁的传递有序而且高效
每个线程抢占锁之前,先尝试创建自己的ZNode。同样,释放锁的时候,就需要删除创建的Znode。创建成功后,如果不是排号最小的节点,只需要等前一个Znode的通知就可以了。前一个Znode删除的时候,会触发Znode事件,当前节点能监听到删除事件,就是轮到了自己占有锁的时候
(四)ZooKeeper的节点监听机制,能避免羊群效应
在每一个节点下面创建临时顺序节点(EPHEMERAL_SEQUENTIAL)类型,新的子节点后面,会加上一个次序编号,而这个生成的次序编号,是上一个生成的次序编号加一。
(二) ZooKeeper节点的递增有序性,可以确保锁的公平
一个ZooKeeper分布式锁,首先需要创建一个父节点,尽量是持久节点(PERSISTENT类型),然后每个要获得锁的线程,都在这个节点下创建个临时顺序节点。由于ZK节点,是按照创建的次序,依次递增的。为了确保公平,可以简单的规定:编号最小的那个节点,表示获得了锁。所以,每个线程在尝试占用锁之前,首先判断自己是排号是不是当前最小,如果是,则获取锁
(三)ZooKeeper的节点监听机制,可以保障占有锁的传递有序而且高效
每个线程抢占锁之前,先尝试创建自己的ZNode。同样,释放锁的时候,就需要删除创建的Znode。创建成功后,如果不是排号最小的节点,只需要等前一个Znode的通知就可以了。前一个Znode删除的时候,会触发Znode事件,当前节点能监听到删除事件,就是轮到了自己占有锁的时候
(四)ZooKeeper的节点监听机制,能避免羊群效应
zookeeper在作为分布式锁时,必须满足一致性才行。所以满足CAP中的CP(一致性和分区容错性)
consul
nacos
网关
zuul
zuulServlet继承了HttpServlet,实现了init方法和service方法
service方法则是每次请求都会调用。总共有四种调用:preRout、rout、postRout、error
最终都会调用到runFilters,此处会得到所有的过滤器循环调用processZuulFilter
最终都会调用到runFilters,此处会得到所有的过滤器循环调用processZuulFilter
最终会调用到IZuulFilter.run()接口,从而执行到我们自定义的过滤规则
gateway
两者均是web网关,处理的是http请求
gateway对比zuul多依赖了spring-webflux,在spring的支持下,功能更强大,内部实现了限流、负载均衡等,扩展性也更强,但同时也限制了仅适合于Spring Cloud套件,而zuul则可以扩展至其他微服务框架中,其内部没有实现限流、负载均衡等
gateway很好的支持异步,而zuul仅支持同步,那么理论上gateway则更适合于提高系统吞吐量(但不一定能有更好的性能),最终性能还需要通过严密的压测来决定
从框架设计的角度看,gateway具有更好的扩展性,并且其已经发布了2.0.0的RELESE版本,稳定性也是非常好的
编码上看,zuul更加简洁易懂,注释规范清晰,而gateway作为Spring家族的一份子,竟然几乎不注释…
总的来说,在微服务架构,如果使用了Spring Cloud生态的基础组件,则Spring Cloud Gateway相比而言更加具备优势,单从流式编程+支持异步上就足以让开发者选择它了。
gateway对比zuul多依赖了spring-webflux,在spring的支持下,功能更强大,内部实现了限流、负载均衡等,扩展性也更强,但同时也限制了仅适合于Spring Cloud套件,而zuul则可以扩展至其他微服务框架中,其内部没有实现限流、负载均衡等
gateway很好的支持异步,而zuul仅支持同步,那么理论上gateway则更适合于提高系统吞吐量(但不一定能有更好的性能),最终性能还需要通过严密的压测来决定
从框架设计的角度看,gateway具有更好的扩展性,并且其已经发布了2.0.0的RELESE版本,稳定性也是非常好的
编码上看,zuul更加简洁易懂,注释规范清晰,而gateway作为Spring家族的一份子,竟然几乎不注释…
总的来说,在微服务架构,如果使用了Spring Cloud生态的基础组件,则Spring Cloud Gateway相比而言更加具备优势,单从流式编程+支持异步上就足以让开发者选择它了。
服务调用(负载均衡)
ribbon
客户端实现负载均衡
消费者从注册中心拉取需要调用
的服务提供者之后,在客户端自
己这边进行负载均衡,然后通过
http或者TCP协议调用服务提供方
消费者从注册中心拉取需要调用
的服务提供者之后,在客户端自
己这边进行负载均衡,然后通过
http或者TCP协议调用服务提供方
轮询策略
权重轮询
随机策略
最少并发数
可用性敏感策略
区域性敏感策略
feign
Feign 默认集成了 Ribbon,并和 Eureka 结合,默认实现了负载均衡的效果。
通过接口调用,添加@FeignClient注解,配置需要调用的服务名,即可实现负载均衡的调用
通过接口调用,添加@FeignClient注解,配置需要调用的服务名,即可实现负载均衡的调用
熔断/降级
hystrix
隔离(线程池隔离和信号量隔离):限制调用分布式服务的资源使用,某一个调用的服务出现问题不会影响其他服务调用。
优雅的降级机制:超时降级、资源不足时(线程或信号量)降级,降级后可以配合降级接口返回托底数据
融断:当失败率达到阀值自动触发降级(如因网络故障/超时造成的失败率高),熔断器触发的快速失败会进行快速恢复。
Nacos
配置中心
Config
Apollo
Nacos
认证和授权
SpringSecurity
shiro
OAuth2
SSO单点登录
分布式事务
JTA接口
Atomikos组件
2PC、3PC
2PC
思路概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈决定各个参与者是否要提交操作还是终止操作。
简单来说,就是将整个过程分为 准备(投票)阶段和提交(执行)阶段
缺点:
1、同步阻塞问题。所有参与节点都是事务阻塞型的,当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态
2、单点故障。协调者的重要性,一旦协调者发生故障,所有参与者都会一直阻塞下去。尤其是在第二阶段,所有参与者处在事务锁定状态。
3、数据不一致。在二阶段提交中,如果由于网络问题,某些节点没有接收到提交指令,某些节点收到提交指令,则会造成数据的不一致
简单来说,就是将整个过程分为 准备(投票)阶段和提交(执行)阶段
缺点:
1、同步阻塞问题。所有参与节点都是事务阻塞型的,当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态
2、单点故障。协调者的重要性,一旦协调者发生故障,所有参与者都会一直阻塞下去。尤其是在第二阶段,所有参与者处在事务锁定状态。
3、数据不一致。在二阶段提交中,如果由于网络问题,某些节点没有接收到提交指令,某些节点收到提交指令,则会造成数据的不一致
3PC
第一步,协调者询问参与者是否能提交;第二步,参与者响应yes;第三步,协调者发出提交指令,参与者响应执行情况
第二步中,如果有任何一个参与者响应NO或者是超时,那么协调者这会执行事务中断
第三步中,如果协调者收到一个执行失败或者超时响应,则执行中断或者回滚
第二步中,如果有任何一个参与者响应NO或者是超时,那么协调者这会执行事务中断
第三步中,如果协调者收到一个执行失败或者超时响应,则执行中断或者回滚
比较
1、3PC在协调者和参与者都引入超时机制。2、3PC在2PC的第一第二阶段都插入了一个准备阶段,保证最后提交之前各个参与节点的状态是一致的
XA模式
TCC模式
tcc-transaction
ByteTcc
Easy Transaction
Seata
SAGA模式
ServiceComb
Seata
LCN模式
tx-Icn
CAP原理
Consistency(一致性)
数据在多个系统中保证一致性,当两个系统AB,A系统数据发生改变,用户从B系统获取到的是改变后的数据
Availability(可用性)
系统对外提供服务必须一直处于可用状态,在任何故障下,客户端都能在合理时间内获得服务端非错误响应
Partition tolerance(分区容错性)
在任何分布式系统中,遇到任何网络分区故障,整个系统任然可以提供服务。
网络分区:分布式系统中,不同节点在不同子网络中,有可能子网络中只有一个节点,在所有网络正常的情况下,由于某些原因导致这些子节点之间的网络出现故障,导致整个节点环境被切分成不同的独立区域,这就是网络分区。
网络分区:分布式系统中,不同节点在不同子网络中,有可能子网络中只有一个节点,在所有网络正常的情况下,由于某些原因导致这些子节点之间的网络出现故障,导致整个节点环境被切分成不同的独立区域,这就是网络分区。
任务调度
Quartz
Elastic-Job
xxl-job
链路追踪与监控
Zipkin
Sleuth
Skywalking
日志分析与监控
ELK
elasticsearch
日志分析
Logstash/filebeat
日志收集
Kibana
可视化
虚拟化/容器化
容器技术
docker
dockerfile
docker 命令
containerd
ctr
podman
容器编排技术
Kubernetes
client-go
RESTClient
DiscoveryClient
DynamicClient
ClientSet
informer
k8s.io/apimachinery
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8s.io/api
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
batchv1 "k8s.io/api/batch/v1"
AdmissionWebhook
MutatingAdmissionWebhook
ValidatingAdmissionWebhook
SchedulerFramework
Operator
kubebuilder
Swarm
docker-compose
docker-compose 模板语法
docker-compose 命令
前端
基础套餐
三大件
HTML
JavaScript
CSS
基础库
jQuery
Ajax
模板框架
JSP/JSTL
Thymeleaf
FreeMarker
组件化框架
Vue
React
Angular
NodeJS
运维知识
web服务器
Nginx
应用服务器
Tomcat
Jetty
Undertow
CDN加速
持续集成/持续部署
Jenkins
代码质量检查
sonar
日志收集和分析
ELk
编程基础
Java语言
语言基础
基础语法
Java是值传递
形参
方法声明中的参数叫形参,在方法被调用时是用来接收参数的
实参
就是实际参数,用于调用时传递给方法的参数。实参在传递给别的方法之前是要被预先赋值的
值传递
在调用方法时,将实际参数拷贝一份传递给方法,这样在方法中修改形式参数时,不会影响到实际参数
引用传递
也叫地址传递,在调用方法时,将实际参数的地址传递给方法,这样在方法中对形式参数的修改,将影响到实际参数
static关键字
修饰变量
静态变量属于所有变量所共有,在内存中只会存在一个副本,当且仅当类被加载时初始化,并且声明周期变长,程序被销毁时销毁;而成员变量属于对象,存在多个副本,各个对象之间互不影响
修饰方法
public修饰的静态方法是所有类和对象共享的方法,由于静态方法不依赖于对象即可访问,所以也不能使用this来访问,因为this是指代的当前对象。并且静态方法是在类加载(比如 Class.forName())时加载的,而非静态方法是在new对象的时候加载的,所以在静态方法中不能调用非静态方法,而非静态方法可以调用静态方法。
静态代码块
static修饰静态块后,将要修饰的成员同一放在一个static中。该静态化会在第一次使用类时,最先初始化static修饰的静态块。并且在程序运行过程中,只需要初始化一次,不会进行多次初始化。
synchronized
修饰静态方法就是类锁
是如果有N个静态的方法被synchronized修饰,有一个线程执行其中一个加锁的静态方法,那么其他的线程就无法继续调用这个类中的其他的加锁的静态方法,直到这个静态方法被执行完,其他加锁静态方法才可以被执行。不加锁的方法是不受影响的。
修饰普通方法就是对象锁
对象锁的意义是,synchronized锁修饰的一个非静态方法在被new的对象调用时,这个对象调用的其他的synchronized修饰非静态方法,是无法执行的,直到正在被调用的方法执行完毕。但是不同的对象之间是互不影响的
类锁和对象锁是相互独立的,互不相斥。
变量
局部变量,在方法体内产生,在方法体内消亡,所以存在于栈中
成员变量
实例变量
随着对象的初始化而创建,每个对象中都存在一个副本,使用对象调用。存放在堆中
静态变量
因为随着类一起初始化,可以直接使用类名调用,存在于方法区中
面向对象
Java 是面向对象的编程语言,对象就是面向对象程序设计的核心。所谓对象就是真实世界中的实体,对象与实体是一一对应的,也就是说现实世界中每一个实体都是一个对象,它是一种具体的概念。对象有以下特点:
1、对象具有属性和行为。
2、对象具有变化的状态。
3、对象具有唯一性。
4、对象都是某个类别的实例。
一切皆为对象,真实世界中的所有事物都可以视为对象
1、对象具有属性和行为。
2、对象具有变化的状态。
3、对象具有唯一性。
4、对象都是某个类别的实例。
一切皆为对象,真实世界中的所有事物都可以视为对象
继承
程序中的继承性是指子类拥有父类的全部特征和行为,这是类之间的一种关系。Java 只支持单继承
封装
Java 语言的基本封装单位是类。由于类的用途是封装复杂性,所以类的内部有隐藏实现复杂性的机制。Java 提供了私有和公有的访问模式,类的公有接口代表外部的用户应该知道或可以知道的每件东西,私有的方法数据只能通过该类的成员代码来访问,这就可以确保不会发生不希望的事情
多态
面向对象的多态性,即“一个接口,多个方法”。多态性体现在父类中定义的属性和方法被子类继承后,可以具有不同的属性或表现方式。多态性允许一个接口被多个同类使用,弥补了单继承的不足
接口
容器
Collection
List
queue
deque
abstractList
ArrayList
LinkedList
Set
AbstractSet
HashSet
SortedSet
NavigableSet
TreeSet
Map
SortedMap
NavigableMap
TreeMap
AbstractMap
HashMap
1.7版本
链表是采用头插法
容易出现逆序且环形链表死循环问题
扩容后计算数据存储的位置
直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定
必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制
数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)
必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制
数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)
数组+单链表
先扩容后插入
1.8版本
链表采用尾插法
因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题
进行了9次扰动处理 = 4次位运算+5次异或
扩容后计算数据存储的位置
新位置 = (扩容前位置+扩容大小)/(原位置)
这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。
进行1次位运算,1次异或
进行1次位运算,1次异或
数组+单链表+红黑树
当链表深度达到8的时候自动转化为红黑树,将时间复杂度从O(n)转为Olog(n)。当红黑树数量为6时缩变为链表
如果选择6和8(如果链表小于等于6树还原转为链表,大于等于8转为树),
中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链
表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,
如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会
频繁的发生树转链表、链表转树,效率会很低。
还有一点重要的就是由于treenodes的大小大约是常规节点的两倍,因此我
们仅在容器包含足够的节点以保证使用时才使用它们,当它们变得太小(由
于移除或调整大小)时,它们会被转换回普通的node节点,容器中节点分布
在hash桶中的频率遵循泊松分布,桶的长度超过8的概率非常非常小。所以作
者应该是根据概率统计而选择了8作为阀值
中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链
表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,
如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会
频繁的发生树转链表、链表转树,效率会很低。
还有一点重要的就是由于treenodes的大小大约是常规节点的两倍,因此我
们仅在容器包含足够的节点以保证使用时才使用它们,当它们变得太小(由
于移除或调整大小)时,它们会被转换回普通的node节点,容器中节点分布
在hash桶中的频率遵循泊松分布,桶的长度超过8的概率非常非常小。所以作
者应该是根据概率统计而选择了8作为阀值
先插入后扩容
Hashtable
Dictionary
异常
运行时异常
这类异常不会被检查到,一般是运行时逻辑错误导致,可以不做处理,也可以捕获或者抛出
空指针异常
数组越界异常
ClassNotFoundException
算数异常
类型转换异常等等
编译异常
这类异常是会在编译时被检查到的,必须处理,如果不处理则编译不通过
泛型
目的:Java中引入泛型最主要的目的是将类型检查工作提前到编译时期,将类型强转(cast)工作交给编译器,从而让你在编译时期就获得类型转换异常以及去掉源码中的类型强转代码。
反射
注解
I/O
JVM虚拟机
类加载机制
双亲委派
启动类加载器bootstrapClassLoader
启动类加载器主要加载的是JVM自身需要的类,是用C++语言实现的,是虚拟机自身的一部分,他负责将/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)
扩展类加载器ExtClassLoader
扩展(ExtClassLoader)类加载器(由java语言实现,父类加载器为null),扩展类加载器是sun公司实现的sun.misc.Launcher$ExtClassLoader类,由java语言实现的,是Launcher的静态内部类,他负责加载/lib/ext目录下或者由系统变量-Djava.ext.dir指定路径中的类库,开发者可以直接使用标准扩展类加载器。
系统类加载器AppClassLoader
由java语言实现,父类加载器为ExtClassLoader),也称为应用程序加载器,他负责加载系统类路径java -classpath 或者 -D java.class.path指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获得到该类加载器。
这三种类加载器之前存在着父子关系(区别于jaba中的继承)子加载器保存着父加载器的引用,当一个类加载器需要加载一个目标类时,会先委托父加载器去加载,然后父加载器会在自己的加载路径中搜索目标类,父加载器在自己的加载范围中找不到时,才会交给子加载器加载目标类。采用双亲委托模式避免类加载混乱,而且还将类分层次了,例如java中lang包下的类在jvm启动时就会启动类加载器加载了,而用户一些代码类则由应用程序类加载器(AppcalssLoader)加载,基于双亲委托模式就算用户定义了与lang包一样的类,最终还是由应用程序类加载器委托给启动类加载器去加载,这个时候启动类加载器发现已经加载过了lang包下的类了,所以两者都不会再重新加载。当然如果使用者通过自定义的类加载器可以强行打破这种双亲委托模式,但也不会成功的,java安全管理器抛出了java.lang.SecurityException异常。
非双亲委派
线程上下文类加载器。核心包的SPI(如JDBC,JNDI)类对外部实现类的加载都是基于线程上下文类加载器的执行的,通过这种方式实现了java核心代码内部去调用外部实现。SPI接口属于Java核心库,但是实现却是第三方库,存在classpath路径下。由于双亲委派,启动类加载器无法反向调用系统加载器加载,这时就需要一种特殊的类加载器来实现。线程上下文类加载器(contextClassLoader)是从JDK1.2开始引入的,我们可以通过java.lang.Thread类中的getContextClassLoader()和setContextClassLoader(ClassLoaser c)方法来获取和设置线程的上线文类加载器。如果没有手动设置上下文类加载器,线程将继承其父类线程的上下文类加载器,初始化线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源。简而言之就是ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或者是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextLoader()取出应用程序类加载器来完成要的操作
线程上下文类加载器默认情况下就是AppClassLoader,那为什么不直接通过getSystemClassLoader()获取类加载器来加载classpath路径下的类的呢?其实是可行的,但这种直接使用getSystemClassLoader()方法获取AppClassLoader加载类有一个缺点,那就是代码部署到不同服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,因为这些服务使用的线程上下文类加载器并非AppClassLoader,而是Java Web应用服自家的类加载器,类加载器不同。所以我们应用该少用getSystemClassLoader()。总之不同的服务使用的可能默认ClassLoader是不同的,但使用线程上下文类加载器总能获取到与当前程序执行相同的ClassLoader,从而避免不必要的问题
用户追求动态性,从而破坏了双亲委派模型,比如:代码热替换,模块热部署,
线程上下文类加载器默认情况下就是AppClassLoader,那为什么不直接通过getSystemClassLoader()获取类加载器来加载classpath路径下的类的呢?其实是可行的,但这种直接使用getSystemClassLoader()方法获取AppClassLoader加载类有一个缺点,那就是代码部署到不同服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,因为这些服务使用的线程上下文类加载器并非AppClassLoader,而是Java Web应用服自家的类加载器,类加载器不同。所以我们应用该少用getSystemClassLoader()。总之不同的服务使用的可能默认ClassLoader是不同的,但使用线程上下文类加载器总能获取到与当前程序执行相同的ClassLoader,从而避免不必要的问题
用户追求动态性,从而破坏了双亲委派模型,比如:代码热替换,模块热部署,
web容器类加载器
Web 类容器类加载器是一种特殊的类加载器,它用于加载 Web 应用程序的类和资源。它的主要作用是从 Web 应用程序的类路径中加载类和资源,并将它们提供给 Web 应用程序。Web 类容器类加载器通常是基于父类加载器的,它可以从父类加载器中加载类和资源,也可以从 Web 应用程序的类路径中加载类和资源。
字节码执行机制
JVM内存模型
线程共享区
堆
Heap(堆)是JVM的内存数据区,Heap的管理很复杂,是被所有线程共享的内存区域,在JVM启动的时候创建,专门用来保存对象的实例,在Heap中分配一定的内存来保存对象的实例,实际上也只是保存对象实例的属性值、属性的类型和对象本身的类型标记等,并不保存对象的方法(以栈帧的形式保存在Stack中)。而对象实例在Heap中分配好以后,需要在Stack中保存一个4字节的Heap内存地址,用来定位该对象实例在Heap中的位置,以便找到该对象实例,是垃圾回收的主要场所。JAVA堆处于物理不连续的内存空间中,只要逻辑上连续即可。
方法区
类型信息是存储在方法区的。除此之外,常量、静态变量、JIT(即时编译器)编译后的代码也都在方法区。正因为方法区所存储的数据与堆有一种类比关系,所以它还被称为Non-Heap。方法区也可以是内存不连续的区域组成的,并且可设置为固定大小,也可以设置为可扩展的,这点与堆一样。垃圾回收在这个区域会比较少出现,这个区域内存回收的目的主要针对常量池的回收和类的卸载。
非线程共享区,生命周期和当前线程一样,
随着线程的创建而创建,线程结束也随之结束
随着线程的创建而创建,线程结束也随之结束
方法栈
存储局部变量表,操作栈,动态链接,方法出口等信息
程序计数器
程序计数器是Java对物理硬件的屏蔽和抽象,他在物理上是通过寄存器来实现的,寄存器可以说是整个CPU组件里读取速度最快的一个单元,因为读取/写指令地址这个动作是非常频繁的。所以Java虚拟机在设计的时候就把CPU中的寄存器当做了程序计数器,用他来存储地址,将来去读取这个地址。用于存储下一条指令的地址。详细的说PC寄存器是用来存储指向下一条指令的地址,也就是即将将要执行的指令代码。由执行引擎读取下一条指令。
线程私有;是JVM规范中唯一一个不会内存溢出的地方;执行Java方法时有值,执行native方法时没有值
本地方法区
GC垃圾回收(Garbage Collection)
判断垃圾的方法
引用计数法。标记每个对象被引用的次数,当次数为0时表示可以回收。但是当有对象循环引用时,这种方法会失效
可达性分析法。这个算法的基本思想就是通过一系列的GC Roots作为起始点,从这些节点开始向下搜索,搜索走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连接的话,则说明此对象是不可用的。
垃圾回收算法
标记-清除算法
这个算法分为两个阶段,第一个阶段扫描内存先将需要回收的垃圾标记好,第二阶段,再次扫描内存将垃圾清除掉。
这个算法有两个问题:
1、需要两次扫描内存,耗时间
2、清除完之后会存在很多内存碎片
1、需要两次扫描内存,耗时间
2、清除完之后会存在很多内存碎片
拷贝算法
这个算法是将内存一分为二,使用时只使用一半的内存空间。回收时将需要留存的对象拷贝到另一半内存空间里,然后将正在使用的另一半完全清除掉
这个算法也有问题:
1、只能使用一半内存空间,比较浪费
2、在清理内存时需要复制对象,调整对象的引用地址
1、只能使用一半内存空间,比较浪费
2、在清理内存时需要复制对象,调整对象的引用地址
标记整理算法
算法也分为两步,第一步和标记清除算法一样,第二步则让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
1、也会扫描两次内存
2、会调整对象的引用
2、会调整对象的引用
当前商业虚拟机的垃圾收集都采用分代收集(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清理或者标记—整理算法来进行回收
垃圾分代收集
新生代
新生代又被分为 Eden区、from surviver区、to surviver区,比例分别为 8:1:1
每当新生代内存耗尽就会触发一次young GC ,剩余的对象年龄+1,直到年龄达到15岁,就会进入老年代。young GC 采用的算法是拷贝算法
老年代
每次老年代内存快被耗尽就会触发 full GC ,老年代采用的算法是 标记清除算法或标记整理算法。这两种算法都比较耗时,所以尽量避免full GC
逃逸分析
逃逸分析就是分析java对象的动态作用域。当一个对象被定义之后,可能会被外部对象引用,称之为方法逃逸;也有可能被其他线程所引用,称之为线程逃逸 。如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配(比如方法内的临时对象)。这样就无需在堆上分配内存,也无须进行垃圾回收。
常见回收器,分别使用什么算法
关于GC的选择
除非应用程序有非常严格的暂停时间要求,否则请先运行应用程序并允许VM选择收集器(如果没有特别要求。使用VM提供给的默认GC就好)。
如有必要,调整堆大小以提高性能。 如果性能仍然不能满足目标,请使用以下准则作为选择收集器的起点:
● 如果应用程序的数据集较小(最大约100 MB),则选择带有选项-XX:+ UseSerialGC的串行收集器。
● 如果应用程序将在单个处理器上运行,并且没有暂停时间要求,则选择带有选项-XX:+ UseSerialGC的串行收集器。
● 如果(a)峰值应用程序性能是第一要务,并且(b)没有暂停时间要求或可接受一秒或更长时间的暂停,则让VM选择收集器或使用-XX:+ UseParallelGC选择并行收集器 。
● 如果响应时间比整体吞吐量更重要,并且垃圾收集暂停时间必须保持在大约一秒钟以内,则选择具有-XX:+ UseG1GC。(值得注意的是JDK9中CMS已经被Deprecated,不可使用!移除该选项)
● 如果使用的是jdk8,并且堆内存达到了16G,那么推荐使用G1收集器,来控制每次垃圾收集的时间。
● 如果响应时间是高优先级,或使用的堆非常大,请使用-XX:UseZGC选择完全并发的收集器。(值得注意的是JDK11开始可以启动ZGC,但是此时ZGC具有实验性质,在JDK15中[202009发布]才取消实验性质的标签,可以直接显示启用,但是JDK15默认GC仍然是G1)
这些准则仅提供选择收集器的起点,因为性能取决于堆的大小,应用程序维护的实时数据量以及可用处理器的数量和速度。
如果推荐的收集器没有达到所需的性能,则首先尝试调整堆和新生代大小以达到所需的目标。 如果性能仍然不足,尝试使用其他收集器
总体原则:减少STOP THE WORD时间,使用并发收集器(比如CMS+ParNew,G1)来减少暂停时间,加快响应时间,并使用并行收集器来增加多处理器硬件上的总体吞吐量。
如有必要,调整堆大小以提高性能。 如果性能仍然不能满足目标,请使用以下准则作为选择收集器的起点:
● 如果应用程序的数据集较小(最大约100 MB),则选择带有选项-XX:+ UseSerialGC的串行收集器。
● 如果应用程序将在单个处理器上运行,并且没有暂停时间要求,则选择带有选项-XX:+ UseSerialGC的串行收集器。
● 如果(a)峰值应用程序性能是第一要务,并且(b)没有暂停时间要求或可接受一秒或更长时间的暂停,则让VM选择收集器或使用-XX:+ UseParallelGC选择并行收集器 。
● 如果响应时间比整体吞吐量更重要,并且垃圾收集暂停时间必须保持在大约一秒钟以内,则选择具有-XX:+ UseG1GC。(值得注意的是JDK9中CMS已经被Deprecated,不可使用!移除该选项)
● 如果使用的是jdk8,并且堆内存达到了16G,那么推荐使用G1收集器,来控制每次垃圾收集的时间。
● 如果响应时间是高优先级,或使用的堆非常大,请使用-XX:UseZGC选择完全并发的收集器。(值得注意的是JDK11开始可以启动ZGC,但是此时ZGC具有实验性质,在JDK15中[202009发布]才取消实验性质的标签,可以直接显示启用,但是JDK15默认GC仍然是G1)
这些准则仅提供选择收集器的起点,因为性能取决于堆的大小,应用程序维护的实时数据量以及可用处理器的数量和速度。
如果推荐的收集器没有达到所需的性能,则首先尝试调整堆和新生代大小以达到所需的目标。 如果性能仍然不足,尝试使用其他收集器
总体原则:减少STOP THE WORD时间,使用并发收集器(比如CMS+ParNew,G1)来减少暂停时间,加快响应时间,并使用并行收集器来增加多处理器硬件上的总体吞吐量。
JVM性能调优
各版本JDK选用
JDK8
JDK11新特性
ZGC:并行的FullGC(JDK8的G1GC是串行),快速的CardTable扫描,自适应堆比例调整,在并发标记阶段进行类型卸载。
更新支持到Unicode10、将Http Client 作为JDK标准、支持动态分配Compiler Threads,可以控制编译线程的数量,原来的编译线程默认会启动大量造成CPU和内存的浪费
新增优化很多方法:String、Collection、InputStream、nio包 、
堆分析能力提升:JVMTI、TLS1.3更新
嵌套访问控制:JVM 访问规则不允许嵌套类之间进行私有访问。我们能通过常规方式可以访问是因为 JVM 在编译时为我们隐式地创建了 桥接方法 。Java 11 中引入了两个新的属性:一个叫做 NestMembers 的属性,用于标识其它已知的静态 nest 成员;另外一个是每个 nest 成员都包含的 NestHost 属性,用于标识出它的 nest 宿主类。在编译期就映射了双方的寄宿关系,不再需要桥接了。
本地参数支持lambda,简单理解就是lambda表达式的变量申明可以用var
单个Java文件可以直接java命令运行
飞行记录器分析工具:Jvm启动参数:-XX:StartFlightRecording
内存泄漏
ThreadLocal内存泄漏
内存溢出
多线程/并发
并发编程的基础
线程的创建
继承Thread类创建线程
实现Runnable接口创建线程
使用Callable和Future创建线程
使用线程池,例如Executor框架
线程池
FixedThreadPool
核心线程数和最大线程数相等的线程池,会把超出线程
处理能力的任务放到任务队列中进行等待
处理能力的任务放到任务队列中进行等待
CachedThreadPool
核心线程数为0,最大线程数为Integer最大值的线程池,
该线程池的线程数量不是固定不变的,当然它也有一个
用于存储提交任务的队列,但这个队列是
SynchronousQueue,队列的容量为0,实际不存储任何
任务,它只负责对任务进行中转和传递,所以效率比较高
该线程池的线程数量不是固定不变的,当然它也有一个
用于存储提交任务的队列,但这个队列是
SynchronousQueue,队列的容量为0,实际不存储任何
任务,它只负责对任务进行中转和传递,所以效率比较高
ScheduledThreadPool
支持定时或周期性执行任务。比如每隔 10 秒钟执行一次任务,
而实现这种功能的方法主要有 3 种:
第一种,表示延迟指定时间后执行一次任务
第二种,以固定周期执行任务,不论上次任务是否执行完成
第三种,表示在上一个任务完成后延迟指定时间执行一次任务
而实现这种功能的方法主要有 3 种:
第一种,表示延迟指定时间后执行一次任务
第二种,以固定周期执行任务,不论上次任务是否执行完成
第三种,表示在上一个任务完成后延迟指定时间执行一次任务
singleThreadPool
单线程线程池,线程数只有一个,如果执行任务过程中报错,
则会创建新的线程执行。这个线程池可以保证提交的任务按照顺序执行
则会创建新的线程执行。这个线程池可以保证提交的任务按照顺序执行
singleThreadScheduledPool
ScheduledThreadPool的一个特例,只有一个线程
ForkJoinPool
Java7加入的线程池,这是一个可以fork子线程的线程池,除了有一个公共
的队列外,每个线程还会有各自的一个deque双端队列来保存各自的子线程
的队列外,每个线程还会有各自的一个deque双端队列来保存各自的子线程
线程池7个核心参数
核心线程数
最大线程数
存活时间,意思是当线程超过核心线程数的线程在执行完任务后的存活时间
存活时间单位
任务队列
线程工厂
拒绝策略
线程的生命周期
1、创建
使用new关键字创建一个线程之后,该线程就处于一个新建状态(初始状态),此时它和其他Java对象一样,仅仅由虚拟机为其分配了内存,并初始化了成员变量,此时线程对象没有任何线程动态特征,也不会运行线程执行体
2、就绪
start()方法之后,该线程就处于就绪状态,虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,只是表示可以运行了,真正的运行还需要线程调度器的调度
3、运行
线程调度器调度了就绪线程列表的线程,就会进入运行状态
4、阻塞
线程因为某种原因放弃CPU
使用权,暂时停止运行
线程因为某种原因放弃CPU
使用权,暂时停止运行
等待阻塞,执行wait方法(wait方法会释放锁)
同步阻塞,运行的线程在获取对象同步锁时,而改锁被其他线程占用,JVM会把当前线程放入锁池中
其他阻塞
线程睡眠,sleep方法,millis设定线程睡眠时间,线程睡眠结束后转为就绪态
线程等待,wait方法,导致当前线程等待,知道其他线程调用此线程的notfify方法或者notifyAll方法唤醒,这三个方法都是Object方法,线程唤醒后进入了就绪状态,类似于wait(0)。
线程让步,yield方法,把当前线程回到就绪态,把执行机会让给相同或更高优先级的线程
线程加入,join方法,底层调用的是wait。例如,在main方法中,创建一个线程A并调用start方法,然后A.join(),那么会优先执行完A线程的run方法,再执行main方法。
线程I/O阻塞,等待相关资源
5、死亡
run方法执行完成,线程正常结束
线程抛出一个未捕获的异常或error
直接调用该线程的stop()方法结束该线程,该方法容易导致死锁,不推荐使用
守护线程
1、在Java中分为两种线程,用户线程和守护线程,任何一个守护线程都是所有非守护线程的保姆
2、只要当前当前JVM中存在着任何一个非守护线程没有结束,守护线程就全部工作,只有当最后一个非守护线程结束时,守护线程才会随着JVM一起结束工作
3、守护线程最典型的应用就是垃圾回收器
4、Thread.setDaemon(true)比如在Thread.start()之前设置,否则抛出IllegalThreadStateException异常
5、在守护线程中产生的也是守护线程
5、不要认为所有的应用都可以分配守护线程进行读写或计算逻辑,这可能会造成数据不一致
2、只要当前当前JVM中存在着任何一个非守护线程没有结束,守护线程就全部工作,只有当最后一个非守护线程结束时,守护线程才会随着JVM一起结束工作
3、守护线程最典型的应用就是垃圾回收器
4、Thread.setDaemon(true)比如在Thread.start()之前设置,否则抛出IllegalThreadStateException异常
5、在守护线程中产生的也是守护线程
5、不要认为所有的应用都可以分配守护线程进行读写或计算逻辑,这可能会造成数据不一致
ThreadLocal
可以将ThreadLocal看成是一个操作ThreadLocalMap的工具,实际存储实在Thread中,Thread中有一个成员属性是ThreadLocal.ThreadLocalMap。
锁
死锁条件
互斥条件,同一时间只能有一个线程获取资源
不可剥夺条件,一个线程已经占有的资源,在释放前不会被其他线程占用
请求和保持条件,多线程等待过程中不会释放已占有资源
循环等待条件,多个线程互相等待中不会释放资源
Java主流锁
https://blog.csdn.net/u013733643/article/details/123927732
https://blog.csdn.net/u013733643/article/details/123927732
线程要不要锁住同步资源
锁住
悲观锁(适合写频繁的操作)
对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁
不锁住
乐观锁(适合读频繁的操作)
乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前内存中有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)
乐观锁的主要实现方式是 “CAS” 的技术。CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。
CAS算法涉及到三个操作数:①需要读写的内存值 V。②进行比较的值 A。③要写入的新值 B。当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。
CAS存在的问题:①ABA问题。(JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中)②在更新时如果长时间不成功,会一直自旋,CPU开销很大;③只能保证一个变量的原子操作(Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作)。
乐观锁的主要实现方式是 “CAS” 的技术。CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。
CAS算法涉及到三个操作数:①需要读写的内存值 V。②进行比较的值 A。③要写入的新值 B。当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。
CAS存在的问题:①ABA问题。(JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中)②在更新时如果长时间不成功,会一直自旋,CPU开销很大;③只能保证一个变量的原子操作(Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作)。
锁住同步资源失败,线程要不要阻塞
阻塞
不阻塞
自旋锁
简单来说,线程获取和释放锁时CPU有状态的切换,当同步代码块非常简单时,切换状态的消耗反而大于执行同步代码块的消耗。所以在多核CPU场景下,当两个线程同时竞争一个资源时,没有获取到资源的线程可以自旋,直到获得锁的线程释放锁就直接开始执行,这样就减少了CPU切换状态带来的开销。当然,需要设置线程自旋的次数(一般10次),在超过次数之后将当前线程挂起,避免带来不必要的CPU开销。
适应性自旋锁
多个线程竞争同步资源的流程细节有无区别
不锁住资源,多个线程只有一个能修改资源成功,其他线程会重试
无锁
同一个线程执行同步资源时自动获取锁资源
偏向锁
多个线程竞争同步资源时,没有获取资源的线程自旋等待锁释放 轻量锁锁自旋
多个线程竞争同步资源时,没有获取资源的线程阻塞等待锁释放
重量锁
多个线程竞争锁时要不要排队
排队
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大
先尝试插队,插队失败再排队
非公平锁
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁
一个线程中的多个流程能不能获取同一把锁
能
可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁
不能
不可重入锁
多个线程能不能共享一把锁
能
共享锁
共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享
不能
排他锁
独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。
公平锁和非公平锁
我们发现在ReentrantLock虽然有公平锁和非公平锁两种,但是它们添加的都是独享锁。根据源码所示,当某一个线程调用lock方法获取锁时,如果同步资源没有被其他线程锁住,那么当前线程在使用CAS更新state成功后就会成功抢占该资源。而如果公共资源被占用且不是被当前线程占用,那么就会加锁失败。所以可以确定ReentrantLock无论读操作还是写操作,添加的锁都是都是独享锁。
原子类
JUC并发工具
https://zhuanlan.zhihu.com/p/407956170
CountDownLatch:等待一个或多个线程直到线程中执行的一组操作完成的同步辅助工具
Semaphore:字面意思翻译是信号量。信号量通常用于限制线程数量,而不是限制访问某些共享资源。
CyclicBarrier:允许一组线程全部等待彼此达到共同屏障点的同步辅助工具
Phaser:一个可重复使用的同步屏障,功能类似于CyclicBarrier和CountDownLatch但支持更灵活的使用。支持CountDownLatch和CyclicBarrier的功能,可以做到替换,并且可以动态的添加或减少设定值。
容器
Java集合总的可以分为这几类:List、Set、Queue、Map
非同步容器
ArraryList,hashmap,hashset,linkList 等这些都是非线程安全的容器
同步容器
同步容器可以简单地理解为通过synchronized来实现同步的容器,比如Vector、Hashtable以及SynchronizedList等容器。
Collections.synchronized Stack等;同步容器就是在容器的某些状态值和需要同步的方法上加上synchronized关键字。这样在同一时刻这个容器只能被一个线程使用。但是这也随之带来了问题是会大大降低容器的并发性能
Collections.synchronized Stack等;同步容器就是在容器的某些状态值和需要同步的方法上加上synchronized关键字。这样在同一时刻这个容器只能被一个线程使用。但是这也随之带来了问题是会大大降低容器的并发性能
并发容器
并发容器就是在同步容器上增加了分段锁的技术。即保证在不操作相同数据的状态下,
可以多线程使用容器,在保证了数据安全性的同时,也提高了吞吐量。采用了CAS算
法和部分代码使用synchronized锁保证线程安全
可以多线程使用容器,在保证了数据安全性的同时,也提高了吞吐量。采用了CAS算
法和部分代码使用synchronized锁保证线程安全
ConcurrentHashMap
对应的非并发容器:HashMap
目标:代替Hashtable、synchronizedMap,支持复合操作
原理:JDK6中采用一种更加细粒度的加锁机制Segment“分段锁”,JDK8中采用CAS无锁算法。
对应的非并发容器:HashMap
目标:代替Hashtable、synchronizedMap,支持复合操作
原理:JDK6中采用一种更加细粒度的加锁机制Segment“分段锁”,JDK8中采用CAS无锁算法。
CopyOnWriteArrayList
对应的非并发容器:ArrayList
目标:代替Vector、synchronizedList
原理:利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性,当然写操作的锁是必不可少的了。
对应的非并发容器:ArrayList
目标:代替Vector、synchronizedList
原理:利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性,当然写操作的锁是必不可少的了。
CopyOnWriteArraySet
对应的非并发容器:HashSet
目标:代替synchronizedSet
原理:基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent方法,其遍历当前Object数组,如Object数组中已有了当前元素,则直接返回,如果没有则放入Object数组的尾部,并返回。
对应的非并发容器:HashSet
目标:代替synchronizedSet
原理:基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent方法,其遍历当前Object数组,如Object数组中已有了当前元素,则直接返回,如果没有则放入Object数组的尾部,并返回。
ConcurrentSkipListMap
对应的非并发容器:TreeMap
目标:代替synchronizedSortedMap(TreeMap)
原理:Skip list(跳表)是一种可以代替平衡树的数据结构,默认是按照Key值升序的
对应的非并发容器:TreeMap
目标:代替synchronizedSortedMap(TreeMap)
原理:Skip list(跳表)是一种可以代替平衡树的数据结构,默认是按照Key值升序的
ConcurrentSkipListSet
对应的非并发容器:TreeSet
目标:代替synchronizedSortedSet
原理:内部基于ConcurrentSkipListMap实现
对应的非并发容器:TreeSet
目标:代替synchronizedSortedSet
原理:内部基于ConcurrentSkipListMap实现
ConcurrentLinkedQueue
不会阻塞的队列
对应的非并发容器:Queue
原理:基于链表实现的FIFO队列(LinkedList的并发版本)
不会阻塞的队列
对应的非并发容器:Queue
原理:基于链表实现的FIFO队列(LinkedList的并发版本)
LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue
对应的非并发容器:BlockingQueue
特点:拓展了Queue,增加了可阻塞的插入和获取等操作
原理:通过ReentrantLock实现线程安全,通过Condition实现阻塞和唤醒
实现类:
LinkedBlockingQueue:基于链表实现的可阻塞的FIFO队列
ArrayBlockingQueue:基于数组实现的可阻塞的FIFO队列
PriorityBlockingQueue:按优先级排序的队列
对应的非并发容器:BlockingQueue
特点:拓展了Queue,增加了可阻塞的插入和获取等操作
原理:通过ReentrantLock实现线程安全,通过Condition实现阻塞和唤醒
实现类:
LinkedBlockingQueue:基于链表实现的可阻塞的FIFO队列
ArrayBlockingQueue:基于数组实现的可阻塞的FIFO队列
PriorityBlockingQueue:按优先级排序的队列
数据结构和算法
数据结构
字符串
数组
链表
单向链表
双向链表
堆、栈、队列
二叉树
二叉查找树,左节点小于右节点
红黑树
平衡二叉查找树
平衡多路查找树
B+树
B树
B*树
哈希
图
算法
排序
查找
贪心算发思想
分治
动态规划
回溯
计算机网络
APR协议
它的全称是“Application Protocol Routing”,即应用层协议路由。它是一种基于TCP/IP协议的应用层协议,可以用于实现跨网络的应用程序通信。它可以支持不同类型的网络,并且可以支持多种应用协议,如HTTP、FTP、SMTP等。它的主要功能是实现跨网络的应用程序通信,以及实现跨网络的负载均衡和安全性
IP协议、ICM协议
TCP、UDP协议
https://blog.csdn.net/jcf52/article/details/124011582
DNS/HTTP/HTTPS协议
将主机的域名映射成IP地址的过程称为域名解析。
域名到 IP 地址的解析是由若干个域名服务器程序完成的。域名服务器程序在专设的结点上运行,运行该程序的机器称为域名服务器
互联网采用了层次树状结构的命名方法。任何一个连接在互联网上的主机或路由器,都有一个唯一的层次结构的名字,即域名。域名的结构由标号序列组成,各标号之间用点隔开,... .三级域名.两级域名.顶级域名
DNS为什么适合使用UDP协议而不是TCP协议
基于UDP协议的DNS只需要一次请求和一次应答即可完成域名解析的任务,而基于TCP协议的DNS需要进行三次握手和四次挥手,明显浪费资源。
DNS数据包一般不是那种大的数据包,所以不需要考虑分包。如果收到了数据那就是收到了全部数据,如果丢包就是全部丢包,只需重传即可。
UDP协议传输内容不能超过512字节
域名到 IP 地址的解析是由若干个域名服务器程序完成的。域名服务器程序在专设的结点上运行,运行该程序的机器称为域名服务器
互联网采用了层次树状结构的命名方法。任何一个连接在互联网上的主机或路由器,都有一个唯一的层次结构的名字,即域名。域名的结构由标号序列组成,各标号之间用点隔开,... .三级域名.两级域名.顶级域名
DNS为什么适合使用UDP协议而不是TCP协议
基于UDP协议的DNS只需要一次请求和一次应答即可完成域名解析的任务,而基于TCP协议的DNS需要进行三次握手和四次挥手,明显浪费资源。
DNS数据包一般不是那种大的数据包,所以不需要考虑分包。如果收到了数据那就是收到了全部数据,如果丢包就是全部丢包,只需重传即可。
UDP协议传输内容不能超过512字节
http是一种无状态协议,即服务器不保留与客户交易时的任何状态。也就是说,上一次的请求对这次的请求没有任何影响,服务端也不会对客户端上一次的请求进行任何记录处理
https要使客户端与服务器端的通信安全和效率得到保证,必须使用对称加密算法。但是协商对称加密算法和对称密匙的过程,需要使用非对称加密算法来保证安全。因此来说,https使用的是对称加密和非对称加密算法结合的混合加密算法。
Session
Cookie
SQL/数据库
SQL语句的书写
SQL语句的优化 最左索引使用>或<的时候 无法使用索引会导致全表扫描
对于创建的多列索引,只要查询的条件中用到了最左边的列,索引一般就会被使用
如果列名是索引,使用 is null 条件时候效率较高。但是is not null条件会导致索引失效
使用OR连接的所有条件列必须都有自己的索引(复合索引的第一个列或者单个普通索引),否则查询不会使用索引,效率极低。
对于同一字段,使用OR和IN,EXPLAIN展示的效率都是一样的。
对于同一字段,使用OR和IN,EXPLAIN展示的效率都是一样的。
如果列类型是字符串,那么一定记得在where条件中把字符常量值用引号引起来,否则即便这个列上有索引,MySQL执行效率也是极低的,因为MySQL默认把输入的常量值进行转换以后才进行检索
不建议在查询条件中使用函数
使用不等于(!=或者<>)的时候 无法使用索引会导致全表扫描
MySQL会对GROUP BY之后的字段进行自动排序,这样会产生排序造成的不必要消耗。如果查询包括GROUP BY,但用户想要避免排序结果的消耗,则可以指定ORDER BY NULL禁止排序
无过滤条件(无where和limit)的order by 必然会出现 Using filesort。
过滤条件中的字段和order by 后跟的字段的顺序不一致,必然会出现 Using filesort。
order by后跟的字段排序即有DESC也有ASC,必然会出现Using filesort。
where条件的值确定,且order by后跟了跟了where条件的排序字段(order by 字段去除定值字段后剩余单字段),即使order by后跟的字段和组合索引字段顺序不一致,也不会出现Using filesort。
对查询条件和排序列创建联合索引,排序没有Using filesort,高效。
对查询条件创建索引,但是排序列没有索引, 排序有Using filesort,不用索引。
对排序列创建索引,但是查询条件没有索引, 排序有Using filesort,不用索引。
无过滤条件(无where和limit)的order by 必然会出现 Using filesort。
过滤条件中的字段和order by 后跟的字段的顺序不一致,必然会出现 Using filesort。
order by后跟的字段排序即有DESC也有ASC,必然会出现Using filesort。
where条件的值确定,且order by后跟了跟了where条件的排序字段(order by 字段去除定值字段后剩余单字段),即使order by后跟的字段和组合索引字段顺序不一致,也不会出现Using filesort。
对查询条件和排序列创建联合索引,排序没有Using filesort,高效。
对查询条件创建索引,但是排序列没有索引, 排序有Using filesort,不用索引。
对排序列创建索引,但是查询条件没有索引, 排序有Using filesort,不用索引。
数据库三大范式
https://blog.csdn.net/voilde/article/details/125205303
https://blog.csdn.net/voilde/article/details/125205303
第一范式:数据库的每一列(每个字段)必须是不可拆分的
第二范式(主要针对联合索引):在满足第一范式的前提下,非主键索引不能只满足主键索引的一部分,即查询某一个字段的值,只需要一个主键索引就可以确认,而不是需要主键索引和非主键索引一起确认。
第三范式:在满足第二范式的前提下,非主键索引必须直接依赖主键索引
事务、隔离级别
事务的特征:原子性、持久性、隔离性、一致性
一致性代表了底层数据存储的完整性。它必须由事务系统和应用开发人员共同来保证。
事务系统通过保证事务的原子性,隔离性和持久性来满足这一要求
一致性代表了底层数据存储的完整性。它必须由事务系统和应用开发人员共同来保证。
事务系统通过保证事务的原子性,隔离性和持久性来满足这一要求
原子性,由undolog日志来保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql
持久性,由redolog来保证,mysql修改数据的时候会在redolog中记录一份日志数据,就算数据没有保存成功,只要日志保存成功了,数据仍然不会丢失
隔离性,是由MVCC(多版本并发控制)来保证
https://www.jb51.net/article/210018.htm
一致性,是由系统和开发人员共同保证业务数据的一致性
读未提交(RU)
允许脏读, 一个事务可以读到另一个事务未提交的数据. 不存在间隙锁
读已提交(RC)
不允许脏读, 但允许不可重复读, 一个事务可以读到另一个事务已提交的数据. 不存在间隙锁,
当A事务进行两次查询的时候会读到其他事务提交前后的的不同结果,所以不可重读
当A事务进行两次查询的时候会读到其他事务提交前后的的不同结果,所以不可重读
可重复读(RR)
加入间隙锁, 不允许脏读、不可重复读, 但允许幻读(InnoDB 默认)
可串行化(S)
串行化, 以上都不允许, select 语句后都自动加上 lock in share mode
索引
聚簇索引:MySQL中一般是自增主键索引。
Innodb 聚簇索引叶子节点包含了整行数据
非聚簇索引:即普通的自定义索引, 非聚簇索引叶子节点只有主键 id
索引覆盖
在查询语句时,如果使用的是普通索引,就会有一个回表的操作。但是,当我们查询的字段值如果刚好就是索引时,这时就可以直接返回数据了而不用回表。例如:复合索引idx_age_name,select name from table where age =?
索引下推
https://baijiahao.baidu.com/s?id=1716515482593299829&wfr=spider&for=pc
https://www.zhihu.com/question/433203047
https://www.zhihu.com/question/433203047
锁
按照属性分
共享锁
排他锁
按照粒度分
行级锁(innodb )
表级锁( innodb 、myisam)
页级锁( innodb引擎)
记录锁
间隙锁
临键锁
操作系统
进程、线程
并发、锁
内存管理和调度
I/O原理
设计模式
创建型模式
单例模式
工厂方法
抽象工厂方法
原型模式
建造者模式
结构型模式
适配器模式
装饰器模式
代理模式
外观模式
桥接模式
组合模式
享元模式
行为型模式
策略模式
模板方法模式
观察者模式
迭代子模式
责任链模式
命令模式
备忘录模式
状态模式
访问者模式
中介者模式
解释器模式
研发工具
集成开发环境
Eclipse
Intellij IDEA
VS Code
Linux系统
常用命令
Shell脚本
项目管理/构建工具
maven
gradle
代码管理工具
SVN
Git
0 条评论
下一页