这篇博客根据上面两个面试中出现的问题,以本人个人理解进行总结:
下面四个必要条件不详细介绍,如果不理解需要温习一下mutex互斥量的作用和lock,unlock加锁解锁的底层原理;
(1)互斥条件
:进程要求对所分配的资源进行排它性控制,即所分配的资源在一段时间内某资源仅为一进程所占用。
(2)请求与保持条件
:当进程因请求资源而阻塞时,对已获得的资源保持不放。(请求新资源的时候,保持已获得的旧资源)
(3)不剥夺条件
:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
(4)循环等待条件
:在发生死锁时,必然存在一个进程–资源的环形链。
破坏4个必要条件中的任一个:
互斥这个规则,这个为了临界区的安全,肯定不能破坏呀;那就破坏后3个;
(1)资源一次性分配
:一次性分配所有资源,用完一次性全部释放,这样就不会再有别的进程请求时的冲突的现象了:
(2)可剥夺资源
:即当某进程获得了部分资源,但得不到其它资源,则释放(剥夺)已占有的资源(破坏不可剥夺条件)
(3)资源有序分配法
:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反,就不会出现环路了(破坏环路等待条件);
我们在开发中常用的锁主要有互斥锁,递归锁,自旋锁,读写锁、乐观锁和悲观锁这五种:
当线程加锁失败时,内核会把线程的状态从运行状态设置为睡眠状态,然后把CPU切换给其他进程运行;
当需要的锁被其他线程释放时,之前睡眠状态的进程会变为就绪状态,然后内核会在合适的时候,把CPU切换给该线程运行。
普通锁看似使用简单,但是由于内核帮我们切换进程的内核态和用户态,存在一定的开销(进行了两次线程上下文切换)
自旋锁的工作原理是在一段时间内反复尝试获取被占用的锁:
当加锁失败时,互斥锁用线程切换来应对,自旋锁则用忙等待(反复尝试获取锁)来应对。
如果能确定被锁住的代码执行时间很短,而且多核处理器,就应该使用自旋锁,减少了普通mutex线程上下文切换的开销;
如果被锁住的代码执行时间长,而且单核处理器,还是用普通mutex好点,因为这时候递归锁反复长时间轮询检测,无疑是浪费了cpu资源的举措。
mutex和自旋锁是锁的两种最基本处理方式,更高级的锁都会选择其中一个方式来实现;
比如读写锁既可以选择基于互斥锁实现,也可以选择基于自旋锁实现。
递归锁只是互斥锁的一个特例,同样只能有一个线程访问该对象;
但递归锁允许同一个线程在 未释放其拥有的锁时 反复对 该锁 进行加锁操作(普通的锁就死锁了)
显然递归锁的常见场景就是对某一个包含加锁操作的递归类型函数进行调用; 某线程反复递归这个函数的时候,使用递归锁就不会像普通mutex那样第二次遇到加锁操作就直接进入睡眠状态挂起了;
线程间读临界区资源的时候,不阻塞,只有在有写的时候才起到mutex的作用,线程间互相阻塞;
这是一种提高效率的锁,比如mysql中的读写锁,读的时候因为不会修改临界区的资源,不会产生线程安全问题,因此读不阻塞效率更高;
写的时候存在线程安全问题,那就阻塞了;
悲观锁
前面提到的互斥锁、自旋锁、读写锁,都属于悲观锁
悲观锁认为多线程同时对共享资源进行修改的事件发生概率比较高,很容易出现冲突问题,所以访问共享资源前,必须先上锁。(做法有点谨慎,悲观,因此得名)
乐观锁(无锁编程)
它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突:
乐观锁全程没有加锁,所以它也叫无锁编程
悲观锁直接进行加锁操作,进行加锁的操作,适用于冲突事件发生概率高的场景(这个场景用悲观锁得不停地)
乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本其实也非常高,所以只有在冲突事件发生的概率非常低,且加锁成本非常高的场景(比如cpu执行效率非常快)时,才考虑使用乐观锁。
还有很多锁,比如mysql在RR隔离级别下处理幻读问题使用了row行锁+GAP间隙锁等,都是一些高级的适用于某种场景的锁,所以锁的可拓展性还是很大的;