Lock - 对锁的一些面试题的总结
看到一个问题:请谈谈你对乐观锁、悲观锁、自旋锁、分段所、读写锁、排它锁、共享锁等等锁的理解,他们有什么区别?这么大致一看,发现没什么思路,觉得对锁的了解还是不够透彻。这边来总结一下,但不会很细致。
乐观锁 - 悲观锁
乐观锁和悲观锁是相对而言的,他们的区别如下表格:
锁 | 概述 | 使用场景 | 样例 |
---|---|---|---|
悲观锁 | 悲观锁对数据被外界修改持保守态度(悲观),因此在整个数据处理过程中,将数据出于锁定状态,而别的任务出于被阻塞的状态; | 写多读少,保证写操作时的数据安全 | 1、JVM中的synchronized和Lock;2、分布式环境基于数据库行锁、页锁、表锁、共享锁(读锁)、排它锁(写锁);3、基于zookeeper、Redis 的分布式锁 |
乐观锁 | 乐观锁认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的重提与否进行检测,如果发现冲突了,程序自动去重试(实现通常用“版本号”) | 读多写少,提高系统吞吐 | 1、JDK并发包中的原子类;2、数据库乐观锁、缓存乐观锁 |
自旋锁
自旋锁是互斥锁的一种实现。在自旋锁中,当资源被枷锁后,其他线程想要获取资源,此时该线程不会被阻塞睡眠而是陷入循环等待状态(CPU不能做其它事情),循环检查资源持有者是否已经释放了资源(为什么叫自旋,如下图),这样做的好处是减少了线程从睡眠到唤醒的资源消耗,但会一直占用CPU的资源。适用于资源的锁被持有的时间短,而又不希望在线程的唤醒上花费太多资源的情况。
分段锁
分段锁(SegmentLock)就是简单的将锁细粒度化,将一个锁分成两段或者多段,线程根据自己操作的段来加锁解锁。这样做可以避免线程之间互相无意义的等待,减少线程的等待时间。常见的应用有ConcurrentHashMap
,它内部实现了Segment<K,V>
继承了ReentrantLock
,分成了16段。
读写锁
读写锁(ReadWriteLock),顾名思义就是将读锁和写锁分离。读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。
在读写锁保持期间也是抢占失效的。
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
排它锁 - 共享锁
排它锁又叫互斥锁、独占锁、写锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。如ReentrantLock
。
共享锁又称读锁,就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。比如说ReadWriteLock
公平锁
公平锁就是遵循了先到先得的原则,多个线程按照申请锁的顺序来获取锁。Java 中的ReentrantLock
中可以通过构造函数构建公平锁,实现原理貌似是链表而不是队列。
可重入锁
可重入锁的意思就是,加入方法 A 获得锁并加锁之后调用了方法 B,而方法 B 也需要锁,这样会导致死锁,可重入锁则会让调用方法 B 的时候自动获得锁(Java 中是通过 lockedBy
字段判断加锁的线程是不是同一个)