在使用多线程的情况中,我们常常需要加锁来确保线程的安全性,
但锁的实现也是分成很多种情况的,不同的锁有不同的特性
本文章将简要介绍常见的锁策略和它们适用情形
一.乐观锁和悲观锁
悲观锁(Pessimistic Lock)
悲观锁的核心思想是在操作数据时悲观地认为会发生并发冲突,因此在访问数据之前先加锁,确保同一时刻只有一个线程可以访问数据,从而避免数据的并发修改。悲观锁的典型代表是关系型数据库中的行级锁。
乐观锁(Optimistic Lock)
乐观锁的核心思想是假设数据的访问和修改不会发生并发冲突,因此在操作数据时不对数据加锁,而是在更新数据时检查是否有其他线程对数据进行了修改。如果检测到数据已经被修改,则放弃本次操作或者进行重试。乐观锁的典型代表是版本控制机制。
Synchronized 初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略
悲观锁适用于对数据并发修改频率较高的场景,而乐观锁适用于对数据并发修改频率较低的场景
二.读写锁(ReadWriteLock)
读写锁(ReadWriteLock)是一种并发控制机制,允许多个线程同时读取共享资源,但在写入时会排斥其他线程的读取和写入操作。读写锁的核心思想是提高系统的并发性能,尽可能地允许多个线程同时读取共享资源,以提高系统的吞吐量。
读写锁通常由两种锁组成:读锁(Read Lock)和写锁(Write Lock)。
-
读锁:允许多个线程同时获取读锁,并发地读取共享资源,但不允许写操作。当没有线程持有写锁时,可以同时有多个线程持有读锁。
-
写锁:写锁是排他的,当一个线程持有写锁时,其他线程无法获取读锁或写锁,直到写锁被释放。写锁用于对共享资源进行写操作,确保写操作的原子性和一致性。
读写锁的特点:
-
读写分离:读操作并发执行,提高了系统的并发性能;写操作独占资源,保证了数据的一致性。
-
适用性:适用于读操作频繁、写操作较少的场景,可以有效地提高系统的吞吐量。
-
公平性:读写锁可以是公平或非公平的。在公平模式下,锁的获取是按照请求的顺序进行的,而在非公平模式下,锁的获取是非公平的,允许插队。
总的来说,读锁和读锁之间是不会排斥的,而其他情况都要排斥
在 Java 中,读写锁的主要实现是 ReentrantReadWriteLock
类,它实现了 ReadWriteLock
接口。使用读写锁时,可以根据具体的业务需求选择合适的锁粒度,以实现对共享资源的高效并发访问。
三.重量级锁和轻量级锁
重量级锁(Heavyweight Lock)
重量级锁是指在Java中通过synchronized关键字实现的锁。当一个线程获取了重量级锁,其他线程就不能进入同步代码块或方法,只能等待锁的释放。重量级锁的实现通常基于操作系统的互斥量(Mutex),在锁竞争激烈的情况下,会导致线程频繁地从用户态切换到内核态,造成较大的性能开销。
重量级锁的特点:
- 互斥性:同一时刻只允许一个线程持有锁。
- 阻塞等待:其他线程在获取锁失败时会被阻塞,直到锁被释放。
轻量级锁(Lightweight Lock)
轻量级锁是一种优化手段,用于解决线程在竞争同步资源时的性能问题。当一个线程尝试获取锁时,JVM会先使用CAS(Compare and Swap)操作尝试原子更新对象的锁信息,如果成功则表示获取锁成功,不需要进入阻塞状态,称为轻量级锁。如果CAS操作失败,表示有其他线程竞争锁,此时会进一步升级为重量级锁。
轻量级锁的特点:
- 自旋:获取锁失败时,线程会进行一定次数的自旋尝试获取锁,避免进入阻塞状态,减少线程切换的开销。
- CAS操作:使用CAS操作尝试获取锁,避免了频繁地切换到内核态,提高了性能。
轻量级锁适用于锁竞争不激烈的情况,可以减少线程的阻塞和唤醒操作,提高了系统的并发性能。但是在锁竞争激烈的情况下,轻量级锁的自旋会消耗CPU资源,可能反而降低性能,此时会升级为重量级锁。在Java中,轻量级锁的实现主要依赖于对象头中的锁标志位。
简单来说,轻量级锁会不断尝试获取锁,而重量级锁如果发现锁被占用会直接使线程进入阻塞状态,等待一段时间后再重新尝试获取
四.自旋锁(Spin Lock)
自旋锁是一种基于忙等待的锁,它通过循环反复尝试获取锁,而不是立即进入阻塞状态。当一个线程尝试获取自旋锁时,如果发现锁已经被其他线程持有,则不断地循环尝试获取锁,直到获取成功或者达到一定的尝试次数。
自旋锁的特点:
- 忙等待:线程在尝试获取锁时会忙等待,不断地循环检查锁的状态,而不是进入阻塞状态。
- 适用于短暂的等待:自旋锁适用于锁竞争不激烈、临界区执行时间短暂的情况,避免了线程频繁地切换到阻塞状态带来的性能开销。
- 有限自旋:为了避免线程长时间的忙等待,自旋锁通常会设置一个尝试获取锁的最大次数或者时间限制,在达到限制后会放弃自旋,转而进入阻塞状态。
- 非公平性:自旋锁通常是非公平的,即先到先得,没有公平队列。但是有些实现中也会提供公平的自旋锁。
自旋锁适用于多核CPU环境下,当线程竞争锁时,通过自旋等待可以避免线程进入阻塞状态,减少了线程切换的开销,提高了系统的并发性能。但是在锁竞争激烈的情况下,自旋锁可能会导致CPU资源的浪费,因此需要根据具体的应用场景和硬件环境进行合理的选择。在Java中,java.util.concurrent
包中提供了一些自旋锁的实现,如ReentrantLock
的无参构造函数会创建一个非公平的自旋锁。
自旋锁是实现轻量级锁的一种方式
五.公平锁和非公平锁
-
公平锁:
- 公平锁是指多个线程按照它们请求锁的顺序来获取锁,即先到先得的原则。
- 当一个线程尝试获取公平锁时,如果锁已经被其他线程持有,则该线程会被放入一个等待队列中,按照先后顺序等待锁的释放。当锁被释放时,等待队列中的第一个线程会被唤醒并获得锁。
- 公平锁的优点是确保了所有线程获取锁的公平性,避免了饥饿现象,但是可能会因为线程频繁地切换状态而带来性能上的损失。
-
非公平锁:
- 非公平锁则没有考虑线程请求锁的顺序,当一个线程尝试获取非公平锁时,它会直接尝试获取锁,如果锁已经被其他线程持有,则该线程会被阻塞或者进入自旋等待,直到获取到锁为止。
- 非公平锁的优点是相对于公平锁,它减少了线程切换的开销,提高了系统的吞吐量,但可能会导致某些线程长时间等待锁,造成不公平性。
在Java中,ReentrantLock
类提供了公平锁和非公平锁的实现。通过传入不同的构造参数,可以创建对应类型的锁。例如,使用ReentrantLock
的带有fair
参数的构造函数可以创建一个公平锁,而默认的构造函数创建的是非公平锁。
选择公平锁还是非公平锁取决于具体的应用场景和需求。如果对锁的公平性有严格要求,并且可以接受一定的性能损失,那么可以选择公平锁;如果追求更高的吞吐量,并且可以容忍一定程度上的不公平性,那么可以选择非公平锁。
六.可重入锁和不可重入锁
可重入锁(Reentrant Lock)和不可重入锁(Non-Reentrant Lock)是两种常见的锁机制,它们的主要区别在于同一个线程能否多次获取同一把锁。
-
可重入锁(Reentrant Lock):
- 可重入锁允许同一个线程在持有锁的情况下再次获取该锁,而不会被自己持有的锁所阻塞。
- 这种锁机制允许线程在持有锁的情况下多次进入由该锁保护的代码块,而不会引发死锁或者其他问题。
- 可重入锁通常会通过记录持有锁的线程以及锁的持有次数来实现。
-
不可重入锁(Non-Reentrant Lock):
- 不可重入锁不允许同一个线程在持有锁的情况下再次获取该锁,如果一个线程在持有锁时尝试再次获取该锁,就会被阻塞。
- 这种锁机制可能会导致死锁,因为线程在持有锁的情况下又尝试获取同一把锁时,会一直被阻塞,而无法释放已持有的锁。
在实际应用中,可重入锁是更常见和更实用的锁机制,因为它能够简化编程模型并且避免了一些潜在的问题,如死锁。Java中的ReentrantLock
就是可重入锁的一个典型实现。不可重入锁在某些特定场景下可能会用到,但是需要开发者特别注意避免死锁等问题的发生。