Skip to content

并发中的资源锁

对自旋锁、乐观锁、悲观锁、读写锁、互斥锁等概念的分析及使用场景。

自旋锁

概念

自旋锁(Spin Lock)是一种基于忙等待的锁机制。其实现思路是在获取锁时循环检查锁的状态是否可用:如果锁被占用,就循环等待,直到锁被释放。自旋锁是非阻塞锁,因为它不会像互斥锁那样阻塞线程,而是循环等待直至获取到锁。 自旋锁主要用于保护临界区,因为它能在多核 CPU 上实现高效的线程同步。尤其是当临界区的竞争并不激烈时,自旋锁可以减少线程上下文切换的开销,从而提高程序性能。 自旋锁的实现非常简单,通常用一个整型变量来表示锁的状态:当锁被占用时变量值为 1,当锁可用时变量值为 0。在获取锁的过程中,线程会不断循环检查锁的状态,直到发现锁可用,然后将其状态置为 1 并返回成功。当释放锁时,线程把锁的状态重置为 0,从而允许其他线程获取锁。 需要注意的是,虽然自旋锁能减少线程上下文切换的开销,但当临界区竞争激烈时,自旋锁的效率会变差,因为它会导致线程忙等待,浪费 CPU 资源。

当资源被占用时,自旋锁会一直忙等待,不断循环检查资源是否可用,直到获取资源为止。在多核 CPU 上,可以利用硬件提供的 CAS(Compare And Swap,比较并交换)等原子操作指令实现自旋锁,从而避免锁竞争和线程上下文切换的开销。在单核 CPU 上,自旋锁的效率可能较低,因为它会一直占用 CPU 资源,导致其他线程无法执行。因此,在实际应用中,需要根据具体情况选择合适的锁机制。

因此,在实际应用中,需要根据具体情况选择合适的锁机制。

使用场景

自旋锁是一种基于忙等待的锁机制。它在等待共享资源被释放的同时,持续检查共享资源是否被占用:如果共享资源已被占用,则等待其释放;如果未被占用,则加锁并访问共享资源。自旋锁适用于以下场景:

  1. 共享资源占用时间短:当共享资源被占用的时间很短、等待时间也不长时,使用自旋锁可以避免线程进入睡眠状态和线程上下文切换的开销,从而提高程序性能。
  2. 访问共享资源的线程数量少:自旋锁适用于访问共享资源的线程数量较少的情形,因为当访问共享资源的线程较多时,自旋锁的忙等待会消耗大量 CPU 资源,导致程序性能下降。
  3. 硬件支持:自旋锁需要硬件支持才能实现忙等待,因此适用于多处理器或多核处理器系统。需要注意的是,自旋锁不适合等待时间较长的场景,因为长时间的忙等待会消耗大量 CPU 资源,导致系统性能下降。在等待时间较长的情况下,应使用其他锁机制,例如互斥锁、读写锁等。

读写锁

概念

读写锁(Read-Write Lock),又称共享独占锁,是一种特殊的锁机制,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。读写锁能有效提升程序的并发性能,尤其是当读操作比写操作更频繁时,它可以减少锁竞争,提高程序的并发性能。 读写锁的实现非常简单,通常用一个计数器和一把互斥锁来表示锁的状态。当一个线程想要读取共享资源时,会先尝试获取读锁;如果当前没有线程持有写锁,读操作就可以继续进行;如果某个线程持有写锁,读操作就必须等待写锁被释放。当一个线程想要写入共享资源时,会先尝试获取写锁;如果当前没有线程持有读锁或写锁,写操作就可以继续进行;如果某个线程持有读锁或写锁,写操作就必须等待所有读锁和写锁被释放。 需要注意的是,虽然读写锁可以提高程序的并发性能,但当写操作频繁时,读写锁的优势可能会被削弱。因为每次写操作都必须等待读锁和写锁都释放,读操作也会被阻塞,从而影响程序性能。因此,在实际应用中,需要根据具体情况选择合适的锁机制。

使用场景

读写锁是一种特殊的锁机制,允许多个线程同时读取共享资源,但在写入共享资源时必须是互斥的。读写锁适用于以下场景:

  1. 读操作远多于写操作:当读操作远多于写操作时,可以使用读写锁来提高程序的并发性能。读写锁允许多个线程同时读取共享资源,从而减少线程间的互斥竞争,提高程序的并发性能。
  2. 共享资源的读操作耗时较长:当共享资源的读操作比较耗时,可以使用读写锁来提高程序性能。读写锁允许多个线程同时读取共享资源,从而减少线程间的互斥竞争和线程上下文切换的开销,提高程序性能。
  3. 共享资源的写操作较少:当共享资源的写操作较少时,可以使用读写锁来提高程序的并发性能。读写锁在写入共享资源时必须互斥,但在读取共享资源时允许多个线程同时进行,从而减少线程间的互斥竞争,提高程序的并发性能。需要注意的是,读写锁适用于读多写少的场景,当读写操作比例相近时,读写锁的性能可能不如互斥锁。此外,使用读写锁时需要注意锁粒度,避免锁粒度过细或过粗而影响程序性能。

互斥锁

概念

互斥锁(Mutex)是最基本的锁机制,它能够保证同一时间只有一个线程可以访问共享资源。使用互斥锁时,当一个线程获取锁之后,其他线程必须等待该线程释放锁之后才能获取锁,从而避免多个线程同时修改共享资源所导致的数据竞争与不一致问题。 互斥锁通常通过加锁(lock)和解锁(unlock)两个操作来实现。当线程想要访问共享资源时,需要先尝试获取锁;如果当前没有其他线程持有锁,该线程就可以获得锁并访问共享资源;如果其他线程持有锁,该线程就必须等待锁被释放。访问结束后,线程需要释放锁,以便其他线程能够获得锁并访问共享资源。 需要注意的是,虽然使用互斥锁能够保证同一时间只有一个线程访问共享资源,但频繁的加锁与解锁会导致程序性能下降,因为加锁和解锁操作需要系统调用与内核切换的开销。因此,在实际应用中,需要谨慎使用互斥锁,避免过多的锁竞争和锁等待。

使用场景

互斥锁是一种常见的锁机制,它确保同一时间只有一个线程能访问共享资源,从而避免线程间的互斥竞争。互斥锁适用于以下场景:

  1. 共享资源占用时间短:当共享资源被占用的时间很短、等待时间也不长时,使用互斥锁可以避免线程进入睡眠状态和线程上下文切换的开销,从而提高程序性能。
  2. 访问共享资源的线程数量少:互斥锁适用于访问共享资源的线程数量较少的情形,因为当访问共享资源的线程较多时,互斥锁的竞争会变得激烈,导致程序性能下降。
  3. 临界区代码较少:互斥锁适用于临界区代码较少的情形,因为当临界区代码较多时,互斥锁的竞争会变得激烈,导致程序性能下降。
  4. 同步需求强:互斥锁适用于同步需求较强的情形,因为它能保证同一时间只有一个线程访问共享资源,从而避免线程间的互斥竞争与数据冲突。需要注意的是,在多线程环境下使用互斥锁,可能会出现死锁等问题,因此需要考虑锁粒度和加锁顺序等问题,以避免死锁等问题。此外,使用互斥锁时还需要注意锁的性能问题,避免过度使用互斥锁导致程序性能下降。

乐观锁与悲观锁

乐观锁和悲观锁是两种不同的锁机制,用于解决并发访问共享资源时的数据竞争问题。 悲观锁是一种悲观的思想。它认为在并发环境中,共享资源很容易被其他线程修改,因此每次访问共享资源时都必须加锁,以保证同一时间只有一个线程能访问共享资源。悲观锁的代表是互斥锁,它能够保证同一时间只有一个线程访问共享资源。但加锁与解锁的代价较高,容易导致程序性能下降。

乐观锁是一种乐观的思想。它认为在并发环境中,共享资源很少被其他线程修改,因此每次访问共享资源时并不加锁,而是先读取共享资源,并在修改前检查共享资源的版本号等信息:如果未被修改,就进行修改并更新版本号等信息;否则放弃修改并重试。乐观锁以无锁编程、CAS(Compare And Swap,比较并交换)等操作作为代表,可以减少锁竞争和线程上下文切换的开销,提高程序的并发性能。 需要注意的是,虽然乐观锁能提高程序的并发性能,但当共享资源被并发频繁修改时,乐观锁的重试次数可能会增加,导致程序性能下降。因此,在实际应用中,需要根据具体情况选择合适的锁机制。