1.死锁产生条件是什么?如何避免?
1.1 什么是死锁
如果一组进程中的每一个进程都在等待仅由该组进程中的其它进程才能引发的事件,那么该组进程就是死锁的。
1.2 死锁出现的场景
(1)多个线程:彼此申请对方资源而导致的死锁。A申请B的资源时,因为资源被占用,A会被挂起等待B释放资源,同时B申请A的资源,因资源被占用B挂起等待A释放资源,而AB都处于挂起状态又无法释放资源,便形成了死锁。
(2)单个线程:A有自己的资源,但还要申请新的资源,而新的资源被占用,则A会挂起等待,同时会保护已占有的资源而不释放,形成死锁。
1.3 死锁产生的原因
1.系统资源的竞争:系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁。
2.进程运行推行顺序不合适:进程在运行过程中,请求和释放资源的顺序不当,会导致死锁。
1.4 产生死锁的四个必要条件
互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
请求与保持:进程已经保持了至少一个资源,但又提出了新的资源请求时,该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
循环等待条件:若干进程间形成首尾相接循环等待资源的关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
1.5 避免死锁的方法
1.5.1 死锁避免的基本思想
系统对进程发出每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配,这是一种保证系统不进入死锁状态的动态策略。
1.5.2 如何避免死锁?
在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。 一般来说互斥条件是无法破坏的,所以在预防死锁时主要从其他三个方面入手 :
(1)破坏请求和保持条件:
在系统中不允许进程在已获得某种资源的情况下,申请其他资源,即要想出一个办法,阻止进程在持有资源的同时申请其它资源。
方法一:在所有进程开始运行之前,必须一次性的申请其在整个运行过程中所需的全部资源,这样,该进程在整个运行期间便不会再提出资源请求,从而破坏了“请求”条件。系统在分配资源时,只要有一种资源不能满足进程的需要,即使其它所需的各资源都空闲也不分配给该进程,而让该进程等待,由于该进程在等待期间未占用任何资源,于是破坏了“保持”条件。
方法二:要求每个进程提出新的资源申请前,释放它所占有的资源。这样,一个进程在需要资源S时,需要先把它先前占有的资源R释放掉,然后才能提出对S的申请,即使它很快又要用到资源R。
两种协议比较:第二种协议优于第一种协议,因为第一种协议会造成资源的严重浪费,使资源利用率大大的降低,也会由于占据大量资源导致其它进程的饥饿问题。
(2)破坏不可抢占条件:允许对资源实行抢夺。
方式一:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
方式二:如果一个进程请求当前被另一个进程占有的资源,则操作系统可以抢占另一个进程,要求它释放资源,只有在任意两个进程的优先级都不相同的条件下,该方法才能预防死锁。
(3)破坏循环等待条件
对系统所有资源进行线性排序并赋予不同的序号,这样我们便可以规定进程在申请资源时必须按照序号递增的顺序进行资源的申请,当以后要申请时需检查要申请的资源的编号大于当前编号时,才能进行申请。
1.5.3 死锁避免和死锁预防的区别:
死锁预防是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现,而死锁避免则不那么严格的限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。死锁避免是在系统运行过程中注意避免死锁的最终发生。
利用银行家算法避免死锁:
所谓银行家算法,是指在分配资源之前先看清楚,资源分配后是否会导致系统死锁。如果会死锁,则不分配,否则就分配。按照银行家算法的思想,当进程请求资源时,系统将按如下原则分配系统资源:
(1) 当一个进程对资源的最大需求量不超过系统中的资源数时可以接纳该进程。
(2) 进程可以分期请求资源,当请求的总数不能超过最大需求量。
(3) 当系统现有的资源不能满足进程尚需资源数时,对进程的请求可以推迟分配,但总能使进程在有限的时间里得到资源。
(4) 当系统现有的资源能满足进程尚需资源数时,必须测试系统现存的资源能否满足该进程尚需的最大资源数,若能满足则按当前的申请量分配资源,否则也要推迟分配。
1.6 死锁的解除
一旦检测出死锁,就应立即采取相应的措施,以解除死锁。死锁的解除主要有两种方法:
(1)抢占资源。从一个或多个进程中抢占足够数量的资源,分配给死锁进程,以解除死锁状态。
(2)终止或者撤销进程。终止或者撤销系统中的一个或多个死锁进程,直到打破循环环路,使系统从死锁状态解脱出来。
2.权限认证:权限树、RBAC模型的概念?
2.1 权限树(难)
2.2 RBAC模型
RBAC模型(Role-Based Access Control:基于角色的访问控制)模型是比较早期提出的权限实现模型,在多用户计算机时期该思想即被提出,其中以美国George Mason大学信息安全技术实验室(LIST)提出的RBAC96模型最具有代表,并得到了普遍的公认。
RBAC认为权限授权的过程可以抽象地概括为:Who是否可以对What进行How的访问操作,并对这个逻辑表达式进行判断是否为True的求解过程,也即是将权限问题转换为Who、What、How的问题,Who、What、How构成了访问权限三元组。
2.2.1 RBAC的组成
在RBAC模型里面,有3个基础组成部分,分别是:用户、角色和权限,它们之间的关系如下图所示
- User(用户):每个用户都有唯一的UID识别,并被授予不同的角色
- Role(角色):不同角色具有不同的权限
- Permission(权限):访问权限
- 用户-角色映射:用户和角色之间的映射关系
- 角色-权限映射:角色和权限之间的映射
例如下图,管理员和普通用户被授予不同的权限,普通用户只能去修改和查看个人信息,而不能创建用户和冻结用户,而管理员由于被授予所有权限,所以可以做所有操作。
2.2.2 RBAC模型分类
基本模型RBAC0
RBAC0是基础,很多产品只需基于RBAC0就可以搭建权限模型了。在这个模型中,我们把权限赋予角色,再把角色赋予用户。用户和角色,角色和权限都是多对多的关系。用户拥有的权限等于他所有的角色持有权限之和。
举个栗子: 譬如我们做一款企业管理产品,如果按传统权限模型,给每一个用户赋予权限则会非常麻烦,并且做不到批量修改用户权限。这时候,可以抽象出几个角色,譬如销售经理、财务经理、市场经理等,然后把权限分配给这些角色,再把角色赋予用户。这样无论是分配权限还是以后的修改权限,只需要修改用户和角色的关系,或角色和权限的关系即可,更加灵活方便。此外,如果一个用户有多个角色,譬如王先生既负责销售部也负责市场部,那么可以给王先生赋予两个角色,即销售经理、市场经理,这样他就拥有这两个角色的所有权限。
角色分层模型RBAC1
RBAC1建立在RBAC0基础之上,在角色中引入了继承的概念。简单理解就是,给角色可以分成几个等级,每个等级权限不同,从而实现更细粒度的权限管理。
举个栗子: 基于之前RBAC0的例子,我们又发现一个公司的销售经理可能是分几个等级的,譬如除了销售经理,还有销售副经理,而销售副经理只有销售经理的部分权限。这时候,我们就可以采用RBAC1的分级模型,把销售经理这个角色分成多个等级,给销售副经理赋予较低的等级即可。
角色限制模型RBAC2
RBAC2同样建立在RBAC0基础之上,仅是对用户、角色和权限三者之间增加了一些限制。这些限制可以分成两类,即静态职责分离SSD(Static Separation of Duty)和动态职责分离DSD(Dynamic Separation of Duty)。具体限制如下图:
举个栗子: 还是基于之前RBAC0的例子,我们又发现有些角色之间是需要互斥的,譬如给一个用户分配了销售经理的角色,就不能给他再赋予财务经理的角色了,否则他即可以录入合同又能自己审核合同;再譬如,有些公司对角色的升级十分看重,一个销售员要想升级到销售经理,必须先升级到销售主管,这时候就要采用先决条件限制了。
统一模型RBAC3
RBAC3是RBAC1和RBAC2的合集,所以RBAC3既有角色分层,也包括可以增加各种限制。
3.自动装配原理
概念:通过注解或者一些简单的配置就能在 Spring Boot 的帮助下实现某块功能。
springboot如何实现:
大概可以把 @SpringBootApplication
看作是 @Configuration
、@EnableAutoConfiguration
、@ComponentScan
注解的集合。这三个注解的作用分别是:
@EnableAutoConfiguration
:启用 SpringBoot 的自动配置机制@Configuration
:允许在上下文中注册额外的 bean 或导入其他配置类@ComponentScan
:扫描被@Component
(@Service
,@Controller
)注解的 bean,注解默认会扫描启动类所在的包下所有的类
其中,@EnableAutoConfiguration 是实现自动装配的核心注解。
EnableAutoConfiguration
只是一个简单地注解,自动装配核心功能的实现实际是通过 AutoConfigurationImportSelector
类。
AutoConfigurationImportSelector
类的继承体系如下:
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
}
public interface DeferredImportSelector extends ImportSelector {
}
public interface ImportSelector {
String[] selectImports(AnnotationMetadata var1);
}
可以看出, AutoConfigurationImportSelector
类实现了 ImportSelector
接口,也就实现了这个接口中的 selectImports
方法,该方法主要用于获取所有符合条件的类的全限定类名,这些类需要被加载到 IoC 容器中。
private static final String[] NO_IMPORTS = new String[0];
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// <1>.判断自动装配开关是否打开
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
//<2>.获取所有需要装配的bean
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
其中,getAutoConfigurationEntry()
方法,这个方法主要负责加载自动配置类的。
private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();
AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
//<1>.
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
//<2>.
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
//<3>.
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
//<4>.
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.filter(configurations, autoConfigurationMetadata);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
第 1 步:
判断自动装配开关是否打开。默认spring.boot.enableautoconfiguration=true
,可在 application.properties
或 application.yml
中设置。
第 2 步:
用于获取EnableAutoConfiguration
注解中的 exclude
和 excludeName
。
第 3 步
获取需要自动装配的所有配置类,读取META-INF/spring.factories。
不光是这个依赖下的META-INF/spring.factories
被读取到,所有 Spring Boot Starter 下的META-INF/spring.factories
都会被读取到。
第 4 步:筛选
@Configuration
// 检查相关的类:RabbitTemplate 和 Channel是否存在
// 存在才会加载
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Import(RabbitAnnotationDrivenConfiguration.class)
public class RabbitAutoConfiguration {
}
4.如何避免递归栈溢出
stack overflow异常是程序中常常会碰到的,原因是调用线程栈不够用。windows默认栈大小是1M,使用栈空间超过了1M就会报出stack overflow异常。
4.1 产生原因:
1、死循环
出现了死循环,例如:递归函数没有出口,不管栈空间多大必定溢出。
long func(int n)
{
return n*func(n-1);
}这是个没有出口的递归函数, 必然引起栈溢出。纠正:
long func(int n)
{
if(n==1) return 1;
return n*func(n-1);
}
2、栈确实不够用
存到栈上的主要内容是:局部变量和函数调用的函数环境,包括函数参数等。
局部变量,例如:char buff[1024*1024],buff为1M,如果你的栈默认也是1M大小,这就会发生栈溢出,因为其他东西会占掉少量栈空间,局部变量能用的空间肯定小于1M,程序在执行到main函数之前,就会跳出stack overflow异常。
3、函数调用层数太深
这种情况一般发生在递归调用中。/在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当程序执行进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,就会导致栈溢出。
函数的参数是通过stack栈来传递的,在调用中会占用线程的栈资源。递归调用在到达最后的结束点后,函数才能依次退栈清栈,如果递归调用层数过多,就可能导致占用的栈资源超过线程的最大值,从而导致栈溢出,程序异常退出。
不得不使用递归的场景:遍历一个目录下的所有文件,包括其子目录的文件。
4.2 如何避免递归栈溢出?
当栈不够使用时,一种办法是修改程序:
主要还是要注意递归调用引起的栈溢出,多数情况可以通过算法优化来解决:
1、控制递归深度。例如,使用动态规划来代替递归算法等。
2、修改栈的大小。
4.2.1 尾递归优化
尾递归是指,在函数返回的时候,调用函数本身,并且return语句不能包含表达式。如果递归调用,都出现在函数的末尾,这个递归函数就是尾递归的函数。
尾递归函数的特点是在回归过程中,不用做操作,这个特性很重要,因为大多数现代编译器会利用这一特点,自动生成优化的代码。有些语言极力提倡尾递归,因为它们的编译器会对代码进行优化,不会因为递归次数的增加,给函数栈带来巨大的开销。尾递归优化,使无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
4.2.2 循环替代递归
所有的递归都可以改写成循环的方式。 斐波那契数列: 1,1,2,3,5,......
求斐波那契数列的第N项的值。
尾递归和循环的执行效率都非常高。但是尾递归的递归层数大到一定程度会出现段错误。尾递归的函数栈开销比普通递归要小的多,执行效率大很多。 但是尾递归仍然有函数栈开销。
正因为尾递归具有函数栈开销,其调用次数比循环小很多。
实现一个功能,能不用递归就别用递归,能用尾递归就用尾递归。
5.线程进程区别
进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
线程:是进程的一个执行单元,是进程内科调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。
进程和线程的区别:
地址空间:线程共享本进程的地址空间,而进程之间是独立的地址空间。
资源:线程共享本进程的资源如内存、I/O、cpu等,不利于资源的管理和保护,而进程之间的资源是独立的,能很好的进行资源管理和保护。
健壮性:多进程要比多线程健壮,一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。
执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口,执行开销大。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,执行开销小。
可并发性:两者均可并发执行。
切换时:进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
其他:线程是处理器调度的基本单位,但是进程不是。
何时使用多进程,何时使用多线程?
对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。
为什么会有线程?
每个进程都有自己的地址空间,即进程空间,在网络或多用户换机下,一个服务器通常需要接收大量不确定数量用户的并发请求,为每一个请求都创建一个进程显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进。
6.redis是单线程还是多线程?
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的。
但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)的:
- Redis 在 2.6 版本,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;
- Redis 在 4.0 版本之后,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。例如执行 unlink key / flushdb async / flushall async 等命令,会把这些删除操作交给后台线程来执行,好处是不会导致 Redis 主线程卡顿。因此,当我们要删除一个大 key 的时候,不要使用 del 命令删除,因为 del 是在主线程处理的,这样会导致 Redis 主线程卡顿,因此我们应该使用 unlink 命令来异步删除大key。
之所以 Redis 为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理,是因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 Redis 主线程就很容易发生阻塞,这样就无法处理后续的请求了。
后台线程相当于一个消费者,生产者把耗时任务丢到任务队列中,消费者(BIO)不停轮询这个队列,拿出任务就去执行对应的方法即可。
关闭文件、AOF 刷盘、释放内存这三个任务都有各自的任务队列:
- BIO_CLOSE_FILE,关闭文件任务队列:当队列有任务后,后台线程会调用 close(fd) ,将文件关闭;
- BIO_AOF_FSYNC,AOF刷盘任务队列:当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到队列中。当发现队列有任务后,后台线程会调用 fsync(fd),将 AOF 文件刷盘,
- BIO_LAZY_FREE,lazy free 任务队列:当队列有任务后,后台线程会 free(obj) 释放对象 / free(dict) 删除数据库所有对象 / free(skiplist) 释放跳表对象;
Redis 6.0 之前为什么使用单线程?
CPU 并不是制约 Redis 性能表现的瓶颈所在,更多情况下是受到内存大小和网络I/O的限制,所以 Redis 核心网络模型使用单线程并没有什么问题,如果你想要使用服务的多核CPU,可以在一台服务器上启动多个节点或者采用分片集群的方式。
使用了单线程后,可维护性高,多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。
Redis 6.0 之后为什么引入了多线程?
虽然 Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,但是在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。
所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理,而不是 Redis 有多线程同时执行命令。
Redis 官方表示,Redis 6.0 版本引入的多线程 I/O 特性对性能提升至少是一倍以上。
7.Linux 如何查看文件,在不知道文件目录的情况下?
在 Linux 环境下,忘记某个文件在哪个目录,可以使用 find 命令通过该文件的名字来进行查找来查找该文件的位置。如果记得该文件大致的位置,那么就可以缩短查找的时间。比如,我们如果记得 lang.sh在 /etc 目录下(很多文件都在 /etc 目录下),那么就可以使用如下命令进行查找:
find /etc/ -name lang.sh
如果不记得文件所在的大致位置,可以直接在根目录下面查找,但速度会慢一点。
find / -name lang.sh