本文主要是介绍锁策略详解:互斥锁、读写锁、乐观锁与悲观锁、轻量级锁与重量级锁、自旋锁、偏向锁、可重入锁与不可重入锁、公平锁与非公平锁,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
一.锁策略
锁策略指的是在多线程编程中用于管理共享资源访问的规则和技术。它们确保在任何给定时间只有一个线程可以访问共享资源,以防止竞态条件和数据不一致性问题。常见的锁策略包括:
-
互斥锁(Mutex):最常见的锁类型,用于确保同一时刻只有一个线程可以访问共享资源。其他线程必须等待当前线程释放锁。
-
读写锁(ReadWriteLock):允许多个线程同时读取共享资源,但只有一个线程可以写入共享资源。这有助于提高并发性能,因为读取操作不会阻塞其他读取操作。
-
悲观锁(Pessimistic Locking):假设会发生并发访问,因此在访问共享资源之前先获取锁。适用于写入操作频繁的场景。
-
乐观锁(Optimistic Locking):假设不会发生并发访问,因此在提交更改时检查是否有其他线程已经修改了资源。适用于读取操作频繁,且冲突不经常发生的场景。
-
自旋锁(Spin Lock):在获取锁时不会阻塞线程,而是使用循环等待直到锁可用。适用于短期占用情况,避免了线程切换的开销。
-
... ...
选择合适的锁策略需要根据具体的应用场景和性能要求来决定。不同的锁策略有不同的开销和适用场景,需要根据实际情况进行权衡和选择。
二.互斥锁
互斥锁(Mutex,Mutual Exclusion)是一种用于多线程编程的同步机制,用于确保在任何给定时间只有一个线程可以访问共享资源。互斥锁提供了两个主要操作:锁定(lock)和解锁(unlock)。在访问共享资源之前,线程需要先获取锁;在访问完成后,需要释放锁,以允许其他线程访问该资源。
互斥锁的特点包括:
- 互斥性:同一时间只有一个线程可以持有互斥锁,其他线程必须等待当前线程释放锁后才能获取锁。
- 阻塞:当一个线程尝试获取一个已经被其他线程持有的互斥锁时,该线程会被阻塞,直到获取到锁为止。
- 锁定状态:互斥锁可以有两种状态:锁定和未锁定。只有持有锁的线程才能访问共享资源。
- 安全性:互斥锁可以确保在多线程环境下对共享资源的安全访问,避免了竞态条件和数据不一致性问题。
在C/C++中,互斥锁通常由pthread_mutex_t
表示,可以使用pthread_mutex_lock
来获取锁,pthread_mutex_unlock
来释放锁。在Java中,可以使用synchronized
关键字来实现互斥锁的功能,也可以使用ReentrantLock
类来实现互斥锁。
三.读写锁
读写锁(ReadWriteLock)是一种并发控制机制,允许多个线程同时读取共享资源,但在写入时需要互斥,即只能有一个线程进行写操作,且写操作时禁止读取操作。读写锁的主要目的是在读操作频繁、写操作较少的情况下提高并发性能。
读写锁有两种状态:读取锁和写入锁。在读取锁状态下,多个线程可以同时获取读取锁,从而允许并发读取操作。在写入锁状态下,只有一个线程可以获取写入锁,其他线程无法获取读取或写入锁,直到写入操作完成。
读写锁的特点包括:
-
读写分离:读取操作不会阻塞其他读取操作,但会阻塞写入操作。这样可以提高读取操作的并发性能。
-
写入互斥:写入操作会阻塞其他读取和写入操作,确保在写入操作时不会有其他线程对共享资源进行读取或写入。
-
公平性:读写锁可以是公平的,即按照请求锁的顺序分配锁;也可以是非公平的,即允许插队获取锁。
Java中的读写锁由ReentrantReadWriteLock
实现,读写锁特别适合于 "频繁读, 不频繁写" 的场景中。
- ReentrantReadWriteLock.ReadLock 类表示一个读锁,这个对象提供了 lock / unlock 方法进行加锁解锁
- ReentrantReadWriteLock.WriteLock 类表示一个写锁,这个对象也提供了 lock / unlock 方法进行加锁解锁
四.乐观锁与悲观锁
乐观锁(Optimistic Locking)和悲观锁(Pessimistic Locking)是两种不同的并发控制机制,用于处理多线程环境下的数据访问冲突。
-
乐观锁(Optimistic Locking):
- 乐观锁的核心思想是假设在大多数情况下,数据并发访问冲突的概率很低,因此不需要加锁。在读取数据时,不会加锁,而是在尝试更新数据时才会进行检查。
- 在使用乐观锁时,通常会在读取数据时记录下数据的版本号或者时间戳,在尝试更新数据时检查这个版本号或者时间戳是否发生变化。如果没有变化,则说明在读取到写入之间数据没有被其他线程修改,可以进行更新操作;如果发生变化,则表示数据被修改过,需要进行冲突处理,如回滚或者重试等。
- 乐观锁适用于读操作频繁、写操作相对较少的场景,可以减少锁竞争,提高并发性能。
-
悲观锁(Pessimistic Locking):
- 悲观锁的核心思想是假设在数据访问时会发生并发冲突,因此在访问数据之前先加锁,确保在任何时候只有一个线程可以访问数据,其他线程需要等待。
- 悲观锁适用于写操作频繁的场景,可以有效避免数据被并发修改而导致的问题。常见的悲观锁实现包括互斥锁(Mutex)和读写锁(ReadWriteLock)等。
总的来说,乐观锁适合于并发写入比较少、并发读取比较多的场景,可以提高系统的并发性能;而悲观锁适合于并发写入比较频繁的场景,可以保证数据的一致性和完整性。选择合适的锁策略取决于具体的应用场景和需求。
举个例子来说
同学 A 认为 "老师是比较忙的, 我来问问题, 老师不一定有空解答"。因此同学 A 会先给老师发消息: "老师你忙嘛? 我下午两点能来找你问个问题嘛?" (相当于加锁操作) 得到肯定的答复之后,才会真的来问问题。 如果得到了否定的答复,那就等一段时间,下次再来和老师确定时间。这个是悲观锁。
同学 B 认为 "老师是比较闲的, 我来问问题, 老师大概率是有空解答的"。因此同学 B 直接就来找老师(没加锁, 直接访问资源) 如果老师确实比较闲,那么直接问题就解决了。如果老师这会确实很忙,那么同学 B 也不会打扰老师,就下次再来(虽然没加锁,但是能识别出数据访问冲突)。这个是乐观锁。
这两种思路不能说谁优谁劣,而是看当前的场景是否合适。如果当前老师确实比较忙,那么使用悲观锁的策略更合适,使用乐观锁会导致 "白跑很多趟", 耗费额外的资源。如果当前老师确实比较闲, 那么使用乐观锁的策略更合适,使用悲观锁会让效率比较低。
五.重量级锁与轻量级锁
轻量级锁和重量级锁是 Java 虚拟机中用于实现锁的两种不同策略,主要针对 synchronized 关键字所提供的锁的不同实现方式。
-
轻量级锁(Lightweight Lock):
- 轻量级锁是为了在多线程竞争情况下,提高性能而引入的一种锁优化技术。当一个线程尝试获取锁时,如果锁没有被其他线程占用,虚拟机会在当前线程的栈帧中使用 CAS 操作尝试将对象头部的 Mark Word 替换为指向当前线程的锁记录指针(Lock Record Pointer)。
- 如果 CAS 操作成功,当前线程就获得了锁,并且锁的状态被标记为轻量级锁。此时其他线程访问同步块时会尝试自旋等待,而不是直接阻塞,以减少线程切换的开销。
- 如果自旋等待一段时间后仍无法获取锁,或者其他线程争用激烈,CAS 操作失败,那么轻量级锁会膨胀为重量级锁。
-
重量级锁(Heavyweight Lock):
- 当轻量级锁膨胀失败时,锁会升级为重量级锁。重量级锁会使其他线程阻塞,而不是进行自旋等待,防止CPU空转浪费资源。
- 重量级锁的实现通常是通过操作系统的互斥量(Mutex)来实现的,当一个线程获取了锁,其他线程就无法获取锁,只能在等待队列中等待锁释放。
总的来说,轻量级锁适用于线程竞争不激烈的情况,可以减少线程切换的开销,提高性能;而重量级锁适用于线程竞争激烈的情况,可以保证数据的一致性和完整性。 Java 虚拟机会根据锁竞争的情况自动选择合适的锁实现方式。
六.自旋锁
自旋锁是一种基于循环重试的锁机制,在多线程编程中用于实现对共享资源的互斥访问。当一个线程尝试获取自旋锁时,如果锁已被其他线程持有,该线程不会立即进入阻塞状态,而是在循环中不断尝试获取锁,直到成功获取为止,或者达到最大尝试次数后才会放弃。
自旋锁的主要特点包括:
- 循环等待:获取自旋锁的线程会在一个循环中不断尝试获取锁,直到成功或者达到一定的尝试次数。
- 忙等待:自旋锁的获取过程是忙等待的,即线程在等待锁的过程中会一直占用CPU资源。
- 无阻塞:自旋锁的获取过程不会使线程进入阻塞状态,适用于对锁的占用时间较短的情况。
- 性能:自旋锁适用于锁竞争不激烈、等待锁时间较短的情况,可以减少线程切换的开销,提高性能。
- 等待限时:为了避免自旋锁的线程一直占用CPU资源,通常会设置一个最大尝试次数或者超时时间,超过这个限制后线程会放弃自旋并进入阻塞状态等待锁的释放。
自旋锁在并发度高、锁竞争不激烈的情况下可以有效地提高性能,但在锁竞争激烈、持有锁时间较长的情况下,自旋锁可能会导致性能下降。因此,在选择锁机制时需要根据实际情况进行权衡和选择。在Java中,java.util.concurrent
包提供了ReentrantLock
的实现,其中包括了可重入锁和自旋锁的功能。
七.偏向锁
偏向锁是Java虚拟机为了减少无竞争情况下的同步原语的性能消耗而引入的一种优化手段。当一个线程访问同步块时,如果该同步块没有被锁定或者被其他线程锁定,那么这个线程会尝试获取偏向锁,并将对象头中的标记设置为指向自己的线程ID。之后,当这个线程再次进入同步块时,不需要再次获取锁,因为对象头中已经记录了这个线程已经获取了偏向锁。
偏向锁适用于大多数情况下都是单线程访问同步块的场景,因为在这种情况下,偏向锁可以显著减少获取锁的代价。但是,如果有其他线程竞争锁,那么偏向锁会失效,对象会膨胀为重量级锁,这样会增加获取锁的代价。
偏向锁的优点包括:
- 减少同步操作的耗时,提高程序性能;
- 对于无竞争的场景,减少不必要的线程切换。
需要注意的是,偏向锁只适用于无竞争的情况,如果存在竞争,会导致额外的性能损失。因此,在使用偏向锁时,需要根据实际场景进行评估和选择。
八.公平锁与非公平锁
公平锁和非公平锁是两种不同的锁获取策略,用于多线程环境下对共享资源的访问控制。
-
公平锁:
- 公平锁是指多个线程按照请求锁的顺序获取锁,即先到先得的原则。当一个线程尝试获取公平锁时,如果锁当前被其他线程持有,该线程会进入等待队列,按照先来后到的顺序等待锁的释放。
- 公平锁保证了锁的获取是按照请求的顺序进行的,避免了某些线程长时间无法获取到锁的情况,确保了线程获取锁的公平性。
-
非公平锁:
- 非公平锁是指多个线程尝试获取锁时,并不考虑锁的等待队列中是否有等待线程,而是直接尝试获取锁。如果锁当前未被其他线程持有,则获取成功;如果锁已被其他线程持有,则当前线程可能会被阻塞,也可能会一直尝试获取锁。
- 非公平锁相比于公平锁,可能会导致某些线程长时间无法获取到锁,存在一定的不公平性。但由于不需要维护等待队列的顺序,非公平锁的性能通常会比公平锁更好。
在Java中,ReentrantLock
和ReentrantReadWriteLock
可以指定为公平锁或非公平锁。在创建锁时,可以通过构造方法来指定锁的公平性。公平锁在一定程度上保证了线程获取锁的顺序,适用于对锁的公平性有要求的场景;而非公平锁则更适用于对性能要求较高,可以容忍一定程度的不公平性的场景。
九.可重入锁与不可重入锁
可重入锁(Reentrant Lock)和不可重入锁是两种锁的概念,主要区别在于同一个线程能否多次获取同一把锁。
-
可重入锁:
- 可重入锁是指同一个线程可以多次获取同一把锁,而不会发生死锁。线程每次获取锁时,锁的计数器会加一;每次释放锁时,计数器会减一。只有当计数器为零时,锁才会被完全释放。
- 可重入锁的一个典型应用场景是方法递归调用。当一个同步方法内部调用同一个类的其他同步方法时,如果没有可重入锁,就会发生死锁。而可重入锁能够保证线程在递归调用时不会被自己阻塞。
-
不可重入锁:
- 不可重入锁是指同一个线程不能多次获取同一把锁。如果一个线程已经持有了某个锁,再次尝试获取该锁时会被阻塞,直到锁被释放。
- 不可重入锁在一些情况下可能会导致死锁,因为线程在持有锁的情况下又试图获取同一把锁,会造成自己阻塞而无法释放已持有的锁。
Java中的synchronized
关键字实现的锁是可重入锁,即同一个线程可以重复获取同一把锁。而ReentrantLock
也是可重入锁的一种实现。可重入锁能够简化并发编程中对锁的使用,避免了一些潜在的死锁情况,提高了程序的可靠性和健壮性。
本次的分享就到此为止了,希望我的分享能给您带来帮助,创作不易也欢迎大家三连支持,你们的点赞就是博主更新最大的动力!如有不同意见,欢迎评论区积极讨论交流,让我们一起学习进步!有相关问题也可以私信博主,评论区和私信都会认真查看的,我们下次再见
这篇关于锁策略详解:互斥锁、读写锁、乐观锁与悲观锁、轻量级锁与重量级锁、自旋锁、偏向锁、可重入锁与不可重入锁、公平锁与非公平锁的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!