一、基础锁类型¶
-
互斥锁(Mutex)
用于确保同一时刻仅有一个线程访问共享资源。
• 特点:简单易用,但可能导致线程阻塞和饥饿问题。
• 实现示例:C++的std::mutex
、Java的synchronized
关键字、Python的threading.Lock
。 -
读写锁(Read-Write Lock)
允许多个线程同时读取资源,但写入时独占访问。
• 适用场景:读操作远多于写操作(如缓存系统)。
• 实现示例:Java的ReentrantReadWriteLock
、C++的std::shared_mutex
。 -
自旋锁(Spinlock)
线程在获取锁失败时循环检查锁状态,而非阻塞。
• 适用场景:锁持有时间极短且避免线程切换的开销(如高频短任务)。
• 注意:长时间自旋可能导致CPU资源浪费。 -
递归锁(Recursive Lock)
允许同一线程多次获取同一把锁,避免递归函数中的死锁。
• 实现示例:C++的std::recursive_mutex
、Java的ReentrantLock
。 -
信号量(Semaphore)
通过计数器控制资源访问数量,支持多线程并发访问。
• 适用场景:连接池、限流等。 -
条件变量(Condition Variable)
用于线程间协作,结合互斥锁实现特定条件的等待与通知(如生产者-消费者模型)。
二、锁的分类标准¶
-
悲观锁 vs 乐观锁
• 悲观锁:假定并发冲突高,直接加锁(如互斥锁、读写锁)。
• 乐观锁:假定冲突低,通过版本号或CAS(Compare-And-Swap)机制检测冲突(如数据库的MVCC)。 -
公平锁 vs 非公平锁
• 公平锁:按请求顺序分配锁(如ReentrantLock(true)
)。
• 非公平锁:允许插队获取锁(默认策略,性能更高)。 -
可重入锁 vs 不可重入锁
• 可重入锁:同一线程可多次获取锁(如synchronized
和ReentrantLock
)。
• 不可重入锁:仅允许单次获取(如早期的Mutex
)。
三、其他高级锁机制¶
- StampedLock(优化读写锁)
C++和Java中提供,支持乐观读锁,减少锁竞争。 - CountDownLatch/CyclicBarrier
用于线程同步(如等待多个任务完成)。 - Exchanger
允许两个线程在特定点交换数据(如管道通信)。
四、选择锁的考量因素¶
- 锁粒度:粗粒度锁降低并发性,细粒度锁增加复杂度。
- 持有时间:尽量减少锁的持有时间以降低竞争。
- 性能与资源消耗:自旋锁适合短任务,互斥锁适合长任务。
- 错误处理:需处理锁获取失败(如超时、中断)。
五、常见问题与优化¶
• 死锁:避免方法包括按顺序获取锁、设置超时(如 tryLock
)。
• 活锁:通过随机退避或优先级调度解决。
• 锁优化:锁降级(写锁转读锁)、锁拆分(减少竞争范围)。
临界区¶
一、临界区的核心特性¶
-
互斥访问
同一时刻仅允许一个线程进入临界区,其他线程必须等待当前线程退出后才能访问。例如,若两个线程同时修改同一全局变量,未加保护的临界区可能导致数据损坏。 -
关联临界资源
临界资源指被共享且需互斥使用的资源(如打印机、共享变量等),临界区则是操作这些资源的代码段。例如,对共享数组的读写操作必须通过临界区保护。 -
有限时间停留
进入临界区的线程需在有限时间内完成操作并退出,否则会导致其他线程无限等待,引发性能问题或资源饥饿。
二、临界区的管理原则¶
为确保安全性和效率,临界区需满足以下要求: 1. 互斥性:同一时刻仅一个线程可进入相关临界区。 2. 有限等待:线程等待进入临界区的时间必须有上限。 3. 非阻塞性:无法进入临界区的线程应释放CPU资源,避免“忙等”(如通过挂起或让出CPU)。 4. 独立性:不相关的临界区(操作不同资源)可并行执行。
三、临界区的实现机制¶
-
互斥锁(Mutex)
通过锁定和解锁操作实现互斥。例如,C++的std::mutex
或 Linux的pthread_mutex_lock()
,线程进入临界区前加锁,退出时解锁。 -
信号量(Semaphore)
使用计数器控制并发访问数量。二元信号量(初始值为1)可等效为互斥锁。例如,数据库连接池可通过信号量限制同时访问的线程数。 -
条件变量(Condition Variable)
结合互斥锁实现线程间协作。例如,生产者-消费者模型中,消费者线程在缓冲区空时等待条件变量触发。 -
原子操作与无锁编程
通过硬件支持的原子指令(如CAS)直接操作共享资源,避免锁的开销。适用于高频短任务,但实现复杂度较高。
四、临界区的应用注意事项¶
-
最小化临界区范围
仅保护必须互斥的代码段,减少锁持有时间。例如,避免在临界区内执行I/O操作或用户输入等待。 -
避免死锁与活锁
按固定顺序获取锁,设置超时机制(如tryLock
),或使用死锁检测算法。 -
性能权衡
锁机制可能引入上下文切换或CPU自旋开销。例如,自旋锁适合短临界区,而互斥锁适合长任务。
五、示例场景¶
• 线程安全计数器:通过互斥锁保护全局计数器变量的自增操作,确保多线程下的结果正确(如代码示例)。 • 共享缓冲区同步:使用条件变量协调生产者和消费者线程,防止缓冲区溢出或读空。
总结¶
临界区是并发编程中保护共享资源的核心机制,其设计需平衡安全性、性能和复杂度。开发者需根据具体场景选择同步原语(如互斥锁、信号量等),并遵循最小化保护范围、避免阻塞等最佳实践。