编程中的锁可以根据维度不同,而对锁进行分类,以下一一介绍。
乐观锁/悲观锁
这一对锁名字比较形象。
悲观锁
先说悲观锁,把锁比作人的话。这位小伙子遇事会比较“悲观”。它会先把资源给锁住再进行操作,这样其他线程请求该资源的时候就会被阻塞,直到悲观锁把资源释放为止。
数据库的锁基本都是悲观锁,比如行锁、表锁等
乐观锁
乐观锁的操作方式是:线程遇到资源后不会马上锁住而是先进行自己的逻辑操作,但是在写入的时候会进行判断(判断数据有无被其他线程修改)。
乐观锁一般有两种实现方式:
- 版本号
版本号机制一般在要修改的数据上增加一个version
字段。操作之前先读出来,操作完之后再version + 1
塞回去。
写入version + 1
之前会校验当前数据的version
值与操作之前读出的值是否一致。如果不一致则说明数据被修改过,那就再执行上述步奏,直到修改成功。
- CAS(Compare And Swap)
CAS是一中无锁算法。可以在没有变量被阻塞的情况下实现变量的同步。
一般CAS操作有三个元素: 需要读写的内存值(v)、进行比较的值(a)、拟写入的新值(b)
仅当a与v相同时,才会将v修改为b,否则啥都不做。很多编程语言都会提供方法来完成CAS操作,这些方法的底层基本都是直接调用cpu的CAS原子操作。
乐观锁这里可能会存在一个ABA问题
,假设一个线程读取时原比较字段为A,想更新为C,最后写入时发现该字段仍为A。但是我们就能说明这其中该字段没有被更改过吗(更为B)?相当于乐观锁只关注一个数据的起始和结束状态,对中间状态是不关心的。这种问题的解决方法不是本文重点,各位可以去网上查询。
两种锁使用场景
悲观锁适合写多读少的场景,但是只能有一个线程才能获取到锁,其他线程会阻塞,所以性能比较低。
乐观锁适合读多写少的场景,因为这种场景很少发生冲突,可以省去锁的开销。但是如果冲突很多,那么线程可能会一直轮询,浪费了cpu时间片。
公平锁/非公平锁
根据线程获取锁的抢占机制,锁可以分为公平锁与非公平锁。
公平锁,多个线程按照时间先后顺序来获取锁,意味着先请求锁的线程会先拿到锁,后面的线程只能阻塞等待。
非公平锁,线程之间并不会按照时间先后顺序来获取锁,可能后来的线程也会获取到锁。
如果没有什么特别的要求,建议使用非公平锁。因为公平锁会导致cpu经常切换线程,带来线程开销。
独占锁/共享锁
根据锁可以被单个还是多个线程持有,锁可以分为独占锁与共享锁
独占锁:任意一个时间点只能由一个线程持有锁。
共享锁:资源可以被多个线程所持有。比如我们常见的读写锁中的读锁就是共享锁。
可重入锁/非可重入锁
当一个线程想要获取其他线程占据的锁,该线程会被阻塞。如果该线程获取自己占用的锁呢?
如果不被阻塞,则说明该锁为可重入锁
。如果被阻塞则说明为不可重入锁
。
可重入锁原理
可重入锁内部会有一个线程标识,代表该锁当前被哪个线程所占用。除了此标识外还有一个计数器。
线程获取到锁时,计数器+1
。释放锁时,计数器-1
。类似pv操作
此时如果其他线程尝试获取锁时,会检测线程标识,发现不是对应线程则会挂起阻塞。当计数器变为0时,线程标识重置为null
其他锁
自旋锁
当一个线程获取不到锁时,会一直旋转,此类锁为自旋锁。我们上文介绍的乐观锁其实也是自旋锁。
还有一些其他锁,如分段锁、偏向锁等。这里先不介绍了,后续慢慢补上