🔥个人主页: 中草药
🔥专栏:【Java】登神长阶 史诗般的Java成神之路
💰一.常见的的锁策略
锁策略(Locking Strategy)是指在多线程环境中,为了控制对共享资源的访问,确保数据一致性和线程安全,而采用的一系列机制和规则。在并发编程中,锁是管理共享资源访问的核心工具,它防止了多个线程同时修改同一份数据,从而避免了数据竞争和不一致性的问题。不同的锁策略有着不同的特性和适用场景,它们在并发控制、性能、复杂性和可扩展性方面存在差异。
1.乐观锁vs悲观锁
乐观锁
乐观锁基于“乐观主义”假设,认为数据不太可能被并发修改,因此在读取数据时不会锁定数据,只有在更新数据时才检查数据是否被其他事务修改过。乐观锁通常使用版本号或时间戳来实现,每次更新数据时都会检查版本号是否与读取时相同,如果不同,则表明数据已经被其他事务修改,本次更新将被拒绝。
乐观锁的优点在于它减少了锁的使用,提高了系统的并发性能,尤其适合读多写少的场景。缺点是如果多个事务同时尝试更新同一份数据,可能会导致更新失败,需要重新读取数据并再次尝试更新,这被称为“重试”。
悲观锁
悲观锁基于“悲观主义”假设,认为数据很可能被并发修改,因此在读取或写入数据之前,会先锁定数据,阻止其他线程或进程的并发访问。悲观锁通过在事务开始时获取锁并在事务结束时释放锁来实现。常见的悲观锁机制包括:
悲观锁的优点在于它能保证数据的一致性,避免了脏读和不可重复读等问题。然而,它的缺点也很明显,主要是锁的等待时间可能较长,容易造成死锁,且降低了系统的并发度。
特征 | 乐观锁(Optimistic Locking) | 悲观锁(Pessimistic Locking) |
---|---|---|
基本假设 | 假设数据不太可能被并发修改 | 假设数据很可能被并发修改 |
实现机制 | 使用版本号或时间戳进行并发控制 | 通过锁定数据防止并发修改 |
锁的使用 | 不在读取数据时使用锁 | 在读取或写入数据时使用锁 |
更新策略 | 更新时检查版本号,如果冲突则重试 | 更新前锁定,更新后释放锁 |
并发性能 | 高,因为它减少了锁的使用 | 低,因为锁的等待时间可能较长 |
数据一致性 | 较低,可能发生重试 | 高,锁定期间确保数据一致性 |
死锁风险 | 无,因为没有锁的等待 | 高,尤其是在复杂的事务中 |
资源消耗 | 低,较少的系统调用和上下文切换 | 高,较多的系统调用和上下文切换 |
适用场景 | 读多写少,对实时性要求较高的系统 | 写操作频繁,对数据一致性要求极高的系统 |
复杂性 | 实现上相对复杂,需要处理版本控制和冲突 | 实现上相对简单,依赖于锁机制 |
2.轻量级锁vs重量级锁
轻量级锁
-
定义:轻量级锁是JVM为了提高锁的性能而引入的一种机制,它试图在没有线程竞争的情况下避免使用重量级锁的开销。轻量级锁在Java 6之后的版本中默认启用。
-
实现:轻量级锁使用了基于CAS(Compare and Swap)的原子操作。当一个线程尝试获取锁时,它会通过CAS操作将当前线程ID写入锁对象的Mark Word中。如果CAS操作成功,那么线程获得了锁;如果失败,则进入下一步骤。
-
特点:
重量级锁
-
实现:重量级锁的获取和释放涉及操作系统层面的线程挂起和唤醒,这通常需要从用户态切换到内核态,开销较大。
-
特点:
特征 | 轻量级锁(Lightweight Lock) | 重量级锁(Heavyweight Lock) |
---|---|---|
实现机制 | 基于CAS的原子操作 | 操作系统层面的线程挂起和唤醒 |
性能 | 在锁竞争较少时性能高 | 锁竞争激烈时,性能低 |
上下文切换 | 减少了上下文切换 | 需要进行上下文切换 |
资源消耗 | 相对较低 | 相对较高 |
适用场景 | 读多写少,锁竞争较小的场景 | 写操作频繁,对数据一致性要求高的场景 |
锁升级 | 当锁竞争加剧时,轻量级锁可能升级为重量级锁 | 无升级过程 |
3.自旋锁vs挂起等待锁
自旋锁(Spin Lock)
挂起等待锁(Sleeping Lock)
-
定义:当一个线程试图获取一个已经被其他线程持有的锁时,挂起等待锁会让当前线程进入等待状态,直到锁变为可用。这通常涉及到线程的挂起和唤醒。
-
特点:
- 减少了CPU的空转,节省了计算资源。
- 适用于锁持有时间较长,或者线程竞争较为激烈的场景。
- 线程的挂起和唤醒涉及到操作系统层面的操作,会有一定的开销,包括上下文切换。
特征 | 自旋锁(Spin Lock) | 挂起等待锁(Sleeping Lock) |
---|---|---|
CPU使用 | 可能导致CPU空转,消耗CPU资源 | 节省CPU资源,避免空转 |
上下文切换 | 减少了线程的上下文切换 | 增加了线程的上下文切换 |
适用场景 | 锁持有时间短,竞争不激烈 | 锁持有时间长,或竞争激烈 |
开销 | 锁竞争大时CPU开销大 | 锁竞争大时上下文切换开销大 |
4.公平锁vs非公平锁
公平锁
非公平锁
特征 | 公平锁(Fair Lock) | 非公平锁(Unfair Lock) |
---|---|---|
获取顺序 | 按照请求锁的顺序获取锁 | 不保证获取顺序,可能存在“插队”现象 |
性能 | 锁竞争激烈时性能可能较低 | 锁竞争较少时性能较高 |
公平性 | 高,避免线程饥饿 | 低,可能存在线程饥饿 |
默认行为 | ReentrantLock 不默认使用 | ReentrantLock 的默认行为 |
应用场景 | 锁竞争激烈,需避免线程饥饿 | 锁竞争较少,追求高吞吐量 |
注意
5.可重入锁vs不可重入锁
可重入锁(Reentrant Lock)
不可重入锁(Non-reentrant Lock)
特征 | 可重入锁(Reentrant Lock) | 不可重入锁(Non-reentrant Lock) |
---|---|---|
递归锁定 | 支持,允许多次获取同一把锁 | 不支持,再次获取同一把锁会导致阻塞 |
死锁预防 | 内部机制可以避免递归锁定导致的死锁 | 简化了锁管理,但限制了灵活性 |
灵活性 | 高,适合复杂多线程场景 | 低,适合简单或不需要递归锁定的场景 |
安全性 | 高,避免了死锁 | 较高,但在某些场景下可能过于限制 |
synchronized 是可重⼊锁
6.读写锁
读写锁(Read-Write Lock)是一种特殊的锁机制,它允许多个读操作同时进行,但写操作是独占的。读写锁的设计目的是为了提高并发性能,尤其是在读操作远远多于写操作的场景下。下面详细解释读写锁的工作原理及其在Java中的实现:
工作原理
特点
-
高并发性:读写锁提高了读操作的并发度,因为在没有写操作时,多个读线程可以同时访问共享资源。
-
写操作独占:写操作总是独占的,确保了数据在写入时的一致性,防止了数据竞争条件。
-
公平性:读写锁可以有不同的公平性实现。有些实现保证了读写操作的公平性,即按照请求顺序获取锁;有些则优先考虑写操作,以避免写饿死。
Java中的实现
在Java中,读写锁通过java.util.concurrent.locks.ReadWriteLock
接口来实现,最常见的实现是ReentrantReadWriteLock
。ReadWriteLock
接口定义了readLock()
和writeLock()
两个方法,分别用于获取读锁和写锁。
ReentrantReadWriteLock.ReadLock 类 表示一个读锁,这个对象提供了lock/unlock方法进行加锁解锁.
ReentrantReadWriteLock.WriteLock 类 表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁.
Synchronized 不是读写锁.
🪙二.CAS
比较并交换(Compare-and-Swap,简称CAS)是一种无锁算法的基本构建块,广泛应用于并发编程中,用于实现原子操作。CAS操作在多处理器架构中特别有用,因为它能够确保即使在多个线程或处理器同时尝试修改同一内存位置时,操作也能正确、原子地执行。下面详细讲述CAS的概念、工作原理以及在Java中的实现。
1.基本概念
CAS是一种硬件级别的原子操作,通常由处理器直接支持。它涉及三个操作数:内存位置(V)、期望的旧值(A)和新值(B)。CAS操作会比较内存位置V的当前值与期望的旧值A是否相等,如果相等,则将V的值原子地更新为新值B;如果不相等,则操作失败,返回当前的V值。由于CAS操作是原子的,这意味着在CAS操作过程中,不会有其他线程或进程能够干扰这个操作。
2.工作原理
-
加载值:CAS操作开始时,线程会加载内存位置V的当前值。
-
比较值:将加载的值与期望的旧值A进行比较。如果两者相等,则进行下一步;如果不等,CAS操作失败。
-
交换值:如果比较成功,CAS操作会原子地将V的值更新为新值B。
-
返回结果:CAS操作返回一个结果,指示操作是否成功。如果成功,通常返回新值;如果失败,返回旧值。
工作原理伪代码
java">boolean CAS(address, expectValue, swapValue) {if (&address == expectedValue) {&address = swapValue;return true;}return false;
}
3.CAS在Java中的实现
在Java中,CAS操作主要通过Unsafe
类的compareAndSwapInt
、compareAndSwapLong
和compareAndSwapObject
方法实现,这些方法提供了对底层硬件CAS操作的访问。然而,直接使用Unsafe
类通常被认为是不推荐的,因为它破坏了Java的封装性和安全性。
更安全且推荐的方式是使用Java并发库中的
Atomic
类,如AtomicInteger
、AtomicLong
和AtomicReference
。这些类内部使用了CAS操作,但对外提供了更高级、更安全的API。
举例
java">import java.util.concurrent.atomic.AtomicInteger;public class demo5 {private static AtomicInteger count=new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for (int i = 0; i < 5000; i++) {count.getAndIncrement(); //count++
// count.incrementAndGet(); ++count
// count.getAndDecrement(); count--
// count.decrementAndGet(); --count
// count.getAndAdd(10) count+=10}});Thread t2=new Thread(()->{for (int i = 0; i < 5000; i++) {count.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}
4.CAS的局限性
尽管CAS操作提供了原子性,但它也有其局限性:
-
ABA问题:如果一个值被多次设置为相同的值A,CAS操作可能误以为值没有被改变。解决这个问题通常需要使用版本号或标记值。
-
循环时间:在多线程环境中,如果多个线程尝试同时更新同一变量,可能会导致CAS操作反复失败,从而导致线程在自旋中消耗大量CPU时间。
尽管如此,CAS仍然是实现无锁数据结构和算法的关键技术,能够显著提高并发程序的性能和可伸缩性。
💳三.相关面试题
不做具体详细的拓展回答,仅做一个简答
悲观锁认为多个线程访问同⼀个共享变量冲突的概率较⼤, 会在每次访问共享变量之前都去真正加锁.
乐观锁认为多个线程访问同⼀个共享变量冲突的概率不大,并不会真的加锁,而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突.
💸四.总结与反思
神龟虽寿,犹有竞时。——曹操
在深入学习基本锁策略和CAS(Compare-and-Swap)操作的过程中,我对并发编程有了更深刻的理解。这些知识点不仅增强了我的编程技能,还帮助我更好地应对多线程环境下的挑战。以下是我在学习过程中的总结与反思。
基本锁策略
基本锁策略包括悲观锁、乐观锁、自旋锁、挂起等待锁、可重入锁、不可重入锁、公平锁、非公平锁、轻量级锁和重量级锁等。每种锁策略都有其独特的应用场景和优缺点。
CAS操作
CAS(Compare-and-Swap)操作是一种无锁算法的基本构建块,广泛应用于并发编程中。CAS操作涉及三个操作数:内存位置(V)、期望的旧值(A)和新值(B)。CAS操作会比较内存位置V的当前值与期望的旧值A是否相等,如果相等,则将V的值原子地更新为新值B;如果不相等,则操作失败,返回当前的V值。
通过对基本锁策略和CAS操作的学习,我深刻认识到在多线程环境下正确使用这些机制的重要性。每种锁策略都有其适用的场景,选择合适的锁策略对于实现高效、稳定的并发程序至关重要。此外,CAS操作作为一种无锁算法的基础,对于提高并发程序的性能和可伸缩性同样重要。
在实际应用中,我们需要根据具体的应用需求和系统特性来选择最合适的锁策略和CAS操作。此外,还需要注意锁策略之间的权衡,如性能与公平性、简单性与灵活性之间的平衡。通过不断实践和探索,我们可以更好地理解和应用这些知识,以解决实际问题。
总之,学习基本锁策略和CAS操作不仅提高了我的并发编程技能,还让我意识到了在设计并发程序时需要考虑的诸多因素。在未来的学习和工作中,我会继续深入研究这些知识点,并努力将它们应用到实践中去。
🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐
制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸