一、死锁
多线程为了防止竞争共享资源⽽导致数据错乱,都会在操作共享资源之前加上互斥锁,只有成功获得到锁的线程,才能操作共享资源,获取不到锁的线程就只能等待,直到锁被释放。
当两个线程为了保护两个不同的共享资源⽽使⽤了两个互斥锁,那应⽤不当的时候,可能会造成两个线程都在等待对⽅释放锁,这种情况就是发⽣了死锁。
说白了就是我有你需要的东西,你也有我需要的东西,但是我们2个谁都不肯放手,就导致了死锁
死锁只有同时满⾜以下四个条件才会发⽣:
互斥条件; 持有并等待条件; 不可剥夺条件; 环路等待条件;
互斥条件
互斥条件是指多个线程不能同时使⽤同⼀个资源。
如果线程 A 已经持有的资源,不能再同时被线程 B 持有,如果线程 B 请求获取线程 A 已经占⽤的资源,那线程 B 只能等待,直到线程 A 释放了资源。
持有并等待条件
持有并等待条件是指,当线程 A 已经持有了资源 1,⼜想申请资源 2,⽽资源 2 已经被线程C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放⾃⼰已经持有的资源 1
不可剥夺条件
不可剥夺条件是指,当线程已经持有了资源 ,在⾃⼰使⽤完之前不能被其他线程获取,线程B 如果也想使⽤此资源,则只能在线程 A 使⽤完并释放后才能获取。
环路等待条件
环路等待条件指都是,在死锁发⽣的时候,两个线程获取资源的顺序构成了环形链。
二、避免死锁问题的发⽣
避免死锁问题就只需要四个条件破环其中⼀个条件就可以了
常⻅的并且可⾏的就是使⽤资源有序分配法,来破坏环路等待条件。
什么是资源有序分配法呢?
线程 A 和 线程 B 获取资源的顺序要⼀样,当线程 A 是先尝试获取资源 A,然后尝试获取资源B 的时候,线程 B 同样也是先尝试获取资源A,然后尝试获取资源 B。也就是说,线程 A 和线程 B 总是以相同的顺序申请⾃⼰想要的资源,即破坏了环路等待条件。
三、 互斥锁和⾃旋锁
最底层的两种就是「互斥锁和⾃旋锁」,有很多⾼级的锁都是基于它们实现的
互斥锁加锁失败后,线程会释放 CPU ,给其他线程; ⾃旋锁加锁失败后,线程会忙等待,直到它拿到锁;
对于互斥锁加锁失败⽽阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执⾏。
所以,互斥锁加锁失败时,会从⽤户态陷⼊到内核态,让内核帮我们切换线程,虽然简化了使⽤锁的难度,但是存在⼀定的性能开销成本。
那这个开销成本是什么呢?会有两次线程上下⽂切换的成本:
当线程加锁失败时,内核会把线程的状态从「运⾏」状态设置为「睡眠」状态,然后把CPU 切换给其他线程运⾏;接着,当锁被释放时,之前「睡眠」状态的线程会变为「就绪」状态,然后内核会在合适的时间,把 CPU 切换给该线程运⾏。
如果你能确定被锁住的代码执⾏时间很短,就不应该⽤互斥锁,⽽应该选⽤⾃旋锁,否则使⽤互斥锁。
⾃旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap),在「⽤户态」完成加锁和解锁操作,不会主动产⽣线程上下⽂切换,所以相⽐互斥锁来说,会快⼀些,开销也⼩⼀些。
⾃旋锁与互斥锁使⽤层⾯⽐较相似,但实现层⾯上完全不同:当加锁失败时,互斥锁⽤「线程切换」来应对,⾃旋锁则⽤「忙等待」来应对。