项目复盘
电商购物系统
1.这个项目是做什么的?为什么要做这个项目
该电商购物系统是基于SpringBoot的SSM框架实现。主要功能有:微信支付、用户管理、商品管理、类目管理、购物车、订单管理、收货地址管理。
设计思路:
用户登录商城,根据所需商品类目浏览商品,将满意的商品放入购物车,然后下单并选择收货地址,最后进行支付。
目的:
随着商品销售渠道的多元化,仅靠线下渠道去接触用户已经远远达不到企业的需要,而且传统线下的经营模式人力成本和物力成本较高,资源分散、宣传成本昂贵。网上商城的出现,能帮助整合资源,让商家能够降低成本,让顾客能收获实惠,让企业能够收获忠实的用户和商家。
2.数据库表是如何设计的
实体设计
面试题自测
3、咳咳,重头戏来了,面试官要开始挖你对Redis的掌握和理解了,问你为什么用Redis而不用MySQL或Memcached?为什么不直接在JVM中做缓存?问你如果Redis在服务器挂了,内存中的数据怎么办?Redis持久化机制?Redis缓存穿透?缓存雪崩?怎么解决?Redis如何实现队列、分布式锁?等等
技术选型为Redis的原因?
- Redis是一个使用C语言开发的数据库,与传统数据库不同的是Redis的数据存在内存中,所以读写速度非常快。
- Redis还提供了多种数据类型来支持不同的业务场景。
- Redis 支持数据的持久化,有灾难恢复机制。可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。
为什么不直接在JVM中做缓存?
Java一般用Map做缓存,主要特点是轻量和快速,生命周期随着JVM的销毁而结束。并且在多实例的情况下,每个实例都各自保存一份缓存,缓存不具有一致性。
- Redis可以用很大的内存来做缓存, Map不行, 一般JVM也就分到几个G的内存。
- Redis的缓存支持持久化,Map是内存对象,程序重启数据会丢失
- Redis可以实现分布式缓存,Map只能存在创建它的程序里
- Redis的单点吞吐量能达到10万级,是专业的缓存服务
- Redis有丰富的API
Redis服务器挂了,内存中的数据怎么办?
这要提到Redis的持久化机制,通过RDB和AOF来实现
RDB 持久化
在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写 入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
优点:
1.整个Redis数据库将只包含一个文件dump.rdb,方便持久化
2.性能最大化,fork子进程来完成写操作,让主进程继续处理命令
缺点:
1.数据安全性低,RDB是间隔一段时间进行持久化,会发生数据丢失。
2.由于RDB是通过fork子进程来协助完成数据持久化。因此,当数据集较大时,可能导致服务器停止服务一段时间。
AOF 持久化
以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以 打开文件看到详细的操作记录
优点:
1.数据安全,Redis提供了3种同步策略,即每秒同步、每修改同步和不同步。
2.通过append模式写文件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过Redis-check-aof工具解决数据一致性问题
3.AOF机制的rewrite模式。定期对AOF文件进行重写,以达到压缩的目的。
缺点:
1.AOF文件比RDB文件大,且恢复速度慢。
2.数据集较大的时候,比RDB启动效率低。
3.运行效率没有RDB快
Redis缓存雪崩、缓存穿透和缓存击穿如何解决?
缓存雪崩是指缓存同一时间大面积的失效,所以后面的请求都会落到数据库上,造成数据库短时间内承受大量的请求而崩掉。
解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量缓存失效现象发生。
- 给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果失效则更新数据缓存。
- 缓存预热,底层数据库内的热点数据加载到缓存内。
- 互斥锁
缓存穿透是指缓存和数据库中都没有数据,导致所有请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
- 接口层增加校验,如用户鉴权校验、id做基础校验,id<= 0的直接拦截。
- 从缓存和数据库中取不到数据,可以将key-value 写成 key - nell, 缓存有效时间设置短点。这样可以防止攻击用户反复使用同一个id暴力攻击。
- 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一定不存在的数据会被这个bitmap拦截,从而避免对底层数据存储系统的查询压力。
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案
- 设置热点数据永远不过期。
- 加互斥锁
4、Redis挖完了,就到第二个重头戏了,MQ,这里为什么是强调MQ,而不是说RabbitMQ,还是像上面说的,技术选型,不需要掌握所有MQ,但是你必须懂得所有MQ的特点,然后掌握本项目的MQ,最好去网上找一些博客,去看看MQ的底层运行机制,然后你就会明白为什么MQ的依赖包是AMQP,你就会明白关于消息的一系列问题是怎么解决的。
技术选型为什么是RabbitMQ?
- 基于erlang开发,并发能力强,性能极其好,延时很低,达到微秒级。
- 消息丢失的可能性非常低
- 功能完善
RabbitMQ的运行原理
生产者将消息发送给交换器时,需要一个RoutingKey,当 BindingKey 和 RoutingKey 相匹配时,消息会被路由到对应的队列中。消费者从队列中获取消息并消费,多个消费者可以订阅同一个队列,这时消息会被轮询给多个消费者进行消费。
- Producer:就是投递消息的一方。消息一般包含两个部分:消息体(
payload)和标签(Label)。 - consumer:也就是接收消息的一方。消费者连接到 RabbitMQ 服务器,并订阅到队列上。消费消息时只消费消息体,丢弃标签。
- Broker : 可以看做 RabbitMQ 的服务节点。一般请下一个 Broker 可以看做一个 RabbitMQ 服务器。
- Queue :RabbitMQ 的内部对象,用于存储消息。多个消费者可以订阅同一队列,这时队列中的消息会被平摊(轮询)给多个消费者进行处理。
- Exchange : 生产者将消息发送到交换器,由交换器将消息路由到一个或者多个队列中。当路由不到时,或返回给生产者或直接丢弃
如何保证消息的可靠性?
消息到 MQ 的过程中搞丢,MQ 自己搞丢,MQ 到消费过程中搞丢。
- 生产者到 RabbitMQ:事务机制和 Confirm 机制,注意:事务机制和 Confirm 机制是互斥的,两者不能共存,会导致 RabbitMQ 报错。
- RabbitMQ 自身:持久化、集群、普通模式、镜像模式。
- RabbitMQ 到消费者:basicAck 机制、死信队列、消息补偿机制。
浅谈RabbitMQ——死信队列与延迟队列 - 知乎 (zhihu.com)
死信队列
死信队列就是一个普通的交换机
- 队列长度到达限制,无法加入新的消息
- 消费者拒接消费消息,并且不重回队列。该信息会被清除并进入死信队列
- 原队列存在消息过期设置,消息到达超时时间未被消费
延时队列
延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。经典的应用场景是下单减库存。
5、然后,面试官就会针对项目的业务,问问你对超卖的解决方案,毕竟还是电商项目嘛,然后问问项目难点和你的解决方案,这里就需要你自己去挖掘难点了,难点也是你项目的最大卖点,一定要准备,切记!
- 超卖问题
(1 封私信 / 2 条消息) 电商系统如何防止超卖? - 知乎 (zhihu.com)
(5条消息) 秒杀:超卖问题(图解+秒懂+史上最全)架构师-尼恩的博客-CSDN博客超卖问题
遇到的问题及解决
cookies跨域问题
跨域&Cookie&Session - 黄河大道东 - 博客园 (cnblogs.com)
一篇文章让你搞懂如何通过Nginx来解决跨域问题 - 腾讯云开发者社区-腾讯云 (tencent.com)
跨域问题是浏览器为了防御CSRF攻击(Cross-site request forgery跨站请求伪造)发生,避免恶意攻击而带来的风险而采取的同源策略限制。协议+域名+端口任意不同则为跨域。
在进行用户登录信息保存的功能开发时,尝试使用IP和域名进行访问,发现了浏览器端Cookies保存的SessionID发生了变化。IP || 域名不一样会跨域,因为cookies中保存的sessionID发生了变化
性能优化问题
首先对于分级查询类目的功能实现采用的是通过递归逐条从数据库中查询匹配的数据。
后来在对接口测试的时候发现访问速度过慢,通过查询资料得知
数据库(内网/外网/本机访问 + 磁盘)
耗时: HTTP(请求) > 磁盘 > 内存
所以在需要对一些表数据进行二次处理时,应该提前将所有数据读取出来,不能即用即查。
人事管理系统
该项目是基于 SpringBoot 的 SSM 框架的人事管理系统,目的是加强各个部门的协调和提高工作效率。主要功能:员工资料、人事管理、工资管理、统计管理和系统管理。
数据库设计
数据库有哪些表,有哪些字段?能详细讲讲问什么这样设计吗
hr 表: 存放基本信息和账号信息,比如姓名电话,地址,用户名 ,密码 ,头像 ,昵称
hr_role:存放hr所对应的角色,字段有hr的id 和 role 的id
role:存放所有角色,字段有角色对应的英文名称和中文名称
role_menu表:存放角色所对应可操作功能项,字段角色id和操作项id
menu表:存放所有的可操作项,字段有名称,路径,是否启用等
employee表:存放员工的详细信息,有很多字段,比如:姓名,性别,出生年月,民族,身份证,入职离职日期等等
用户登录与权限管理如何实现
用户登录
本系统采取用户名+密码+验证码的验证方式。
1.验证码:验证码通过后端编写一个验证码工具类生成。当前端访问登录接口时,Controller层则会调用验证码生成工具类生成验证码,并且将验证信息保存在session中,最后通过Java的ImageIO.write()进行输出。
2.由于前端使用了图片验证码,前端数据传输的编码发生改变,之前的getParameter()方法无法从表单中提取数据,需要使用getInputStream()或getReader()来获取提交的数据。于是继承了UsernamePasswordAuthenticationFilter的LoginFilter来自定义用户登录校验
3.在继承WebSecurityConfigurerAdapter的Security配置类中使用
LoginFilter。
权限管理
1.实现FilterInvocationSecurityMetadataSource接口,调用写好的service层方法来根据用户传来的请求地址,分析出需要的角色
2.实现AccessDecisionManager接口,重写decide方法来判断当前用户是否具备合法角色。
3.在security配置类中加入到PostProcessor
Springsecurity认证授权原理
这部分采用的是SpringSecurity框架实现。
SpringSecurity采用AOP,是基于Servlet过滤器实现的安全框架.
核心功能:
- 认证(Authentication ): 用户登录
- 授权(Authorization): 权限鉴别
- 攻击防护
用户登录验证流程:
- 前端发起HTTP请求,携带从前端的表单得到身份信息,用户名和密码被过滤器获取到,封装成
Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。 - 将Authentication对象传给“验证管理器” ProviderManager进行验证ProviderManager在一条链上一次调用
AuthenticationProvider进行验证,一般是通过DaoAuthenticationProvider实现UsernamePasswordAuthenticationToken和UserDetails密码的比对 - 验证成功则返回一个封装了权限信息的Authentication对象(密码通常会被移除)
SecurityContextHolder安全上下文处理器将上面填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到SecurityContext安全上下文容器中- 需要时,可以将Authentication对象从SecurityContextHolder上下文中取出
1.hr表是用户表,存放了用户的基本信息。
2.role是角色表,name字段表示角色的英文名称,按照SpringSecurity的规范,将以ROLE_开始,nameZh字段表示角色的中文名称。
3.menu表是一个资源表,该表涉及到的字段有点多,由于我的前端采用了Vue来做,因此当用户登录成功之后,系统将根据用户的角色动态加载需要的模块,所有模块的信息将保存在menu表中,menu表中的path、component、iconCls、keepAlive、requireAuth等字段都是Vue-Router中需要的字段,也就是说menu中的数据到时候会以json的形式返回给前端,再由vue动态更新router,menu中还有一个字段url,表示一个url pattern,即路径匹配规则,假设有一个路径匹配规则为/admin/**,那么当用户在客户端发起一个/admin/user的请求,将被/admin/**拦截到,系统再去查看这个规则对应的角色是哪些,然后再去查看该用户是否具备相应的角色,进而判断该请求是否合法。
邮件发送
使用RabbitMq + Redis实现消息的幂等性处理
用数据库保证消息不丢失,如果在1分钟数据库的状态还是发送中,就会消息重发,至多重发2次(不过第1次第2次好像都是10秒检查一次成功与否),如果3次都没收到ack,就把数据库状态改为发送失败,以后人工检查。 用redis保证消息去重,如果redis已有消息数据,说明是网络延迟等原因导致的消息重发,只需要返回ack就行。并且只确认当前tag(那个递增的唯一id)为了避免消息丢失。
多人操作的数据不一致问题该怎么解决?
乐观锁
主要是两个步骤:冲突检测和数据更新
- 版本号控制:即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的“version”字段来实现。
- 时间戳机制:通过为数据表增加一个字段类型使用时间戳(timestamp)。
- 条件限制:这个适用于只更新时做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高。
update goods
set quantity = quantity- #{buyQuantity}
where id = #{id}
#条件控制
AND quantity - #{buyQuantity} >= 0
AND status = 1
悲观锁
1.在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locks)。
2.如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
3.如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
4.期间如果有其他对该记录做修改或加排他锁的操作,都会等待解锁或直接抛出异常。
Redis缓存(Redis分布式锁方案)
用redis解决多用户同时编辑同一条数据问题 - 我俩绝配 - 博客园 (cnblogs.com)
Redis如何实现分布式锁?-阿里云开发者社区 (aliyun.com)
用redis的SETNX 命令(加锁): 设置成功,返回 1 , 设置失败,返回 0 。
用redis的DEL命令(解锁):
以目标数据的编号为key的一部分,操作用户ID为value,可以实现提示该数据被xxx操作的功能。
Java基础篇💖
基础篇
异常
集合
ArrayList
HashMap
CurentHashMap
面试题篇
什么是面向对象, 面向对象和面向过程的区别
面向对象是一种编程思想,在现实生活中对事物进行归类抽象出一类事物,他们的共同特征为属性,他们的共同行为为方法,每一个对象都是该类的实例。
面向过程更注重事情的每一个步骤及顺序
区别:
1.封装性不同, 面向过程封装的是功能,面向对象封装的是数据和功能;
2.面向对象具有继承性和多态性。
面向对象的三大特性
1.封装:隐藏内部实现细节,把数据和操作数据的方法进行封装,对数据的访问只能通过已定义的接口。
2.继承:使用已存在的类的定义为基础建立新的类,新的类可以增加新的数据和功能,提供代码的重用和程序的可维护性。
3.多态:分为编译时多态(重载)和运行时多态(重写),通常指的是运行时多态,实现运行时多态需要子类继承父类并重写父类中的方法,二是父类型引用子类型对象,最终的引用调用方法会根据子类对象的不同表现出不同的行为称为多态。
重载和重写的区别
重载:发生在同一个类中,方法名必须相同,参数列表不同,方法返回值和访问修饰符可以不同,发生编译时。
重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。
==和equals
==对比的是栈中的值,基本数据类型是变量值,引用数据类型是堆中内存对象存放的地址。
equals用于比较对象的值是否相等。
hashCode与equals
hashCode()的默认行为是对堆上的对象产生特殊值,如果没有重写hashCode(),则该class的两个对象永远不相等。
- 如果两个对象相等,那么hashCode也相等
- 如果两个对象的hashCode相等,对象不一定相等
static和final关键字
static修饰目标属于类,不属于类的对象。主要用于对象间共享同一个变量
可修饰:修饰内部类、方法、成员变量、代码块
不可修饰:外部类、局部变量
静态内部类和非静态内部类加载与外部类的关系
它们的关系是按需加载,如果你调用了外部类的静态属性、静态方法或者构造器,那么外部类将会被加载而内部类并不会加载。 如果你调用了静态内部类的静态属性、静态方法或者构造器,那么内部类将会被加载而外部类并不会被加载。
外部类的加载不会影响到内部类,非静态内部类的加载依赖于外部类的加载(即非静态内部类实例化是在外部类实例的基础上)
被final修饰说明不可变
可修饰:类、方法、变量(类变量、成员变量和局部变量)
修饰类:表示不可继承
修饰方法:表示不可重写
修饰变量:
- 基本数据类型:初始化后不能更改
- 引用数据类型:初始化后引用的指向不能更改,但是引用指向的值是可变的
String、StringBuffer、StringBuilder
String:final修饰,不可变
StringBuffer和StringBuilder都是在原对象上操作
StringBuffer:线程安全
StringBuilder:线程不安全
性能:StringBuilder > StringBuffer > String
常用方法:
java中String的常用方法 - CrazyAC - 博客园 (cnblogs.com)
浅拷贝、深拷贝、引用拷贝
- 引用拷贝:两个不同的引用指向同一个对象
- 浅拷贝:浅拷贝会在堆上创建一个新的对象,不过,如果原对象的属性是引用类型的话,浅拷贝会直接复制内部对象的引用
- 深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象
接口和抽象类
共同点:
- 都不能被实例化
- 都可以包含抽象方法
- 都可以有默认实现的方法(Java8可以用default关键字在接口中定义默认方法
- 都可以有静态方法(静态方法需要实现,Java8在接口中可以实现静态方法)
区别:
- 接口主要用于对类的行为进行约束,抽象类主要用于代码复用强调从属关系
- 一个类只能继承一个类,但可以实现多个接口
- 接口中的成员变量只能是
public static final,不能被修改且必须由初始值。而抽象类中的成员变量默认为default,可以在子类中重新的定义与复制
List和Set区别
- List:有序,可重复,允许多个null元素对象
- Set:无序,不可重复,最多允许一个null元素对象
ArrayList和LinkedList区别
底层数据结构:ArrayList采用Object数组,LinkedList使用双向链表
操作的时间复杂度:就指定位置插入方法和删除方法而言,ArrayList和LinkedList的时间复杂度都是O(n)的,ArrayList主要是需要移动元素,LinkedList主要是需要遍历查找插入位置;在查询上,ArrayList实现**RandomAccess**接口,标志了其具有随机访问的能力,而LinkedList没有。
空间占用上:ArrayList的空间浪费体现在结尾预留空间,而LinkedList主要体现在节点需要存放前驱和后继
HashMap 和 HashTable的区别
HashMap的方法没被Synchronized修饰,线程不安全,HashTable线程安全。
HashMap允许key 和 value 为null,而 HashTable 不允许。
HashMap底层实现是数组 + 链表
HashMap的扩容机制:
capacity即容量,默认为16;
loadFactory加载因子,默认为0.75;
threshold阈值。阈值等于容量*加载因子。默认为12.当元素数量超过阈值时便会触发扩容,调用扩容的方法resize()。新容量 = 旧容量 * 2。
为了保证执行效率,JDK8开始链表高度到8,数组长度超过64,链表转为红黑树,若数组长度没达到64,HashMap则会进行扩容,使得链表分为高位和低位。
- 计算key的hash值,二次hash然后对数组长度取模,对应到数组下标
- 如果没有产生hash冲突,则直接创建Node存入数组
- 如果产生hash冲突,先进行equal比较,相同则取代该元素,不同,则遍历链表进行尾插,链表长度达到8,并且数组长度达到64则转变为红黑树,长度低于6则将红黑树转回链表。
Java中的异常体系
Java中所有的异常都来自顶级父类Throwable
Throwable下有两个子类Exception和Error
Error是程序无法处理的错误,一旦出现,程序将被迫停止运行
Exception不会导致程序停止,又分为RuntimeException和CheckedException
RuntimeException常常发生在程序运行时,会导致程序当前线程执行失败。CheckedException常常发生在程序编译时,会导致程序编译不通过。
RuntimeException:
NullPointerException(空指针错误)IllegalArgumentException(参数错误比如方法入参类型错误)NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)ArrayIndexOutOfBoundsException(数组越界错误)ClassCastException(类型转换错误)ArithmeticException(算术错误)SecurityException(安全错误比如权限不够)UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)
CheckedException:
- IO 相关的异常
- ClassNotFoundException
- SQLException
注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
IO基础
IO流的本质是数据传输,根据数据传输的特性将流区分为各种类,方便更直观的进行数据操作。
按照数据处理的不同可分为
- 字节流
InputStream和OutputStream
最常用的是FileInputStream和FileOutputStream
- 字符流
Reader和Writer
InputStreamReader和OutputStreamWriter
不过更多时候是用缓冲流将数据加载至缓冲区,这样可以大幅减少 IO 次数,提高读取效率。
字节缓冲流:BufferedInputStream和BufferedOutputStream
字符缓冲流:BufferedReader 和 BufferedWriter
随机访问流(RandomAccessFile):常用于实现大文件的 断点续传。简单来说就是上传文件中途暂停或失败(比如遇到网络问题)之后,不需要重新上传,只需要上传那些未成功上传的文件分片即可。
IO模型
同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O
双亲委托模型
好处:
- 安全性,避免用户自己编写的类动态替换Java的核心类
- 避免类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不 同的 ClassLoader加载就是不同的两个类
GC如何判断对象可以被回收
- 引用计数法:每个对象都有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收
- 可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径为引用链。当一个对象到GC Roots 没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。
GC Roots的对象有:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对像
可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由 虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。
当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回 收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象 的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活” 每个对象只能触发一次finalize()方法
由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘它。
引用类型
- 强引用:当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
- 软引用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。
- 弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
- 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
单例模式、多例模式、工厂模式
(6条消息) 单例模式和多例的区别Armymans的博客-CSDN博客单例和多例的区别
设计模式之工厂模式(factory pattern) - alpha_panda - 博客园 (cnblogs.com)
JUC篇
基础篇
JMM(Java内存模型)
线程的生命周期
锁
十分钟学会18种Java锁(图文讲解) - 掘金 (juejin.cn)
java里的锁总结(synchronized隐式锁、Lock显式锁、volatile、CAS) - Life_Goes_On - 博客园 (cnblogs.com)
线程池
面试题篇
线程的生命周期?线程有几种状态
1.线程通常有五种状态,分别是创建、就绪、运行、阻塞、死亡。
2.阻塞的情况又分为三种:
- 等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能唤醒,wait是Object类的方法
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”。
- 其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程设置为阻塞状态。当sleep状态超时、join等待线程终止或超时、或者I/O处理完毕,线程重新转入就绪状态。sleep是Thread类的方法。
1.新建状态:新创建了一个线程对象
2.就绪状态:线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行池中,变的可运行,等待获取CPU的使用权。
3.运行状态:就绪状态的线程获取了CPU,执行程序代码
4.阻塞状态:阻塞状态是线程因为某种原因放弃CPU,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
5.死亡状态:线程执行完了或者异常原因退出了run方法,该线程结束生命周期。
sleep()、wait()、join()、yield()的区别
- sleep:线程类(Thread)的方法,导致此线程暂停执行指定时间,将执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用 sleep 不会释放对象锁。 sleep() 使当前线程进入阻塞状态,在指定时间内不会执行
- wait:Object 类的方法,对此对象调用 wait 方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出 notify 方法(或 notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
- join:用于在某一个线程的执行过程中调用另一个线程执行,等到被调用的线程执 行结束后,再继续执行当前线程。如:t.join();//主要用于等待 t 线程运行结束,若无此句, main 则会执行完毕,导致结果不可预测
- yield:停止当前线程,让同等优先权的线程或更高优先级的线程有执行的机会。 如果没有的话,那么 yield()方法将不会起作用,并且由可执行状态后马上又被执行。
对线程安全的理解
线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施,导致出现脏数据或者其他不可预见的结果问题。Java中堆是进程和线程共有的空间,所以在Java中我们谈到的线程安全问题基本是堆内存的处理问题。
Thread、Runable 的区别
Thread和Runnable的实质是继承关系,没有可比性。无论使用Runnable还是Thread,都会new Thread,然后执行run方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简 单的执行一个任务,那就实现runnable。
对守护线程的理解
守护线程是为所有非守护线程提供服务。例如GC垃圾回收线程
- 为其他线程提供服务
- 程序结束,守护线程必须立刻的正常关闭
ThreadLocal的原理和使用场景
每一个 Thread 对象均含有一个 ThreadLocalMap 类型的成员变量 threadLocals ,它存储本线程中所有ThreadLocal对象及其对应的值。
ThreadLocalMap 由一个个 Entry 对象构成
Entry 的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该 key就会被垃圾收集器回收。
使用场景:
- 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束;
- 线程间数据隔离
- 进行事务操作,用于存储事务信息
- 数据库连接,session会话管理
ThreadLocal内存泄露原因,如何避免
内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露的危害可以忽略,但内存泄露堆积后果很严重。
ThreadLocalMap存储以ThreadLocal为 key ,Object 对象为 value 的键值对。
ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal正确的使用方法
- 每次使用完ThreadLocal都调用它的remove()方法清除数据
- 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任 何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
并发、并行、串行的区别
串行:在时间上任务顺序执行。
并行:单位时间内多任务同时执行。 -- 时刻
并发:某个时间段内多任务交替执行 -- 时间段
并发的三大特性
原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
可见性:在多线程环境,某个线程对共享变量进行修改,其他线程是能立即知道的,然后在需要读取变量时,会重新从主存中读取,不会从自己的工作空间读取。
有序性:虚拟机在进行代码编译优化的时候,对于那些改变顺序之后不会对最终变量的值造成影响的代码,是有可能将他们进行重排序的。
synchronized关键字满足以上三种特性,但是volatile关键字不满足原子性
volatile关键字
volatile可实现可见性和禁止指令重排序
什么情况下volatile能够保证线程安全
1.运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
t = t + 1;
这种情况就是依赖变量当前值。
2.变量不需要与其他状态变量共同参与不变约束。
if(a && b)
当对a, b变量进行volatile声明后,当前线程在读取b的时候,a被其他线程进行了修改。
为什么用线程池?解释下线程池参数?
1.降低资源消耗:提高线程利用率,降低创建和销毁线程的消耗
2.提高响应速度:当任务来了直接有线程可用
3.提高线程的可管理性:线程池可以同一分配调优监控。
简述下线程池处理流程
并发实战
死锁
生产者和消费者
Java实现生产者和消费者的5种方式 - 掘金 (juejin.cn)
两个线程交替打印奇数和偶数
java并发控制:ReentrantLock Condition使用详解-阿里云开发者社区 (aliyun.com)
经典面试题-两个线程交替打印奇数和偶数 - 腾讯云开发者社区-腾讯云 (tencent.com)
三个线程交替顺序打印ABC
三线程按顺序交替打印ABC的四种方法-魔豆IT (imodou.com.cn)
线程间通信---数字交替打印(循环打印abc) - 掘金 (juejin.cn)
MySQL篇💖
SQL的书写顺序和执行顺序
(7) SELECT
(8) DISTINCT <select_list>
(1) FROM <main_table>
(3) <join_type> JOIN <join_table>
(2) ON <join_condition>
(4) WHERE <where_condition>
(5) GROUP BY <group_by_list>
(6) HAVING <having_condition>
(9) ORDER BY <order_by_condition>
(10) LIMIT <limit_number>
执行顺序按照左侧编号进行
FROM → ON → JOIN → WHERE → GROUP BY → HAVING → SELECT →DISTINCT → ORDER BY→ LIMIT
Select执行流程
1.连接器:建立连接,管理连接、校验用户身份;
##查看当前连接数
show processlist;
##查看最大空闲时间
show variables like 'wait_timeout'
##断开连接, id为连接的编号
kill connection +id;
2.查询缓存:查询语句如果命中查询缓存则直接返回。但是查询缓存很鸡肋,当表有更新操作,这个表的查询缓存就会被清空。所以在MySQL8.0版本将查询缓存删除掉了。
3.解析器(也称分析器):进行词法分析和语法分析,构建语法树判断是否符合MySQL语法,方便后续模块读取表名、字段、语句类型;。
4.预处理器:检查SQL查询语句中的表或字段是否存在;将select 中的 * 符号扩展为表上的所有列。
5.优化器:优化器是在表里面有多个索引时决定使用哪个索引;或者在多表关联时决定表的连接顺序。
##查看使用哪个索引
explain select * from product where id = 1
6.执行器:验证是否有权限进行查询操作,确认后调用引擎提供的接口进行查询操作,并将结果读取出来。
索引及优化
count()及优化
count(1)、 count(*)、 count(主键字段)在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。
所以,如果要执行 count(1)、 count(*)、 count(主键字段) 时,尽量在数据表上建立二级索引,这样优化器会自动采用 key_len 最小的二级索引进行扫描,相比于扫描主键索引效率会高一些。
再来,就是不要使用 count(字段) 来统计记录个数,因为它的效率是最差的,会采用全表扫描的方式来统计。如果你非要统计表中该字段不为 NULL 的记录个数,建议给这个字段建立一个二级索引。
事务及优化
脏读
如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象。
不可重复读
在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。
幻读
在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。
隔离性与隔离级别
SQL标准的事务隔离级别:
读未提交:一个事务还没提交时,它做的变更就能被别的事物看见;
读提交:一个事务提交后,它做的变更才会被其他事物看见;
可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据一致。
个人理解像是对启动时拍下的快照进行操作。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
串行化:对同一行事务进行读写加锁
可以看出,并行性依次降低,安全性依次提高
InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它通过next-key lock 锁(行锁和间隙锁的组合)来锁住记录之间的“间隙”和记录本身,防止其他事务在这个记录之间插入新的记录,这样就避免了幻读现象。
锁及优化
表锁和行锁是满足读读共享、读写互斥、写写互斥的。
全局锁
flush tables with read lock
unlock tables
全局锁主要用做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。缺点就是业务停滞。
InnoDB存储引擎默认的事务隔离级别是可重复读,备份期间可以对数据进行更新,所以不需要使用全局锁。
表级锁
针对非索引字段加锁
表锁
//表级别的共享锁,也就是读锁;
lock tables t_student read;
//表级别的独占锁,也就是写锁;
lock tables t_stuent write;
//释放当前会话的所有表锁
unlock tables
表锁除了会限制别的线程的读写外,也会限制本线程的读写操作。
表锁的颗粒度太大,会影响并发性能,所以InnoDB通常使用行级锁。
元数据锁(MDL)
我们不需要显示的使用 MDL,因为当我们对数据库表进行操作时,会自动给这个表加上 MDL:
- 对一张表进行 CRUD 操作时,加的是 MDL 读锁;
- 对一张表做结构变更操作的时候,加的是 MDL 写锁;
MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。
MDL 是在事务提交后才会释放,这意味着事务执行期间,MDL 是一直持有的。
意向锁
- 在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」;
- 在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」;
意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables ... read)和独占表锁(lock tables ... write)发生冲突。
意向锁的目的是为了快速判断表里是否有记录被加锁。
AUTO-INC 锁
行级锁
行锁实际上是加在索引上而非行上。
所以需要避免Where中使用非索引条件,进行全表扫描,导致全表加上Next-key lock锁。
强制使用指定索引
SELECT *
FROM table_name
FORCE INDEX (index_list)
WHERE condition;
- Record Lock,记录锁,也就是仅仅把一条记录锁上;
- Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身;
- Next-Key Lock:临键锁,Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
//对读取的记录加共享锁
select ... lock in share mode;
//对读取的记录加独占锁
select ... for update;
上面这两条语句必须在一个事务中,因为当事务提交了,锁就会被释放,所以在使用这两条语句的时候,要加上 begin、start transaction 或者 set autocommit = 0。
间隙锁使用的意义:
解决了MySQL RR级别下幻读的问题
Next-key Lock
next-key lock 是前开后闭区间,而间隙锁是前开后开区间。
唯一索引等值查询
- 当查询的记录是存在的,在用「唯一索引进行等值查询」时,next-key lock 会退化成「记录锁」。
- 当查询的记录是不存在的,在用「唯一索引进行等值查询」时,next-key lock 会退化成「间隙锁」。
非唯一索引等值查询
- 当查询的记录存在时,除了会加 next-key lock 外,还额外加间隙锁,也就是会加两把锁。
- 当查询的记录不存在时,只会加 next-key lock,然后会退化为间隙锁,也就是只会加一把锁。
非唯一索引和主键索引的范围查询的加锁规则不同之处在于:
- 唯一索引在满足一些条件的时候,next-key lock 退化为间隙锁和记录锁。
- 非唯一索引范围查询,next-key lock 不会退化为间隙锁和记录锁。
日志
- undo log(回滚日志) :是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC。
- redo log(重做日志) :是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复;
- binlog (归档日志) :是 Server 层生成的日志,主要用于数据备份和主从复制;
undo log
- 实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执 行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。
- 实现 MVCC(多版本并发控制)关键因素之一。MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录
redo log
- 实现事务的持久性,让 MySQL 有 crash-safe 的能力,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失;
- 将写操作从「随机写」变成了「顺序写」,提升 MySQL 写入磁盘的性能。
binlog
binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT 和 SHOW 操作。
主从复制
MySQL 的主从复制依赖于 binlog ,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上。复制的过程就是将 binlog 中的数据从主库传输到从库上。
这个过程一般是异步的,也就是主库上执行事务操作的线程不会等待复制 binlog 的线程同步完成。
MySQL 集群的主从复制过程梳理成 3 个阶段:
- 写入 Binlog:主库写 binlog 日志,提交事务,并更新本地存储数据。
- 同步 Binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。
- 回放 Binlog:回放 binlog,并更新存储引擎中的数据。
具体详细过程如下:
- MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应。
- 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。
- 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。
在完成主从复制之后,你就可以在写数据时只写主库,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响读请求的执行。
所以在实际使用中,一个主库一般跟 2~3 个从库(1 套数据库,1 主 2 从 1 备主),这就是一主多从的 MySQL 集群结构。
模型
主要有三种:
- 同步复制:MySQL 主库提交事务的线程要等待所有从库的复制成功响应,才返回客户端结果。这种方式在实际项目中,基本上没法用,原因有两个:一是性能很差,因为要复制到所有节点才返回响应;二是可用性也很差,主库和所有从库任何一个数据库出问题,都会影响业务。
- 异步复制(默认模型):MySQL 主库提交事务的线程并不会等待 binlog 同步到各从库,就返回客户端结果。这种模式一旦主库宕机,数据就会发生丢失。
- 半同步复制:MySQL 5.7 版本之后增加的一种复制方式,介于两者之间,事务线程不用等待所有的从库复制成功响应,只要一部分复制成功响应回来就行,比如一主二从的集群,只要数据成功复制到任意一个从库上,主库的事务线程就可以返回给客户端。这种半同步复制的方式,兼顾了异步复制和同步复制的优点,即使出现主库宕机,至少还有一个从库有最新的数据,不存在数据丢失的风险。
两段式提交
redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两 个状态保持逻辑上的一致。
1.写入redo log,处于prepare状态;
2.写入binlog;
3.提交事务,redo log 写入磁盘,处于commit状态;
在step2完成前崩溃,重启恢复:没有step3,回滚。没有binlog,无法备份恢复。
在step2完成后崩溃,事务认可,存在binlog,备份恢复。
update执行流程
具体更新一条记录 UPDATE t_user SET name = 'xiaolin' WHERE id = 1; 的流程如下:
-
执行器负责具体执行,会调用存储引擎的接口,通过主键索引树搜索获取 id = 1 这一行记录:
- 如果 id=1 这一行所在的数据页本来就在 buffer pool 中,就直接返回给执行器更新;
- 如果记录不在 buffer pool,将数据页从磁盘读入到 buffer pool,返回记录给执行器。
-
执行器得到聚簇索引记录后,会看一下更新前的记录和更新后的记录是否一样:
- 如果一样的话就不进行后续更新流程;
- 如果不一样的话就把更新前的记录和更新后的记录都当作参数传给 InnoDB 层,让 InnoDB 真正的执行更新记录的操作;
-
开启事务, InnoDB 层更新记录前,首先要记录相应的 undo log,因为这是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面,不过在修改该 Undo 页面前需要先记录对应的 redo log,所以先记录修改 Undo 页面的 redo log ,然后再真正的修改 Undo 页面。
-
InnoDB 层开始更新记录,根据 WAL 技术,先记录修改数据页面的 redo log ,然后再真正的修改数据页面。修改数据页面的过程是修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页,为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。
-
至此,一条记录更新完了。
-
在一条更新语句执行完成后,然后开始记录该语句对应的 binlog,此时记录的 binlog 会被保存到 binlog cache,并没有刷新到硬盘上的 binlog 文件,在事务提交时才会统一将该事务运行过程中的所有 binlog 刷新到硬盘。
-
事务提交(为了方便说明,这里不说组提交的过程,只说两阶段提交):
- prepare 阶段:将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘;
- commit 阶段:将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit(将事务设置为 commit 状态后,刷入到磁盘 redo log 文件);
-
至此,一条更新语句执行完成。
日常优化心得
慢查询的解决思路
- 首先分析SQL语句,看看是否load额外的数据。可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,需要对语句进行重写。
- 分析语句的执行计划,然后或得其使用索引的情况后进行修改,使得语句尽可能的命中索引。
- 如果表中数据量过大,可以进行横向或纵向的分表。
SQL查询去重可以使用Distinct或者Group BY
如果去重的字段有索引,那么 group by 和 distinct 都可以使用索引,此情况它们的性能是相同的;而当去重的字段没有索引时,distinct 的性能就会高于 group by,因为在 MySQL 8.0 之前,group by 有一个隐藏的功能会进行默认的排序,这样就会触发 filesort 从而导致查询性能降低。
// 1. 去重
select distinct prod_id from OrderItems
// 2. 分组
select prod_id from OrderItems group by prod_id
框架原理篇
Spring
谈谈Spring IOC的理解,原理和实现?
总:
控制反转:控制反转是一种设计思想。IOC的思想就是将原本在程序中手动创建对象的控制权,交由Spring框架来管理。
IOC容器:IOC容器是Spring用来实现IOC的载体,实际上IOC容积就是个Map(key, value),Map中存放的是各种对象。在Spring中一般存在三级缓存,singletonObjects存放完整的Bean对象,在整个Bean的生命周期,从创建到使用全部都是由容器来管理。
依赖注入:由IOC容器在运行期间,动态的将某种依赖关系注入到对象中。
意义:将对象之间的相互依赖关系交给IOC容器来管理,并由IOC容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。
Bean的生命周期
- 解析类得到BeanDefinition
- 如果有多个构造方法,则要推断构造方法
- 确定构造方法后,进行实例化得到一个对象
- 对对象中的加了@Autowired注解的属性进行属性填充
- 回调Aware方法, 比如BeanNameAware,BeanFactoryAware
- 调用BeanPostProcessor的初始化前的方法
- 调用初始化方法
- 调用BeanPostProcessor的初始化方法,在这里会进行AOP
- 使用Bean
- Spring容器关闭时调用DisposableBean中的destory()方法
Spring如何解决循环依赖问题?
面试必杀技,讲一讲Spring中的循环依赖 - 程序员DMZ - 博客园 (cnblogs.com)
(2条消息) Spring IOC---AOP代理对象生成的时机三木加两木的博客-CSDN博客aop代理对象在什么时候创建
(2条消息) 关于Spring提前暴露AOP代理对象时的细节扩展lvqinglou的博客-CSDN博客spring提前暴露
三级缓存 提前暴露对象 AOP
总:
循环依赖就是A依赖B, B依赖A
分:
1.先创建A对象,实例化A对象,此时A对象中的b属性为空,需要填充b;
2.从容器中查找对象,发现找不到B对象;
3.实例化B对象,此时B对象中的a属性为空,需要填充属性a;
4.从容器中查找A对象,发现找不到A对象。
| 依赖情况 | 依赖注入方式 | 循环依赖是否被解决 |
|---|---|---|
| AB相互依赖(循环依赖) | 均采用setter方法注入 | 是 |
| AB相互依赖(循环依赖) | 均采用构造器注入 | 否 |
| AB相互依赖(循环依赖) | A中注入B的方式为setter方法,B中注入A的方式为构造器 | 是 |
| AB相互依赖(循环依赖) | B中注入A的方式为setter方法,A中注入B的方式为构造器 | 否 |
用构造器注入会循环核心问题是:构造器发生在对象实例化过程中,是不放放入缓存的,所以每次都去实例化,导致死循环。而可以通过set注入方式,是把对象分成实例化和初始化,实例化后就放入到对应的3级缓存,后面初始化就可用缓存,不必再重新实例化。
Spring通过三级缓存解决循环依赖,其中一级缓存为单例池(singletonObject),二级缓存为早期曝光对象earlySingletonObject, 三级缓存为早期曝光对象工厂(singletonFactories)。当A、B两个类发生循环引用时,在A完成实例化后,就是用实例化后的对象去创建一个对象工厂,并添加到三级缓存中,如果A被AOP代理,那么通过这个工厂获取到的就是A代理后的对象,并且因为普通对象和代理对象不能同时出现在容器中,因此代理对象会覆盖之前的普通对象;如果A没有被AOP代理,那么这个工厂获取到的就是A实例化的对象。当A进行属性注入时,会去创建B,同时B又依赖A,所以创建B的同时又会去调用getBean(a)来获取需要的依赖,此时的getBean(a)会从缓存中获取。第一步,先获取到三级缓存中的工厂;第二步,调用对象工厂的getObject方法来获取对应的对象,得到这个对象后将其注入到B中。紧接着B会走完它的生命周期,包括初始化、后置处理器等。当B创建完后,会将B再注入到A中,此时A再完成它的整个生命周期。至此,循环依赖结束。
为什么要使用三级缓存呢?二级缓存能解决循环依赖吗?
如果要使用二级缓存解决循环依赖,意味着所有Bean在实例化后就要完成AOP代理,这样违背了Spring设计的原则,Spring在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理,而不是在实例化后就立马进行AOP代理。
Spring的AOP的原理
AOP能够将与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理,日志管理、控制权限等)封装成一个切面,然后注入到具体的业务逻辑中,便于较少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性。
Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP 会使用JDK Proxy 去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib生成一个被代理对象的子类作为代理。
| 术语 | 含义 |
|---|---|
| 目标(Target) | 被通知的对象 |
| 代理(Proxy) | 向目标对象应用通知之后创建的代理对象 |
| 连接点(JoinPoint) | 目标对象的所属类中,定义的所有方法均为连接点 |
| 切入点(Pointcut) | 被切面拦截 / 增强的连接点(切入点一定是连接点,连接点不一定是切入点) |
| 通知(Advice) | 增强的逻辑 / 代码,也即拦截到目标对象的连接点之后要做的事情 |
| 切面(Aspect) | 切入点(Pointcut)+通知(Advice) |
| Weaving(织入) | 将通知应用到目标对象,进而生成代理对象的过程动作 |
分:
bean的创建过程中有一个步骤可以对bean进行扩展实现,aop本身就是一个扩展功能,所以在BeanPostProcessor的后置处理方法中来进行实现
1.代理对象的创建过程(advice,切面,切点)
2.通过jdk或者cglib的方式来生成代理对象
3.在执行方法调用的时候,会调用到生成的字节码文件中,直接回找到DynamicAdvisoredInterceptor类中的intercept方法,从此方法开始执行
4.根据之前定义好的通知来生成拦截器
5.从拦截器链中依次获取每一个通知开始进行执行,在执行过程中,为了方便找到下一个通知是哪个,会有一个CglibMethodInvocation的对象,找的时候是从-1的位置依次开始查找并且执行的。
事务
事务是逻辑上的一组操作,要么都执行,要么都不执行。
事务的特性(ACID)
- 原子性(Atomicity): 一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。
- 一致性(Consistency): 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
- 隔离性(Isolation): 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read uncommitted)、提交读(read committed)、可重复读(repeatable read)和串行化(Serializable)。
- 持久性(Durability): 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
事务的隔离级别
DEFAULT
READ_UNCOMMITTED
READ_COMMITTED
REPEATABLE_READ
SERIALIZABLE 可串行化
带你读懂Spring 事务——事务的传播机制 - 知乎 (zhihu.com)
spring事务传播
A方法调用B方法,AB方法都有事务,并且传播特性不同,那么A如果有异常,B怎么办,B如果有异常,A怎么办?
总:事务的传播特性指的是不同方法的嵌套调用过程中,事务该如何进行处理,是同一个事务还是不同的事务,当出现异常的时候会回滚还是提交,两个方法之间相互影响,在日常工作中,使用比较多的是Required,Requireds_new。
传播特性有几种?七种
支持当前事务:
Required:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务;
Supports:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行;
Mandatory:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
不支持当前事务:
Requireds_new:创建一个新的事务,如果当前存在事务,则把当前事务挂起;
Not_Supported:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
Never:以非事务方式运行,如果当前存在事务,则抛出异常。
其他情况:
nested:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于Required。
Spring中用到的设计模式
- 工厂设计模式 : Spring 使用工厂模式通过
BeanFactory、ApplicationContext创建 bean 对象。 - 代理设计模式 : Spring AOP 功能的实现。
- 单例设计模式 : Spring 中的 Bean 默认都是单例的。
- 模板方法模式 : Spring 中
jdbcTemplate、hibernateTemplate等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 - 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
- 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
- 适配器模式 : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配
Controller。
单例模式:bean默认都是单例的
原型模式:指定作用域为prototype
工厂模式:BeanFactory
模板模式:jdbcTemplate、hibernateTemplate
策略模式:XmlBeanDefinitionReader,PropertiesBeanDefinitionReader
观察者模式:listener,event,multicast
适配器模式:Adapter
装饰者模式:BeanWrapper
责任链模式:使用aop的时候会生成一个拦截器
代理模式:动态代理
委托者模式:delegate
Spring MVC
流程说明(重要):
- 客户端(浏览器)发送请求,
DispatcherServlet拦截请求。 DispatcherServlet根据请求信息调用HandlerMapping。HandlerMapping根据 uri 去匹配查找能处理的Handler(也就是我们平常说的Controller控制器) ,并会将请求涉及到的拦截器和Handler一起封装。DispatcherServlet调用HandlerAdapter适配执行Handler。Handler完成对用户请求的处理后,会返回一个ModelAndView对象给DispatcherServlet,ModelAndView顾名思义,包含了数据模型以及相应的视图的信息。Model是返回的数据对象,View是个逻辑上的View。ViewResolver会根据逻辑View查找实际的View。DispaterServlet把返回的Model传给View(视图渲染)。- 把
View返回给请求者(浏览器)
Springboot
自动装配
通过注解或者一些简单的配置就能在 Spring Boot 的帮助下实现某块功能,而不像使用Spring时需要配置大量XML或者注解
Spring Boot 通过@EnableAutoConfiguration开启自动装配,通过 SpringFactoriesLoader 最终加载META-INF/spring.factories中的自动配置类实现自动装配,自动配置类其实就是通过@Conditional按需加载的配置类。
1.首先我们知道SpringBoot的核心注解SpringBootApplication。可以把@SpringBootApplication看做
@EnableAutoConfiguration:启用 SpringBoot 的自动配置机制@Configuration:允许在上下文中注册额外的 bean 或导入其他配置类@ComponentScan: 扫描被@Component(@Service,@Controller)注解的 bean,注解默认会扫描启动类所在的包下所有的类。
2.然后看@EnableAutoConfiguration 只是一个简单注解,自动装配核心功能的实现实际上通过AutoConfigurationImportSelector加载自动装配类。
3.这个类后面的实现细节记不清了,但是大概的实现步骤是:
1.判断自动配置开关是否打开,默认是开启的;
2.判断哪些不需要自动配置:获取EnableAutoConfiguration注解中的 exclude 和 excludeName;
3.获取需要自动配置的所有配置类:读取META-INF/spring.factories;
4.通过@Conditional滤掉不需要加载的配置。
SpringSecurity
Spring Security 采用AOP, 是基于Servlet过滤器实现的的安全框架。
核心功能:
- 认证(Authentication ): 用户登录
- 授权(Authorization): 权限鉴别
- 攻击防护
Mybatis
#{}和${}的区别是什么?
#{}是预编译处理、是占位符,${}是字符串替换、是拼接符。
#{}对应的变量自动加上单引号,而${}不会,所以使用#{}可以有效的防止SQL注入,提高系统安全性。
嵌套查询和嵌套结果
Mybatis 的嵌套查询与嵌套结果的区别 - 东郊 - 博客园 (cnblogs.com)
JVM原理篇
Java内存区域
程序计数器:当前线程所执行字节码的行号指示器,通过改变这个计数器的值来选取下一条需要执行的字节码指令。
虚拟机栈:方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。
- 局部变量表:主要存放了编译期可知的各种数据类型和对象引用。
- 操作数栈:用于存放方法执行过程中产生的中间计算结果。
- 动态链接:为了将符号引用转换为调用方法的直接引用
本地方法栈:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
堆:Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。 Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap) 。
方法区:当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
直接内存(非运行时数据区的一部分): 可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为 避免了在 Java 堆和 Native 堆之间来回复制数据。
对象内存分配
-
指针碰撞 :
- 适用场合 :堆内存规整(即没有内存碎片)的情况下。
- 原理 :用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
- 使用该分配方式的 GC 收集器:Serial, ParNew
-
空闲列表 :
- 适用场合 : 堆内存不规整的情况下。
- 原理 :虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
- 使用该分配方式的 GC 收集器:CMS
对象的访问定位
- 句柄:
- 直接指针
使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
Java垃圾回收
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
Java类加载
系统设计
Step1:问清楚系统具体要求
功能性需求:找出业务的核心功能
非功能性需求:确定约束条件
响应时间:用户发出请求到用户收到系统处理结果所需的时间
业务量:
- QPS:服务器每秒可以执行的查询次数
- TPS:服务器每秒处理的事务数(一个事务可以理解成客户发出请求到服务器收到的过程)
- 吞吐量:系统单位时间内处理的请求数量,一般用QPS和TPS量化
系统活跃度:
- PV(Page View) 访问量,即页面浏览量或点击量
- UV(Unique Visitor)独立访客,即统计1天内访问某站点的用户数
- DAU(Daily active users)日活跃
- MAU(Monthly active users)月活跃
硬件要求:要选择满足业务要求并留有升级余地的硬件设备
精度:一些特殊业务对于精度要求是非常严格的
常见特性:安全性、可靠性、兼容性、易用性、可维护性
Step2:对系统进行抽象
画出系统抽象架构图
Step3:考虑系统目前需要优化的点
- 分布式:是否需要负载均衡、分布式文件系统
- 数据库:是否需要优化索引、读写分离、缓存、分库分表
- 安全:是否存在安全隐患
SQL优化、JVM、DB、Tomcat参数调优 > 硬件性能优化(内存升级、CPU核心数增加、硬盘升级) > 业务逻辑优化/缓存 > d读写分离、集群等 > 分库分表
Redis篇💖
RabbitMQ篇💖
数据结构篇
计算机网络篇(3天)
基础篇
OSI 七层网络模型
- 应用层,负责给应用程序提供统一的接口;
- 表示层,负责把数据转换成兼容另一个系统能识别的格式;
- 会话层,负责建立、管理和终止表示层实体之间的通信会话;
- 传输层,负责端到端的数据传输;
- 网络层,负责数据的路由、转发、分片;
- 数据链路层,负责数据的封帧和差错检测,以及 MAC 寻址;
- 物理层,负责在物理网络中传输数据帧;
TCP/IP 四层网络模型
- 应用层,负责向用户提供一组应用程序,比如 HTTP、DNS、FTP 等;
- 传输层,负责端到端的通信,比如 TCP、UDP 等;
- 网络层,负责网络包的封装、分片、路由、转发,比如 IP、ICMP 等;
- 网络接口层,负责网络包在物理网络中的传输,比如网络包的封帧、 MAC 寻址、差错检测,以及通过网卡传输网络帧等;
Linux 网络协议栈
HTTP 常见的状态码
TCP 三次握手
TCP 四次挥手
试题篇
OSI 的七层模型分别是?各自的功能是什么?
应用层:负责给应用程序提供统一的接口
表示层:负责把数据转换成兼容另一个系统能识别的格式
会话层:负责创建、管理、终止表示层实体间的通信会话
传输层:负责端到端的数据传输
网络层:负责数据的路由、转发、切片
数据链路层:负责数据的封帧和差错检测,以及MAC寻址
物理层:负责数据在物理网络中传输数据帧
为什么需要三次握手?两次不行?
三次握手
刚开始客户端处于closed状态,服务端处于listen状态。
1.第一次握手:客户端给服务端发送一个SYN报文,并指明客户端的初始化序列号ISN(c)。
此时客户端处于SYN_send状态。
2.第二次握手:服务端收到客户端的SYN报文后,会给客户端发送自己的SYN + ACK报文,并且也指定自己的初始化序列号ISN(s),同时会把客户端的ISN+1作为ACK值,表示自己已经收到了客户端的SYN,此时服务端处于SYN_RCVD状态。
3.第三次握手:客户端收到了服务端的SYN+ACK报文后,会发送一个ACK报文,把服务器的ISN+1作为ACK的值,表示已经收到了服务端的SYN+ACK报文,此时客户端处于established状态。
4.服务端收到ACK报文后,也处于established状态。此时,双方成功建立TCP连接。
三次握手的作用:
1、确认双方的接收能力、发送能力是否正常。
2、指定自己的初始化序列号,为后面的可靠传送做准备。
3、三次握手才可以避免资源浪费
ISN动态生成的主要原因有两个方面:
为了防止历史报文被下一个相同四元组的连接接收(主要方面); 为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收;
第三次握手是可以携带数据的,前两次握手是不可以携带数据的
为什么需要四次挥手?三次不行?
1.第一次挥手:客户端发送一个FIN报文,报文中指定一个序列号。此时客户端处于FIN_WAIT1状态。
2.第二次挥手:服务端收到FIN之后,会发送ACK报文,且把客户端的序列号+1作为AKC的值,表示已经收到客户端的FIN报文。此时服务端处于CLOSE_WAIT状态。当客户端收到第二次挥手,也就是收到服务端发送的 ACK 报文后,客户端就会处于 FIN_WAIT2 状态
3.第三次挥手:如果服务端处理完数据也要断开连接了,会向客户端发送一个FIN报文,报文中指定一个序列号。此时服务端处于LAST_ACK状态。
4.第四次挥手:客户端收到FIN之后,一样发送一个ACK报文作为应答,且把服务端的序列号+1作为自己的ACK的值。此时客户端处于TIME_WAIT状态。需要过2MSL(报文最大生存时间)以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态
5、服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
TIME_WAIT状态
TIME_WAIT 持续的时间至少是一个报文的来回时间。
作用
- 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
- 保证「被动关闭连接」的一方,能被正确的关闭;
如果客户端(主动关闭方)最后一次 ACK 报文(第四次挥手)在网络中丢失了,那么按照 TCP 可靠性原则,服务端(被动关闭方)会重发 FIN 报文。
危害
- 第一是内存资源占用;
- 第二是对端口资源的占用,一个 TCP 连接至少消耗「发起连接方」的一个本地端口;
如果服务端要避免过多的 TIME_WAIT 状态的连接,就永远不要主动断开连接,让客户端去断开,由分布在各处的客户端去承受 TIME_WAIT。
TCP与UDP有哪些区别?各自应用场景?
TCP 和 UDP 区别:
1. 连接
- TCP 是面向连接的传输层协议,传输数据前先要建立连接。
- UDP 是不需要连接,即刻传输数据。
2. 服务对象
- TCP 是一对一的两点服务,即一条连接只有两个端点。
- UDP 支持一对一、一对多、多对多的交互通信
3. 可靠性
- TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达。
- UDP 是尽最大努力交付,不保证可靠交付数据。
4. 拥塞控制、流量控制
- TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
- UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
5. 首部开销
- TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是
20个字节,如果使用了「选项」字段则会变长的。 - UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
6. 传输方式
- TCP 是流式传输,没有边界,但保证顺序和可靠。
- UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
7. 分片不同
- TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
- UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。
TCP 和 UDP 应用场景:
由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:
FTP文件传输;- HTTP / HTTPS;
由于 UDP 面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于:
- 包总量较少的通信,如
DNS、SNMP等; - 视频、音频等多媒体通信;
- 广播通信;
基于TCP和UDP的常用协议
HTTP、HTTPS、FTP、TELNET、SMTP(简单邮件传输协议)协议基于可靠的TCP协议。
DNS、DHCP、TFTP、SNMP(简单网络管理协议)、RIP基于不可靠的UDP协议。
HTTP 哪些常用的状态码及使用场景?
101 切换请求协议,从 HTTP 切换到 WebSocket
200 请求成功,有响应体
301 永久重定向:会缓存
302 临时重定向:不会缓存
304 协商缓存命中
403 服务器禁止访问
404 资源未找到
400 请求错误
500 服务器端错误
503 服务器繁忙
HTTPS通信流程
发起请求、验证身份、协商秘钥、加密会话
在浏览器中输入 URL 地址到显示主页的过程?
1.DNS 解析:浏览器查询 DNS,获取域名对应的 IP 地址:具体过程包括浏览器搜索自身的 DNS 缓存、搜索操作系统的 DNS 缓存、读取本地的 Host 文件和向本地 DNS 服务器进行查询等。对于向本地 DNS 服务器进行查询,如果要查询的域名包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析(此解析具有权威性);如果要查询的域名不由本地 DNS 服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个 IP 地址映射,完成域名解析(此解析不具有权威性)。如果本地域名服务器并未缓存该网址映射关系,那么将根据其设置发起递归查询或者迭代查询;
2.TCP 连接:浏览器获得域名对应的 IP 地址以后,浏览器向服务器请求建立链接,发起三次握手;
3.发送 HTTP 请求:TCP 连接建立起来后,浏览器向服务器发送 HTTP 请求;
4.服务器处理请求并返回 HTTP 报文:服务器接收到这个请求,并根据路径参数映射到特定的请求处理器进行处理,并将处理结果及相应的视图返回给浏览器;
5.浏览器解析渲染页面:浏览器解析并渲染视图,若遇到对 js 文件、css 文件及图片等静态资源的引用,则重复上述步骤并向服务器请求这些资源;浏览器根据其请求到的资源、数据渲染页面,最终向用户呈现一个完整的页面。
6.连接结束。
操作系统篇
基础篇
系统调用
内核程序执行在内核态,用户程序执行在用户态。当应用程序使用系统调用时,会产生一个中断。发生中断后, CPU 会中断当前在执行的用户程序,转而跳转到中断处理程序,也就是开始执行内核程序。内核处理完后,主动触发中断,把 CPU 执行权限交回给用户程序,回到用户态继续工作。
- 设备管理。完成设备的请求或释放,以及设备启动等功能。
- 文件管理。完成文件的读、写、创建及删除等功能。
- 进程控制。完成进程的创建、撤销、阻塞及唤醒等功能。
- 进程通信。完成进程之间的消息传递或信号传递等功能。
- 内存管理。完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。
进程管理
- 阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;
- 就绪挂起状态:进程在外存(硬盘),但只要进入内存,即刻立刻运行;
内存管理
虚拟内存
操作系统引入了虚拟内存,进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存,如下图所示:
内存分段
内存分页
段页式
试题篇
简述下并行和并发
- 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生;
- 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件;
并发针对单核 CPU 而言,它指的是 CPU 交替执行不同任务的能力;并行针对多核 CPU 而言,它指的是多个核心同时执行多个任务的能力。
进程与线程
概念
进程:进程是系统进行资源分配和调度的基本单位,是系统并发执行的单位。
线程:是进程的一个实体,线程是CPU调度和分派的基本单位。
线程产生的原因
1.进程在同一时间只能干一件事情;
2.进程在执行的过程中如果阻塞,整个进程会被挂起,即使进程中有些工作不依赖与等待资源仍然不会执行。
所以引入粒度更小的线程,作为并发执行的基本单位,减少程序在并发执行的时间和空间开销,提高并发性能。
区别
1.进程是资源分配的最小单位, 而线程是CPU调度的最小单位;
2.进程的创建或撤销,系统都要为之分配或回收资源,系统开销远大于线程的创建或撤销;
3.不同的进程地址空间相互独立,同一进程内的线程共享同一地址空间;
4.进程间不会相互影响,而线程挂掉可能导致整个进程挂掉。
进程的状态转换
1.就绪 —> 运行:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程,则进程由就绪态转为运行态;
2.运行—>阻塞:当进程请求某个事件且必须等待时,例如进程提出输入/输出请求而变成等待外部设备传输信息的状态,进程申请资源(主存空间或外部设备)得不到满足时变成等待资源状态,进程运行中出现了故障(程序出错或主存储器读写错等)变成等待干预状态等等;,则进程由运行态转为阻塞态。
3.阻塞—>就绪:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;
4.运行—>就绪:处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,或在采用抢先式优先级调度算法的系统中,当有更高优先级的进程要运行而被迫让出处理机时,操作系统会把该进程变为就绪态
进程间的通信方式
- 管道/匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
- 有名管道(Names Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out) 。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
- 信号(Signal) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
- 消息队列(Message Queuing) :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点。
- 信号量(Semaphores) :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
- 共享内存(Shared memory) :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
- 套接字(Sockets) : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
线程间的同步方式
- 互斥量(Mutex) :采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
- 信号量(Semaphore) :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
- 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作
死锁
概念:死锁是指多个进程在运行过程中因争夺资源而造成的一种互相等待现象,若无外力作用,它们都将无法推进下去。
死锁产生的必要条件
1.互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
2.请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
4.环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
解决死锁的基本方法
- 预防 是采用某种策略,限制并发进程对资源的请求,从而使得死锁的必要条件在系统执行的任何时间上都不满足。
- 避免则是系统在分配资源时,根据资源的使用情况提前做出预测,从而避免死锁的发生
- 检测是指系统设有专门的机构,当死锁发生时,该机构能够检测死锁的发生,并精确地确定与死锁有关的进程和资源。
- 解除 是与检测相配套的一种措施,用于将进程从死锁状态下解脱出来