Author : Cyan_RA9
Source : 【卡码笔记】网站
Question : 介绍一下几种典型的锁?
【简要回答】
- 互斥锁(Mutex Lock)
- 用于保护临界区,确保同一时间只有一个线程可以访问共享资源。通常与条件变量配合使用,实现线程间的条件同步。
- 读写锁(Read-Write Lock)
- 允许多个读线程同时访问共享资源,但写线程必须独占资源。
- 自旋锁(Spin Lock)
- 线程在获取锁时,如果锁已被占用,会一直循环检查锁的状态(忙等待),直到锁可用。
【详细回答】
- 互斥锁(Mutex Lock)
- 功能:互斥锁用于保护临界区,确保同一时间只有一个线程可以访问共享资源。
- 实现方式:当一个线程获取锁后,其他线程必须等待锁释放后才能获取。
- 使用场景:适用于需要独占访问共享资源的场景,如修改全局变量、访问共享数据结构等。
- 与条件变量的配合使用:条件变量通常与互斥锁配合使用,用于实现线程间的条件同步。例如,在生产者-消费者模型中,生产者线程在条件不满足时等待,消费者线程在条件满足时通知生产者线程。
- 读写锁(Read-Write Lock)
- 功能:允许多个读线程同时访问共享资源,但写线程必须独占资源。
- 实现方式:分为共享锁(读锁)和独占锁(写锁)。读锁可以被多个线程同时持有,写锁只能被一个线程持有。
- 使用场景:适用于读多写少的场景,如缓存系统、数据库等。
- 自旋锁(Spin Lock)
- 功能:线程在获取锁时,如果锁已被占用,会一直循环检查锁的状态(忙等待),直到锁可用。
- 实现方式:通过忙等待(Busy-Waiting)实现,避免线程切换的开销。
- 使用场景:适用于锁占用时间极短的场景,如内核中的临界区保护。
【知识图解】
-
自旋锁(Spinlock)的工作示意图,如下所示:
-
自旋与非自旋的流程图,如下图所示:
【知识拓展】
信号量、锁和条件变量的关系?
- 信号量:
- 信号量是一种通用的同步机制,通过计数器来控制对资源的访问,信号量机制可以实现各种同步机制,包括锁。
- 互斥锁是信号量的一个特例,通常是一个二进制信号量(只能取0或1)。
- 信号量可以用来实现各种同步问题,如生产者-消费者问题、读者-写者问题等。
- 锁:
- 锁是一个更高级别的概念,通常用于保护临界区,确保同一时间只有一个线程访问共享资源。
- 互斥锁是锁的一种,通常与信号量相关联,但它本身是锁的一个具体实现。
- 读写锁和自旋锁也是锁的类型,但它们并不属于信号量的特例。
- 读写锁允许多个读操作同时进行,但写操作是排他的。
- 自旋锁是在获取锁失败时循环等待,而不是进入阻塞状态。
- 条件变量:
- 条件变量通常与互斥锁一起使用,用于线程之间的通信。
- 它允许线程在某个条件不满足时等待,直到其他线程发出通知。
- 条件变量本身不是信号量,但可以与信号量结合使用来实现更复杂的同步逻辑。
为什么使用锁和条件变量而不是直接使用信号量?
- 抽象层次:
- 锁和条件变量提供了更高层次的抽象,使代码更易读、易写和易维护。
- 信号量更底层,需要开发者手动管理计数器,容易出错。
- 功能专门化:
- 锁专门用于保护临界区,条件变量专门用于线程间通信,它们组合使用可以更清晰地表达同步逻辑。
- 信号量虽然功能强大,但可能需要更多的代码来实现相同的功能,并且容易出现死锁或资源争用的问题。
读写锁和自旋锁是否属于信号量的特例?
- 虽然“互斥锁”属于信号量的特例,但读写锁和自旋锁不属于信号量的特例;它们是锁的不同类型,用于不同的场景:
- 读写锁允许并发读取,但 exclusive 写入。
- 自旋锁适用于锁持有时间非常短的场景,避免线程切换的开销。
你上面说的这三种锁能不能举出一些具体的案例呢?
- 互斥锁(Mutex):
- synchronized 关键字:这是 JVM 层面 提供的互斥锁。它不仅是互斥的,还是可重入的。在竞争不激烈时,它会经过偏向锁、轻量级锁的优化,最终升级为重量级锁(真正的互斥)。
- java.util.concurrent.locks.ReentrantLock:这是 JDK 层面 提供的互斥锁。它比 synchronized 更灵活,不但支持公平锁/非公平锁选择、而且支持可中断的锁获取以及超时获取。
- 读写锁 (Read-Write Lock) :
- java.util.concurrent.locks.ReentrantReadWriteLock:这是Java中最经典的读写锁实现。它包含一个 ReadLock(共享锁)和一个 WriteLock(独占锁)。
- 自旋锁 (Spinlock) :
- Java 没有在标准库中直接提供一个名为 SpinLock 的公共类,因为自旋锁如果使用不当会严重消耗 CPU。但自旋锁的思想在 Java 中随处可见,比如在 synchronized 的实现中,当一个线程尝试获取重量级锁失败时,JVM 不会立即挂起线程,而是先让它自旋一小段时间(自适应自旋),如果这段时间内锁释放了,就避免了昂贵的内核态切换。。