死锁描述
- 死锁是指两个以上的线程永远阻塞的情况,这种情况产生至少需要两个以上的线程和两个以上的资源。
- 系统发生死锁现象不仅浪费大量的系统资源,还会导致整个系统崩溃,带来灾难性后果。
- 死锁满足的条件:
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不可剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
死锁解决方案
- 死锁是由四个必要条件导致的,所以一般来说,只要破坏这四个必要条件中的一个条件,死锁情况就应该不会发生。
- 打破死锁的四个条件:
- 如果想要打破互斥条件,我们需要允许进程同时访问某些资源,这种方法受制于实际场景,不太容易实现条件。
- 打破不可抢占条件,这样需要允许进程强行从占有者那里夺取某些资源,或者简单一点理解,占有资源的进程不能再申请占有其他资源,必须释放手上的资源之后才能发起申请,这个其实也很难找到适用场景。
- 进程在运行前申请得到所有的资源,否则该进程不能进入准备执行状态。这个方法看似有点用处,但是它的缺点是可能导致资源利用率和进程并发性降低。
- 避免出现资源申请环路,即对资源事先分类编号,按号分配。这种方式可以有效提高资源的利用率和系统吞吐量,但是增加了系统开销,增大了进程对资源的占用时间。
- 以下为消除死锁的几种方式:
- 最简单、最常用的方法就是进行系统的重新启动,不过这种方法代价很大,它意味着在这之前所有的进程已经完成的计算工作都将付之东流,包括参与死锁的那些进程,以及未参与死锁的进程。
- 撤消进程,剥夺资源。终止参与死锁的进程,收回它们占有的资源,从而解除死锁,这时又分两种情况:一次性撤消参与死锁的全部进程,剥夺全部资源;或者逐步撤消参与死锁的进程,逐步收回死锁进程占有的资源。一般来说,选择逐步撤消的进程时要按照一定的原则进行,目的是撤消那些代价最小的进程,比如按进程的优先级确定进程的代价;考虑进程运行时的代价和与此进程相关的外部作业的代价等因素。
- 进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处,并由此点处继续执行,以求再次执行时不再发生死锁。虽然这是个较理想的办法,但是操作起来系统开销极大,要有堆栈这样的机构来记录进程的每一步变化,以便今后的回退,有时这是无法做到的。
避免死锁的算法
线性资源分配法
- 这种算法资源必须按照某种规则给系统中的所有资源统一编号(例如打印机为1、磁带机为2、磁盘为3、等等),申请时必须申请序列号高的资源。系统要求申请进程:
- 对它所必须使用的而且属于同一类的所有资源,必须一次申请完;
- 在申请不同类资源时,必须按各类设备的编号依次申请。例如:进程P1,使用资源的顺序是R1,R2,进程P2,使用资源的顺序是R2,R1,若采用动态分配有可能形成环路条件,造成死锁。
- 采用有序资源分配法:R1的编号为1,R2的编号为2;
- P1:申请次序应是:R1,R2
- P2:申请次序应是:R1,R2
- 这样就破坏了环路条件,避免了死锁的发生
系统安全状态法
- 即在系统分配资源之前,就应该检测此次资源分配的安全性,如果此次分配资源会导致进程进入不安全状态,那就等待,如果不会,则分配资源。
银行家算法
- 我们可以把操作系统看作是银行家,操作系统管理的资源相当于银行家管理的资金,进程向操作系统请求分配资源相当于用户向银行家贷款。
- 四个条件:
- 分批向银行贷款时,申请的总额不能超过一开始申请的额度。
- 申请贷款时不能超过银行现有资金数目
- 当银行资金不能满足顾客贷款需求时,可以推迟支付,但是肯定会让顾客在需求时间内得到贷款
- 顾客拿到贷款后必须在规定时间内归还。
- 操作系统按照银行家制定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。当进程在执行中继续申请资源时,先测试该进程本次申请的资源数是否超过了该资源所剩余的总量。若超过则拒绝分配资源,若能满足则按当前的申请量分配资源,否则也要推迟分配。
数据库中的锁
- 乐观锁和悲观锁都是为了解决并发控制问题, 乐观锁可以认为是一种在最后提交的时候检测冲突的手段,而悲观锁则是一种避免冲突的手段。
- 乐观锁: 是应用系统层面和数据的业务逻辑层次上的(实际上并没有加锁,只不过大家一直这样叫而已),利用程序处理并发, 它假定当某一个用户去读取某一个数据的时候,其他的用户不会来访问修改这个数据,但是在最后进行事务的提交的时候会进行版本的检查,以判断在该用户的操作过程中,没有其他用户修改了这个数据。开销比较小 。
- 乐观锁的优势:
- 如果数据库记录始终处于悲观锁加锁状态,可以想见,如果面对几百上千个并发,那么要不断的加锁减锁,而且用户等待的时间会非常的长,乐观锁机制避免了长事务中的数据库加锁解锁开销,大大提升了大并发量下的系统整体性能表现,所以如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以建议就要选择乐观锁定的方法, 而如果并发量不大,完全可以使用悲观锁定的方法。乐观锁也适合于读比较多的场景。
- 乐观锁的劣势:
- 但是乐观锁也存在着问题,只能在提交数据时才发现业务事务将要失败,如果系统的冲突非常的多,而且一旦冲突就要因为重新计算提交而造成较大的代价的话,乐观锁也会带来很大的问题,在某些情况下,发现失败太迟的代价会非常的大。而且乐观锁也无法解决脏读的问题
- 悲观锁: 完全依赖于数据库锁的机制实现的,在数据库中可以使用Repeatable Read的隔离级别(可重复读)来实现悲观锁,它完全满足悲观锁的要求(加锁)。
- 它认为当某一用户读取某一数据的时候,其他用户也会对该数据进行访问,所以在读取的时候就对数据进行加锁, 在该用户读取数据的期间,其他任何用户都不能来修改该数据,但是其他用户是可以读取该数据的, 只有当自己读取完毕才释放锁。
- 悲观锁的优势:
- 能避免冲突的发生,
- 悲观锁的劣势 :
- 开销较大,而且加锁时间较长,对于并发的访问性支持不好。
- 事务的并发问题有哪几种?
- 答:
- 丢失更新:一个事务的更新覆盖了另一个事务的更新;
- 脏读:一个事务读取了另一个事务未提交的数据;
- 不可重复读:不可重复读的重点是修改,同样条件下两次读取结果不同,也就是说,被读取的数据可以被其它事务修改;
- 幻读:幻读的重点在于新增或者删除,同样条件下两次读出来的记录数不一样。
- 事务的隔离级别有哪几种?
- 答:隔离级别决定了一个session中的事务可能对另一个session中的事务的影响。ANSI标准定义了4个隔离级别,MySQL的InnoDB都支持,分别是:
- 读未提交(READ UNCOMMITTED):最低级别的隔离,通常又称为dirty read,它允许一个事务读取另一个事务还没 commit 的数据,这样可能会提高性能,但是会导致脏读问题;
- 读已提交(READ COMMITTED):在一个事务中只允许对其它事务已经 commit 的记录可见,该隔离级别不能避免不可重复读问题;
- 可重复读(REPEATABLE READ):在一个事务开始后,其他事务对数据库的 update 在本事务中不可见,直到本事务 commit 或 rollback。但是,其他事务的 insert/delete 操作对该事务是可见的,也就是说,该隔离级别并不能避幻读问题。在一个事务中重复 select 的结果一样,除非本事务中 update 数据库。
序列化(SERIALIZABLE):最高级别的隔离,意思是说这个事务执行的时候不允许别的事务并发执行.完全串行化的读,只要存在读就禁止写,但可以同时读,消除了幻读。虽然最安全最省心,但是效率太低,一般不会用。
- 共享锁和排它锁是具体的锁,是数据库机制上的锁
- 共享锁(读锁):在同一个时间段内,多个用户可以读取同一个资源,读取的过程中数据不会发生任何变化。读锁之间是相互不阻塞的, 多个用户可以同时读,但是不能允许有人修改, 任何事务都不允许获得数据上的排它锁,直到数据上释放掉所有的共享锁。
- 排它锁(写锁):在任何时候只能有一个用户写入资源,当进行写锁时会阻塞其他的读锁或者写锁操作,只能由这一个用户来写,其他用户既不能读也不能写。
- 加锁会有粒度问题,从粒度上从大到小可以划分为
- 表锁:开销较小,一旦有用户访问这个表就会加锁,其他用户就不能对这个表操作了,应用程序的访问请求遇到锁等待的可能性比较高。
- 页锁:是MySQL中比较独特的一种锁定级别,锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。
- 行锁:开销较大,能具体的锁定到表中的某一行数据,但是能更好的支持并发处理, 会发生死锁。举个例子,一个用户表user,有主键id和用户生日birthday当你使用update ... where id=?这样的语句数据库明确知道会影响哪一行,它就会使用行锁,当你使用update ... where birthday=?这样的的语句的时候因为事先不知道会影响哪些行就可能会使用表锁。