【JavaEE】锁策略和CAS

devtools/2024/9/23 7:10:49/

    🔥个人主页: 中草药

🔥专栏:【Java】登神长阶 史诗般的Java成神之路


💰一.常见的的策略

        策略(Locking Strategy)是指在多线程环境中,为了控制对共享资源的访问,确保数据一致性和线程安全,而采用的一系列机制和规则。在并发编程中,是管理共享资源访问的核心工具,它防止了多个线程同时修改同一份数据,从而避免了数据竞争和不一致性的问题。不同的策略有着不同的特性和适用场景,它们在并发控制、性能、复杂性和可扩展性方面存在差异。

1.乐观vs悲观

乐观

        乐观基于“乐观主义”假设,认为数据不太可能被并发修改,因此在读取数据时不会定数据,只有在更新数据时才检查数据是否被其他事务修改过。乐观通常使用版本号或时间戳来实现,每次更新数据时都会检查版本号是否与读取时相同,如果不同,则表明数据已经被其他事务修改,本次更新将被拒绝。

        乐观的优点在于它减少了的使用,提高了系统的并发性能,尤其适合读多写少的场景。缺点是如果多个事务同时尝试更新同一份数据,可能会导致更新失败,需要重新读取数据并再次尝试更新,这被称为“重试”。

悲观

        悲观基于“悲观主义”假设,认为数据很可能被并发修改,因此在读取或写入数据之前,会先定数据,阻止其他线程或进程的并发访问。悲观通过在事务开始时获取并在事务结束时释放来实现。常见的悲观机制包括:

  • 排他(Exclusive Locks):写操作通常需要排他,不允许任何其他读写操作同时进行。
  • 共享(Shared Locks):读操作可以获取共享,允许多个读操作同时进行,但不允许写操作。

        悲观的优点在于它能保证数据的一致性,避免了脏读和不可重复读等问题。然而,它的缺点也很明显,主要是的等待时间可能较长,容易造成死,且降低了系统的并发度。

乐观vs悲观

特征乐观(Optimistic Locking)悲观(Pessimistic Locking)
基本假设假设数据不太可能被并发修改假设数据很可能被并发修改
实现机制使用版本号或时间戳进行并发控制通过定数据防止并发修改
的使用不在读取数据时使用在读取或写入数据时使用
更新策略更新时检查版本号,如果冲突则重试更新前定,更新后释放
并发性能高,因为它减少了的使用低,因为的等待时间可能较长
数据一致性较低,可能发生重试高,定期间确保数据一致性
风险无,因为没有的等待高,尤其是在复杂的事务中
资源消耗低,较少的系统调用和上下文切换高,较多的系统调用和上下文切换
适用场景读多写少,对实时性要求较高的系统写操作频繁,对数据一致性要求极高的系统
复杂性实现上相对复杂,需要处理版本控制和冲突实现上相对简单,依赖于机制
Synchronized 初始使用乐观策略,当发现竞争比较频繁的时候,就会自动切换为悲观策略

2.轻量级vs重量级

轻量级

  • 定义:轻量级是JVM为了提高的性能而引入的一种机制,它试图在没有线程竞争的情况下避免使用重量级的开销。轻量级在Java 6之后的版本中默认启用。

  • 实现:轻量级使用了基于CAS(Compare and Swap)的原子操作。当一个线程尝试获取时,它会通过CAS操作将当前线程ID写入对象的Mark Word中。如果CAS操作成功,那么线程获得了;如果失败,则进入下一步骤。

  • 特点

    • 性能较高,因为它避免了操作系统层面的线程挂起和唤醒,减少了上下文切换的开销。
    • 竞争较少时,轻量级的效果最佳。
    • 如果在一定次数的自旋后仍未能获取,轻量级会膨胀为重量级
    • 少量内核态用户态的切换
    • 不太容易引发线程的调度

重量级

  • 定义:重量级是传统的实现,当一个线程获取重量级时,其他试图获取该的线程将被阻塞,直到被释放。

  • 实现:重量级的获取和释放涉及操作系统层面的线程挂起和唤醒,这通常需要从用户态切换到内核态,开销较大。

  • 特点

    • 性能较低,因为涉及到线程挂起和唤醒的开销。
    • 竞争激烈时,重量级可以确保数据的一致性和线程安全。
    • 当线程竞争时,重量级可以更好地保证数据的完整性,但牺牲了性能。
    • 大量内核态用户态的切换
    • 很容易引发线程的调度

轻量级vs重量级

特征轻量级(Lightweight Lock)重量级(Heavyweight Lock)
实现机制基于CAS的原子操作操作系统层面的线程挂起和唤醒
性能竞争较少时性能高竞争激烈时,性能低
上下文切换减少了上下文切换需要进行上下文切换
资源消耗相对较低相对较高
适用场景读多写少,竞争较小的场景写操作频繁,对数据一致性要求高的场景
升级竞争加剧时,轻量级可能升级为重量级无升级过程

synchronized 开始是⼀个轻量级. 如果冲突⽐较严重, 就会变成重量级

3.自旋vs挂起等待

自旋(Spin Lock)

  • 定义:当一个线程试图获取一个已经被其他线程持有的时,自旋会让当前线程在一个循环中不断检查的状态,直到变为可用状态。

  • 特点

    • 避免了线程的挂起和唤醒,减少了线程上下文切换的开销。
    • 适用于持有时间非常短的场景,因为在这种情况下,自旋等待的CPU消耗可能比线程挂起和唤醒的开销要小。
    • 如果的持有时间较长,或者竞争的线程数量很多,自旋可能会导致大量的CPU空转,浪费计算资源。

挂起等待(Sleeping Lock)

  • 定义:当一个线程试图获取一个已经被其他线程持有的时,挂起等待会让当前线程进入等待状态,直到变为可用。这通常涉及到线程的挂起和唤醒。

  • 特点

    • 减少了CPU的空转,节省了计算资源。
    • 适用于持有时间较长,或者线程竞争较为激烈的场景。
    • 线程的挂起和唤醒涉及到操作系统层面的操作,会有一定的开销,包括上下文切换。

自旋vs挂起等待

特征自旋(Spin Lock)挂起等待(Sleeping Lock)
CPU使用可能导致CPU空转,消耗CPU资源节省CPU资源,避免空转
上下文切换减少了线程的上下文切换增加了线程的上下文切换
适用场景持有时间短,竞争不激烈持有时间长,或竞争激烈
开销竞争大时CPU开销大竞争大时上下文切换开销大

4.公平vs非公平

公平

  • 定义:公平遵循先进先出(FIFO)的原则,确保请求的线程按照它们请求的顺序来获取。这意味着如果一个线程在另一个线程之前请求了,那么它将在那个线程之前获得,除非那个线程释放了

  • 特点

    • 提供了更高的公平性,避免了后请求的线程“插队”。
    • 由于必须检查等待队列中的所有线程,因此在的竞争中可能会有更高的性能开销。
    • 在线程交替请求的场景下,公平可以避免线程饥饿,即某个线程长期得不到的情况。

非公平

  • 定义:非公平不保证的获取顺序,它允许后请求的线程有可能比先请求的线程更快地获得。在默认情况下,ReentrantLock就是非公平。(后文详细介绍)

  • 特点

    • 性能通常优于公平,因为它在获取时不需要遍历等待队列,而是直接尝试获取。
    • 可能会出现线程饥饿现象,即某些线程长时间无法获取到
    • 的竞争较少的情况下,非公平的性能优势更加明显。

公平vs非公平

特征公平(Fair Lock)非公平(Unfair Lock)
获取顺序按照请求的顺序获取不保证获取顺序,可能存在“插队”现象
性能竞争激烈时性能可能较低竞争较少时性能较高
公平性高,避免线程饥饿低,可能存在线程饥饿
默认行为ReentrantLock不默认使用ReentrantLock的默认行为
应用场景竞争激烈,需避免线程饥饿竞争较少,追求高吞吐量

注意

  • 操作系统内部的线程调度就可以视为随机的,如果不做任何额外的限制,就是非公平,如果想要实现公平,就需要依赖额外的数据结构,来记录线程的先后顺序
  • 公平和非公平没有好坏之分,关键还看适用场景 

5.可重入vs不可重入

可重入(Reentrant Lock)

  • 定义:可重入允许一个线程多次获取同一把,而不会导致死。每当一个线程获取时,的计数器会递增,当该线程释放时,计数器递减,直到计数器归零,才真正被释放。

  • 特点

    • 支持递归定,即一个线程可以在已经获取的情况下再次获取
    • 避免了因递归定而导致的死问题。
    • 在多线程环境中,特别当线程需要多次进入同一临界区时,可重入提供了灵活性和安全性。

不可重入(Non-reentrant Lock)

  • 定义:不可重入不允许一个线程多次获取同一把。如果一个线程已经获取了一把,再次尝试获取这把会导致阻塞,直到被另一个线程释放。

  • 特点

    • 简化了的管理,因为不需要跟踪的嵌套级别。
    • 如果不恰当地使用递归定,不可重入可以防止死,但这也限制了它的使用场景。
    • 在单线程多次访问同一临界区的场景下,不可重入可能不如可重入灵活。

可重入vs不可重入

特征可重入(Reentrant Lock)不可重入(Non-reentrant Lock)
递归支持,允许多次获取同一把不支持,再次获取同一把会导致阻塞
预防内部机制可以避免递归定导致的死简化了管理,但限制了灵活性
灵活性高,适合复杂多线程场景低,适合简单或不需要递归定的场景
安全性高,避免了死较高,但在某些场景下可能过于限制
synchronized 是可重⼊

6.读写

读写(Read-Write Lock)是一种特殊的机制,它允许多个读操作同时进行,但写操作是独占的。读写的设计目的是为了提高并发性能,尤其是在读操作远远多于写操作的场景下。下面详细解释读写的工作原理及其在Java中的实现:

工作原理

读写维护了两把:一把读和一把写

  • :允许多个线程同时获取,只要没有线程持有写。这意味着多个线程可以同时读取共享资源,只要没有线程正在进行写操作。

  • :是独占的,意味着在任何时刻,只能有一个线程持有写。当一个线程持有写时,其他所有线程(无论是读还是写)都无法获取,直到写被释放。

特点

  • 高并发性:读写提高了读操作的并发度,因为在没有写操作时,多个读线程可以同时访问共享资源。

  • 写操作独占:写操作总是独占的,确保了数据在写入时的一致性,防止了数据竞争条件。

  • 公平性:读写可以有不同的公平性实现。有些实现保证了读写操作的公平性,即按照请求顺序获取;有些则优先考虑写操作,以避免写饿死。

Java中的实现

在Java中,读写通过java.util.concurrent.locks.ReadWriteLock接口来实现,最常见的实现是ReentrantReadWriteLockReadWriteLock接口定义了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.工作原理

  1. 加载值CAS操作开始时,线程会加载内存位置V的当前值。

  2. 比较值:将加载的值与期望的旧值A进行比较。如果两者相等,则进行下一步;如果不等,CAS操作失败。

  3. 交换值:如果比较成功,CAS操作会原子地将V的值更新为新值B。

  4. 返回结果CAS操作返回一个结果,指示操作是否成功。如果成功,通常返回新值;如果失败,返回旧值。

工作原理伪代码

下⾯写的代码不是原⼦的, 真实的 CAS 是⼀个原⼦的硬件指令完成的. 这个伪代码只是辅助理解 CAS 的⼯作流程.

java">boolean CAS(address, expectValue, swapValue) {if (&address == expectedValue) {&address = swapValue;return true;}return false;
}

3.CAS在Java中的实现

在Java中,CAS操作主要通过Unsafe类的compareAndSwapIntcompareAndSwapLongcompareAndSwapObject方法实现,这些方法提供了对底层硬件CAS操作的访问。然而,直接使用Unsafe类通常被认为是不推荐的,因为它破坏了Java的封装性和安全性。

更安全且推荐的方式是使用Java并发库中的Atomic类,如AtomicIntegerAtomicLongAtomicReference。这些类内部使用了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操作提供了原子性,但它也有其局限性:

  1. ABA问题:如果一个值被多次设置为相同的值A,CAS操作可能误以为值没有被改变。解决这个问题通常需要使用版本号或标记值。

  2. 循环时间:在多线程环境中,如果多个线程尝试同时更新同一变量,可能会导致CAS操作反复失败,从而导致线程在自旋中消耗大量CPU时间。

  3. 性能问题:在高并发场景下,CAS操作的性能可能会下降,因为失败的CAS操作需要重试,这可能导致CPU空转。

尽管如此,CAS仍然是实现无数据结构和算法的关键技术,能够显著提高并发程序的性能和可伸缩性。

💳三.相关面试

不做具体详细的拓展回答,仅做一个简答

1.你是怎么理解乐观和悲观

        悲观认为多个线程访问同⼀个共享变量冲突的概率较⼤, 会在每次访问共享变量之前都去真正加.

        乐观认为多个线程访问同⼀个共享变量冲突的概率不大,并不会真的加,而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突.

        悲观的实现就是先加(⽐如借助操作系统提供的 mutex), 获取到再操作数据. 获取不到就等待。
        乐观的实现可以引入⼀个版本号. 借助版本号识别出当前的数据访问是否冲突. 
2.介绍一下读写
读写就是把读操作和写操作分别进⾏加.
和读之间不互斥.
和写之间互斥.
和读之间互斥.
读写最主要⽤在 "频繁读, 不频繁写" 的场景中.
3.什么是自旋,为什么要使用自旋策略,缺点是什么?
如果获取失败, ⽴即再尝试获取, ⽆限循环, 直到获取到为⽌. 第⼀次获取失败, 第⼆次的尝试 会在极短的时间内到来. ⼀旦被其他线程释放, 就能第⼀时间获取到.
相⽐于挂起等待,
优点: 没有放弃 CPU 资源, ⼀旦被释放就能第⼀时间获取到, 更⾼效. 在持有时间⽐较短的场景
下⾮常有⽤.
缺点: 如果的持有时间较⻓, 就会浪费 CPU 资源.
4.讲解一下你自己了解的CAS机制
全称 Compare and swap, 即 "⽐较并交换". 相当于通过⼀个原⼦的操作, 同时完成 "读取内存, ⽐较是否相等, 修改内存" 这三个步骤. 本质上需要 CPU 指令的⽀撑.
5.ABA问题怎么解决
给要修改的数据引⼊版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期. 如 果发现当前版本号和之前读到的版本号⼀致, 就真正执⾏修改操作, 并让版本号自增; 如果发现当前版 本号比之前读到的版本号大, 就认为操作失败

💸四.总结与反思

神龟虽寿,犹有竞时。——曹操

在深入学习基本策略和CAS(Compare-and-Swap)操作的过程中,我对并发编程有了更深刻的理解。这些知识点不仅增强了我的编程技能,还帮助我更好地应对多线程环境下的挑战。以下是我在学习过程中的总结与反思。

基本策略

基本策略包括悲观、乐观、自旋、挂起等待、可重入、不可重入、公平、非公平、轻量级和重量级等。每种策略都有其独特的应用场景和优缺点。

  1. 悲观与乐观

    • 悲观假设数据很可能被并发修改,因此在读取或写入数据之前会定数据,确保数据的一致性。
    • 乐观假设数据不太可能被并发修改,因此在读取数据时不定数据,只有在更新数据时才检查数据是否被其他事务修改过。
    • 选择哪种策略取决于具体的应用场景和需求,如数据访问模式、并发程度、对数据一致性的要求等。
  2. 自旋与挂起等待

    • 自旋在尝试获取失败时不会放弃CPU,而是持续循环尝试获取,适用于持有时间短的情况。
    • 挂起等待在尝试获取失败时会释放CPU,等待可用后再获取,适用于持有时间长的情况。
    • 选择自旋还是挂起等待需要考虑的持有时间和线程竞争的程度。
  3. 可重入与不可重入

    • 可重入允许一个线程多次获取同一把,而不会导致死
    • 不可重入不允许一个线程多次获取同一把,适用于不需要递归定的简单场景。
    • 选择哪种取决于程序设计的需求,如是否存在线程需要多次进入同一临界区的情况。
  4. 公平与非公平

    • 公平按照线程请求的顺序来分配,保证了的公平性。
    • 非公平不保证获取的顺序,可能存在线程饥饿的情况。
    • 竞争激烈的情况下,公平可以避免线程饥饿,但在竞争较少的情况下,非公平的性能更高。
  5. 轻量级与重量级

    • 轻量级使用基于CASS的原子操作,在没有线程竞争的情况下避免使用重量级的开销。
    • 重量级涉及操作系统层面的线程挂起和唤醒,适用于竞争激烈的情况。
    • 选择哪种需要考虑的持有时间、线程竞争程度等因素。

CAS操作

CAS(Compare-and-Swap)操作是一种无算法的基本构建块,广泛应用于并发编程中。CAS操作涉及三个操作数:内存位置(V)、期望的旧值(A)和新值(B)。CAS操作会比较内存位置V的当前值与期望的旧值A是否相等,如果相等,则将V的值原子地更新为新值B;如果不相等,则操作失败,返回当前的V值。

  1. CAS操作的特点

    • CAS操作是原子的,能够在多线程环境下确保数据的一致性。
    • CAS操作可以用来实现无数据结构,如无队列、无栈等。
    • CAS操作也可能遇到ABA问题,即一个值被多次设置为相同的值A,可以通过使用版本号或标记值来解决这个问题。
  2. CAS操作的局限性

    • 在高并发场景下,CAS操作的性能可能会下降,因为失败的CAS操作需要重试,可能导致CPU空转。
    • CAS操作在多处理器架构中特别有用,但在单处理器系统中可能不是最佳选择。

        通过对基本策略和CAS操作的学习,我深刻认识到在多线程环境下正确使用这些机制的重要性。每种策略都有其适用的场景,选择合适的策略对于实现高效、稳定的并发程序至关重要。此外,CAS操作作为一种无算法的基础,对于提高并发程序的性能和可伸缩性同样重要。

        在实际应用中,我们需要根据具体的应用需求和系统特性来选择最合适的策略和CAS操作。此外,还需要注意策略之间的权衡,如性能与公平性、简单性与灵活性之间的平衡。通过不断实践和探索,我们可以更好地理解和应用这些知识,以解决实际问题。

        总之,学习基本策略和CAS操作不仅提高了我的并发编程技能,还让我意识到了在设计并发程序时需要考虑的诸多因素。在未来的学习和工作中,我会继续深入研究这些知识点,并努力将它们应用到实践中去。

🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀

以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐

  制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸 


http://www.ppmy.cn/devtools/96077.html

相关文章

MySQL基础练习题48-连续出现的数字

目录 题目 准备数据 分析数据 题目 找出所有至少连续出现三次的数字。 准备数据 ## 创建库 create database db; use db;## 创建表 Create table If Not Exists Logs (id int, num int)## 向表中插入数据 Truncate table Logs insert into Logs (id, num) values (1, 1) i…

LeetCode 第三十一天 2024.8.17

1. &#xff1a;打家劫舍 题目链接: 198. 打家劫舍 - 力扣&#xff08;LeetCode&#xff09; 应用条件&#xff1a;动态规划 难点&#xff1a; # 确定dp数组&#xff08;dp table&#xff09;以及下标的含义&#xff1a;dp[i]表示在i这房子能投的最高金额 # 确定递推公式: dp…

音视频开发

通过多线程分别获取高分辨率(1920 * 1080)和低分辨率(1280 * 720) 初始化VI模块 初始化HIGH VENC模块 初始化LOW VENC模块 初始化RGA模块 绑定 VI和HIGH VENC 绑定 VI和RGA 创建线程 HIGH VENC处理 RGA处理 LOW VENC处理 销毁 QP原理的讲解 QP参数调节&#xff0c;指的是量化…

DotPlot 的宽高自动设置 | 线性拟合

1. 线性模型计算width和height 输入是基因集合 scRNA.markers <- FindAllMarkers(scRNA, only.pos TRUE, min.pct 0.25, logfc.threshold 0.25) #scRNAmisc[["markers"]]scRNA.markers #scRNA.markers %>% group_by(cluster) %>% top_n(n 2, wt avg_…

xss复现

目录 反射型 Ma Spaghet! Jefff Ugandan Knuckles onfocus Ricardo Milos Ah Thats Hawt location Ligma Mafia 构造函数 dom破坏 Ok, Boomer 反射型 Ma Spaghet! <!-- Challenge --> <h2 id"spaghet"></h2> <script>spaghe…

npm install pnpm -g 报错的解决方法

npm install pnpm -g 报错的解决方法 npm error code ETIMEDOUT npm error errno ETIMEDOUT npm error network request to https://registry.npmjs.org/pnpm failed, reason: npm error network This is a problem related to network connectivity. npm error network In mo…

oracle UNPIVOT的使用

Oracle UNPIVOT是一种用于将列转换为行的SQL操作&#xff0c;它允许用户将多个列的数据转换为多行的形式&#xff0c;以便进行更灵活的数据分析和报表生成。以下是关于Oracle UNPIVOT使用的详细解释&#xff1a; 一、基本语法 Oracle UNPIVOT的基本语法如下&#xff1a; SEL…

《数据挖掘》期末考核重点

1.数据预处理的目的与形式 数据预处理的目的是提供干净&#xff0c;简洁&#xff0c;准确的数据&#xff0c;以达到简化模型和提高算法泛化能力的目的&#xff0c;使挖掘过程更有效&#xff0c;更容易&#xff0c;提高挖掘效率和准确性。 2.数据预处理的形式 数据清理&#…