众所周知,加锁/解锁都是有时间、系统资源上的开销。那在编程的时候就要考虑效率问题。那我们就要在保持线程安全的前提下,尽可能地提高效率,因此锁策略就应运而生了。下面,我将以Java中的synchronized来介绍六个不同角度上的用锁策略。
一、悲观锁和乐观锁
1、什么是悲观锁策略?
加锁的时候,预测当前代码的锁冲突概率大,因此对代码的线程安全是比较悲观的。那么操作前就要对可能要发生锁冲突的地方对资源加锁,操作完后才能释放锁。这个过程工作量往往比较大,因此加锁开销就会更大。
2、什么是乐观锁策略?
加锁的时候,预测当前代码的锁冲突概率小,因此对代码的线程安全是比较乐观的。可能要发生的锁冲突的地方就比较少。因此这个过程工作量往往比较小,因此加锁开销就会更小。
因此,在加锁的时候,我们可以根据预测当前代码发生锁冲突的概率大还是小来选择合适的锁策略。
synchronized支持自适应,能自动地统计当前代码发生锁冲突的概率大小。当冲突概率低时,按照乐观锁的方式来执行(速度更快);当冲突概率高时,按照悲观锁的方式来执行(做的事情更多)。因此,它既是乐观锁也是悲观锁。
二、重量级锁和轻量级锁
什么是重量级锁和轻量级锁?
重量级锁和轻量级锁,顾名思义那就是看加锁的过程中,做的事情的多少。如果做的事情多,那就是重量级锁,否则就是轻量级锁。
一般来说,由于悲观锁策略是处理锁冲突概率高的场景,因此悲观锁的工作量比较大,往往是重量级锁;而乐观锁策略是解决锁冲突概率比较低的的场景,因此工作量比较少,往往是轻量级锁。因此,这两组概念可能会有所重叠,可能会混着用。
但是是否有既预测到当前代码出现锁冲突概率高,但实际上做的事情比较少的情况,这个就我目前就不得而知了。 因此,目前还是建议区分一下这两组概念,但是别人混着用时,你也得懂这个概念。
同理,synchronized既可以是重量级锁,也可以是轻量级锁(自适应)。
三、自旋锁和挂起锁
1、什么是自旋锁?
自旋锁是在一个死循环里面,不断地尝试获取锁。若满足获取锁的条件便可以直接获取锁,否则一直判定(一直在空转,消耗着cpu资源、忙等,但是可以第一时间拿到锁),是轻量级锁的一种典型方式。
2、什么是挂起锁?
挂起锁是借助系统中的线程调度机制来实现的。尝试加锁时,若发现锁被占用了(出现锁冲突),就会让当前这个尝试加锁的线程被挂起,不参与调度。直到这个锁被释放,然后系统才唤醒该线程,去尝试重新获取锁(线程挂起时不消耗cpu,但该线程啥时候被唤醒,是不可控的,可能要经历很长的时间,拿到锁的时间可能要很久),是重量级锁的一种典型实现方式。
不难发现,synchronized的轻量级锁部分是基于自旋锁实现(基于CAS机制),重量级锁部分是基于挂起等待锁实现(通过系统用内核,调用系统API)
四、可重入锁和不可重入锁
什么是可重入锁和不可重入锁?
一个线程,针对一把锁,连续加锁多次是否会死锁。如果不会,那就是可重入锁;否则就是不可重入锁。
synchronized就是一个可重入锁,连续加锁两次也不会死锁,而c++的std::mutex就是一个不可重入锁,它连续加两次锁,会发生死锁。
五、公平锁和非公平锁
什么是公平锁和非公平锁?
以是否按照严格的先后顺序来获取锁的标准来判别。如果照严格的等待的先后顺序来获取锁,为公平锁;否则为不公平锁。
synchronized是一个非公平锁。多个线程尝试获取这把锁时,是按照概率均等的方式来获取的(线程是抢占式执行,系统调度的顺序是随机的)
六、互斥锁和读写锁
synchronized是一个普通的互斥锁,只有加锁和解锁。而读写锁,则是一个更加特殊的锁,有加读锁、加写锁和解锁。
注意:
数据库中的事务:
1)给读操作加锁:读的时候不能进行其它操作。
2)给写的操作加锁:写的时候不能进行其它操作。
降低并发能力
而Java的读写锁是这样子设定的:
读锁与读锁之间,不产生互斥
写锁与写锁之间,产生互斥
读锁与写锁之间,产生互斥
降低锁冲突的概率,提高并发能力
总而言之,synchronized既是悲观锁也是乐观锁;既是轻量级锁,也是重量级锁。轻量级锁是自旋锁实现的,重量级锁是挂起锁实现的。它是可重入锁,不是读写锁,属于非公平锁