草庐IT

【多线程进阶】锁策略和CAS面试题

书生-w 2024-05-18 原文

🎉🎉🎉点进来你就是我的人了
博主主页:🙈🙈🙈戳一戳,欢迎大佬指点!

欢迎志同道合的朋友一起加油喔🦾🦾🦾​​​​​​​


目录

1.乐观锁vs悲观锁

1.1 悲观锁

1.2 乐观锁

2.重量级锁vs轻量级锁

2.1 轻量级锁

2.2 重量级锁

3.自旋锁VS挂起等待锁

3.1 自旋锁

3.2 挂起等待锁

4.互斥锁VS读写锁

4.1 互斥锁

4.2 读写锁

5.可重入锁VS不可重入锁

5.1 可重入锁

5.2 不可重入锁

6.CAS

6.1 实现原子类:

6.2 实现自旋锁:

7.面试题,CAS的ABA问题怎么解决



1.乐观锁vs悲观锁

Java中的乐观锁和悲观锁是两种并发控制的策略,用于解决多线程访问共享资源时可能出现的竞争和冲突问题。

1.1 悲观锁

悲观锁的思想是,每次访问共享资源时都假定其他线程可能同时访问该资源,因此会对该资源进行加锁保护。在Java中,synchronized、ReentrantLock等内置锁就是悲观锁的实现。当一个线程获取到锁后,其他线程必须等待锁的释放才能访问该资源,从而保证了资源的独占性和数据的一致性。悲观锁的缺点是,对于高并发场景,频繁地获取和释放锁会导致性能瓶颈和系统负担增加,同时也可能导致死锁等问题。

1.2 乐观锁

乐观锁的思想是,每次访问共享资源时都假定其他线程不会同时访问该资源,因此不对该资源进行加锁保护,而是通过版本号、时间戳等机制来检测数据是否被其他线程修改过。如果检测到数据已经被修改过,则会进行回滚或重试等操作。在Java中,Atomic系列的原子类和StampedLock就是乐观锁的实现。乐观锁的优点是,避免了频繁地获取和释放锁,提高了并发性能,但是需要保证并发修改的场景较少,且对数据的一致性有一定的影响。

总的来说,悲观锁适用于并发写入比较多的场景,可以有效地保证数据的一致性,但是在高并发的情况下可能会影响性能。乐观锁适用于并发读取比较多的场景,可以提高并发性能,但是在并发修改较多的情况下可能会导致数据一致性问题。因此,在实际应用中需要根据具体的业务场景和性能要求来选择合适的锁策略。


2.重量级锁vs轻量级锁

Java中的轻量级锁和重量级锁是两种内置锁的实现方式,用于控制并发访问共享资源的情况。

2.1 轻量级锁

轻量级锁是一种基于对象头的锁实现方式,主要用于优化低竞争情况下的锁性能。当一个线程尝试获取对象的锁时,如果该对象没有被其他线程锁定,则该线程将对象头中的标志位改为“轻量级锁标志”,并将对象头中存储的线程ID更新为当前线程的ID。如果该对象已经被其他线程锁定,则该线程会自旋等待锁的释放。在自旋的过程中,如果其他线程已经释放了锁,则当前线程可以直接获取锁,否则就会膨胀为重量级锁。

2.2 重量级锁

重量级锁是一种基于操作系统互斥量的锁实现方式,主要用于高竞争情况下的锁性能。当一个线程尝试获取对象的锁时,如果该对象已经被其他线程锁定,则该线程会进入阻塞状态,直到其他线程释放了锁,该线程才能继续执行。重量级锁的实现涉及到操作系统内核的系统调用,因此在高并发情况下会产生较大的系统开销和资源消耗。

总的来说,轻量级锁适用于低竞争情况下的并发访问,可以有效地提高锁的性能,但在高并发情况下容易退化为重量级锁。重量级锁适用于高竞争情况下的并发访问,可以保证数据的正确性,但会带来较大的系统开销和资源消耗。因此,在实际应用中需要根据具体的业务场景和并发情况来选择合适的锁实现方式


3.自旋锁VS挂起等待锁

Java中的自旋锁和挂起等待锁是两种锁的实现方式,用于控制并发访问共享资源的情况。

3.1 自旋锁

自旋锁是一种基于忙等待的锁实现方式,当一个线程尝试获取锁时,如果该锁已经被其他线程占用,则该线程会不断地循环检测锁是否被释放,直到获取到锁为止。在Java中,synchronized关键字和ReentrantLock等内置锁都是自旋锁的实现。自旋锁的优点是可以避免线程的阻塞和切换,因此对于锁的竞争不是非常激烈的情况下,自旋锁可以提供较好的性能表现。但是,当锁的竞争非常激烈的时候,自旋锁的忙等待可能会浪费CPU时间,导致性能下降。

3.2 挂起等待锁

挂起等待锁是一种基于线程挂起的锁实现方式,当一个线程尝试获取锁时,如果该锁已经被其他线程占用,则该线程会被挂起等待锁的释放。在Java中,Object类中的wait()、notify()和notifyAll()方法就是基于挂起等待的锁实现方式。挂起等待锁的优点是可以避免线程的忙等待,节省CPU资源,同时也可以防止锁的竞争过于激烈,从而保证程序的稳定性。但是,挂起等待锁的缺点是在线程挂起和恢复的过程中,需要进行线程的切换和上下文切换,这些操作会带来一定的系统开销和性能下降。

总的来说,自旋锁适用于锁的竞争不是非常激烈的情况下,可以提高性能而挂起等待锁适用于锁的竞争比较激烈的情况下,可以保证程序的稳定性。在实际应用中,需要根据具体的业务场景和并发情况来选择合适的锁实现方式。


4.互斥锁VS读写锁

Java中的互斥锁和读写锁是两种常见的锁的实现方式,用于控制并发访问共享资源的情况。

4.1 互斥锁

互斥锁是一种最基本的锁实现方式,也是最常见的锁实现方式之一。当一个线程获取到互斥锁时,其他线程就无法再获取到该锁,直到该线程释放锁。在Java中,synchronized关键字和ReentrantLock等内置锁都是互斥锁的实现。互斥锁的优点是能够保证数据的一致性,但缺点是会带来较大的性能开销,因为每次获取锁时,需要进行线程的阻塞和上下文切换。

4.2 读写锁

读写锁是一种针对读写操作的锁实现方式,相对于互斥锁,读写锁可以实现更细粒度的并发控制。读写锁允许多个线程同时进行读操作,但只允许一个线程进行写操作。在Java中,ReentrantReadWriteLock是一种常用的读写锁实现方式。读写锁的优点是能够提高读操作的并发性能,从而提高程序的吞吐量,但缺点是可能会出现写饥饿问题,即写操作无法获取到锁,导致长时间等待,降低程序的响应性。

读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写 锁.

  • ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁操作

  • ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进 行加锁解锁.

    这两个是操作系统内核提供的API在Java里进行封装的,系统API再底层的实现就是CPU指令级别了

其中,

  • 读加锁和读加锁之间, 不互斥.

  • 写加锁和写加锁之间, 互斥.

  • 读加锁和写加锁之间, 互斥.

注意, 只要是涉及到 “互斥”, 就会产生线程的挂起等待. 一旦线程挂起, 再次被唤醒就不知道隔了多久了.

因此尽可能减少 “互斥” 的机会, 就是提高效率的重要途径.

读写锁特别适合于 “频繁读, 不频繁写” 的场景中. (这样的场景其实也是非常广泛存在的).

总结,在使用读写锁时,需要根据具体的业务场景和性能要求来进行权衡和选择。如果读操作远远多于写操作,并且数据的一致性要求不是非常高,那么可以考虑使用读写锁来提高程序的性能表现。如果读操作和写操作比较均衡,并且数据的一致性要求比较高,那么可能需要使用互斥锁来保证数据的正确性。


5.可重入锁VS不可重入锁

Java中的锁可以分为可重入锁和不可重入锁两种类型。

5.1 可重入锁

可重入锁是指同一个线程可以多次获取同一把锁而不会被阻塞,即线程可以重复地获取已经持有的锁,而不必担心死锁。可重入锁通常是通过一个计数器来记录锁的持有次数,每次加锁计数器加1,解锁计数器减1,当计数器为0时,锁被完全释放。Java中的synchronized关键字和ReentrantLock类都是可重入锁。

5.2 不可重入锁

不可重入锁是指同一个线程不能重复获取已经持有的锁,否则会被阻塞。不可重入锁通常需要在锁内部记录锁的持有线程,当线程再次尝试获取锁时,锁会判断是否是当前持有锁的线程,如果是,则允许获取锁,否则会被阻塞。Java中的普通的Lock接口的实现类,如不可重入锁ReentrantLock的父类Sync,以及Semaphore类都是不可重入锁。

需要注意的是,虽然可重入锁允许同一线程多次获取锁,但需要注意锁的释放顺序,避免出现死锁情况。而不可重入锁虽然不会出现死锁情况,但也需要注意线程的获取和释放锁的顺序,否则可能会出现资源竞争和死锁问题。

在实际应用中,需要根据具体的业务场景和性能要求来选择使用可重入锁还是不可重入锁,以确保程序的正确性和性能。


6.CAS

CAS(Compare and Swap,比较并交换)是一种基于原子操作的内存并发控制方式,是实现乐观锁的一种方式。CAS机制通常由硬件指令提供支持,但是在Java中,CAS机制是通过sun.misc.Unsafe类提供的一些本地方法实现的。

CAS机制包含三个参数:内存位置V,预期原值A,新值B。当且仅当V的值等于A时,CAS操作才会通过原子方式将V的值修改为B。如果V的值不等于A,那么CAS操作将不会执行任何操作,并且会返回V的当前值。

在Java中,CAS操作主要由java.util.concurrent.atomic包提供的原子类来实现。这些原子类提供了一系列基于CAS机制的线程安全的原子操作,包括原子加、原子减、原子更新等操作。这些原子类通过使用CAS机制,可以避免了锁机制的使用,从而提高了并发性能。

CAS机制的优点是:无锁化的实现方式,避免了锁机制的使用,可以避免由于锁竞争导致的线程挂起、唤醒等操作,从而提高了系统的并发性能。此外,CAS操作不会阻塞其他线程的访问,可以提高线程的响应速度。

但是,CAS机制也存在一些缺点。首先,CAS机制需要在循环中不断地进行CAS操作,直到成功为止,这可能会引起ABA问题。其次,CAS机制只能针对一个变量进行原子操作,如果需要对多个变量进行原子操作,就需要使用锁机制来保证操作的原子性。最后,CAS机制的实现依赖于CPU硬件支持,不同的CPU对于CAS操作的支持程度不同,可能会影响CAS机制的效率。

CAS(Compare and Swap,比较并交换)操作包含三个参数:内存位置V,预期原值A,新值B。具体的操作流程如下:

  1. 首先,CAS操作会先获取内存位置V的当前值,记为C。

  2. 接着,CAS操作会判断当前内存位置V的值是否等于预期原值A,如果相等,那么说明内存位置V的值没有被其他线程修改过,可以进行修改操作,否则说明内存位置V的值已经被其他线程修改过,不能进行修改操作,直接返回。

  3. 如果内存位置V的值等于预期原值A,那么CAS操作会使用新值B来替换内存位置V的当前值C。

  4. 在CAS操作过程中,由于CAS操作是原子的,因此只有一个线程能够成功执行CAS操作,其他线程会失败,但不会阻塞,而是重新开始执行CAS操作,直到成功为止。

需要注意的是,CAS操作涉及到CPU和内存之间的交互,而CPU和内存之间的交互需要通过寄存器来完成在执行CAS操作时,预期原值A通常会被加载到寄存器中,然后和内存位置V中的值进行比较,如果相等,则将新值B写入内存位置V中,否则重新从内存位置V中加载当前的值,并继续执行比较操作,直到比较成功为止。因此,可以说寄存器在CAS操作中扮演了一个重要的角色,但预期原值A并不是寄存器中的值

6.1 实现原子类:

在Java中,可以使用java.util.concurrent.atomic包提供的原子类来实现基于CAS机制的线程安全的原子操作。这些原子类包括AtomicInteger、AtomicLong、AtomicBoolean等。这些原子类提供了一系列基于CAS机制的线程安全的原子操作,包括原子加、原子减、原子更新等操作。这些原子类通过使用CAS机制,可以避免了锁机制的使用,从而提高了并发性能。

具体来说,原子类的实现方式通常是将要操作的变量作为一个volatile类型的成员变量,然后使用CAS机制来保证对该变量的原子操作。在执行原子操作时,先获取变量的当前值,然后通过CAS机制比较当前值和预期值是否相等,如果相等,则使用新值替换当前值,否则重新获取当前值并重复以上步骤,直到操作成功为止。

典型的就是 AtomicInteger 类.,下面代码演示AtomicInteger的使用:

public class AtomiclntegerDemo {
    private static int number = 0;
    private static AtomicInteger atomicInteger = new AtomicInteger(0);
 
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i <=100000; i++) {
                //number++;
                atomicInteger.getAndIncrement();//++i 
                //AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.
            }
        });
        thread.start();
        thread.join();
        System.out.println("最终结果" + atomicInteger.get());
    }
}
 

6.2 实现自旋锁:

自旋锁是一种基于忙等待的锁机制,它可以避免线程上下文切换所带来的性能损失,从而提高系统的并发性能。自旋锁的实现方式通常是使用一个标志位来表示锁的状态,当需要获取锁时,线程会不断地检查标志位的值,如果标志位为0,则将其设置为1,并执行临界区代码,否则线程将继续等待。

基于CAS机制实现自旋锁的方式是,将锁状态作为一个volatile类型的变量,并使用CAS机制来进行原子操作。当需要获取锁时,线程会先使用CAS机制将锁状态设置为1,如果设置成功,则表示获取锁成功,可以执行临界区代码;否则表示锁已被其他线程占用,线程将继续自旋等待。当线程释放锁时,只需要将锁状态设置为0即可。

需要注意的是,基于CAS机制实现的自旋锁可能会导致CPU资源的浪费,因为当线程不断地自旋等待时,会占用CPU资源,导致CPU利用率降低。因此,在实际应用中,应该根据实际情况来选择合适的锁机制,以充分利用CPU资源,提高系统的并发性能。


7.面试题,CAS的ABA问题怎么解决

ABA问题是指在使用CAS操作进行比较-交换时,如果变量在操作期间被修改了两次及以上,那么CAS操作可能会误判。为了解决ABA问题,可以引入版本号机制,即在变量值的基础上增加一个版本号,每次操作时都需要比较变量的值和版本号

具体来说,解决ABA问题的方式是,将要操作的变量和一个版本号一起打包成一个对象,称为版本号对象。在进行CAS操作时,不仅需要比较变量的值,还需要比较版本号。如果变量的值和版本号都与期望值相等,才会进行CAS操作,否则CAS操作失败。每次CAS操作成功后,都需要将版本号加一,从而防止下一次出现相同的值。

举个例子来说,假设有两个线程A和B同时对一个变量进行CAS操作。开始时,变量的值为1,版本号为1。线程A首先将变量的值从1修改为2,同时版本号加一变成2,然后将变量的值又修改为1,版本号再次加一变成3。此时,线程B也尝试对变量进行CAS操作,将变量的值从1修改为3,并将版本号设置为2。由于线程B对变量的修改版本号不同于线程A最后的版本号3,因此CAS操作失败,从而避免了ABA问题的发生。

需要注意的是,引入版本号虽然可以解决ABA问题,但也会带来额外的开销。每次操作都需要更新版本号,因此在高并发场景下可能会影响性能。因此,在实际应用中,需要根据具体情况来考虑是否引入版本号机制来解决ABA问题。

以下是一个使用AtomicStampedReference解决ABA问题的示例代码:

import java.util.concurrent.atomic.AtomicStampedReference;

public class CASWithVersionExample {
    private static final AtomicStampedReference<Integer> count = new AtomicStampedReference<>(1, 1);

    public static void main(String[] args) {
        // 创建两个线程
        Thread thread1 = new Thread(() -> {
            int stamp = count.getStamp(); // 获取初始版本号
            int oldValue = count.getReference(); // 获取初始值
            System.out.println("Thread 1: old value is " + oldValue + ", old stamp is " + stamp);

            // 模拟线程执行过程中被其他线程干扰,使得 oldValue 被修改
            count.compareAndSet(oldValue, 2, stamp, stamp + 1); // 将变量的值由1改为2
            System.out.println("Thread 1: new value is " + count.getReference() + ", new stamp is " + count.getStamp());

            // 模拟操作完成之后,变量值又被改回1
            count.compareAndSet(2, 1, count.getStamp(), count.getStamp() + 1); // 将变量的值由2改回1
            System.out.println("Thread 1: latest value is " + count.getReference() + ", latest stamp is " + count.getStamp());
        });

        Thread thread2 = new Thread(() -> {
            int stamp = count.getStamp(); // 获取初始版本号
            int oldValue = count.getReference(); // 获取初始值
            System.out.println("Thread 2: old value is " + oldValue + ", old stamp is " + stamp);

            // 模拟操作过程中,变量的值被其他线程改为了2
            count.compareAndSet(oldValue, 3, stamp, stamp + 1); // 将变量的值由1改为3,此时线程A已经将变量的值由1改为2
            System.out.println("Thread 2: new value is " + count.getReference() + ", new stamp is " + count.getStamp());
        });

        thread1.start();
        thread2.start();
    }
}

在这个例子中,我们使用了AtomicStampedReference类来实现带版本号的CAS操作。首先,在main方法中创建了一个AtomicStampedReference对象,它包含了一个初始值1和一个初始版本号1。然后,创建了两个线程,模拟了ABA问题的发生过程:

  • 线程1首先获取变量的初始值1和初始版本号1,然后将变量的值修改为2,版本号加一,接着将变量的值又改回1,版本号再加一。
  • 线程2获取变量的初始值1和初始版本号1,然后将变量的值修改为3,此时线程1已经将变量的值由1改为2,版本号也从1变成了2,因此线程2的CAS操作会失败。

从输出结果中可以看到,线程1的CAS操作成功了两次,而线程2的CAS操作失败了一次。这就是因为我们使用了带版本号的CAS操作,它可以有效地防止ABA问题的发生。


有关【多线程进阶】锁策略和CAS面试题的更多相关文章

  1. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  2. Hive SQL 五大经典面试题 - 2

    目录第1题连续问题分析:解法:第2题分组问题分析:解法:第3题间隔连续问题分析:解法:第4题打折日期交叉问题分析:解法:第5题同时在线问题分析:解法:第1题连续问题如下数据为蚂蚁森林中用户领取的减少碳排放量iddtlowcarbon10012021-12-1212310022021-12-124510012021-12-134310012021-12-134510012021-12-132310022021-12-144510012021-12-1423010022021-12-154510012021-12-1523.......找出连续3天及以上减少碳排放量在100以上的用户分析:遇到这类

  3. ruby - 如何让Ruby捕获线程中的语法错误 - 2

    我正在尝试使用ruby​​编写一个双线程客户端,一个线程从套接字读取数据并将其打印出来,另一个线程读取本地数据并将其发送到远程服务器。我发现的问题是Ruby似乎无法捕获线程内的错误,这是一个示例:#!/usr/bin/rubyThread.new{loop{$stdout.puts"hi"abc.putsefsleep1}}loop{sleep1}显然,如果我在线程外键入abc.putsef,代码将永远不会运行,因为Ruby将报告“undefinedvariableabc”。但是,如果它在一个线程内,则没有错误报告。我的问题是,如何让Ruby捕获这样的错误?或者至少,报告线程中的错误?

  4. ruby - 如何在 ruby​​ 中运行后台线程? - 2

    我是ruby​​的新手,我认为重新构建一个我用C#编写的简单聊天程序是个好主意。我正在使用Ruby2.0.0MRI(Matz的Ruby实现)。问题是我想在服务器运行时为简单的服务器命令提供I/O。这是从示例中获取的服务器。我添加了使用gets()获取输入的命令方法。我希望此方法在后台作为线程运行,但该线程正在阻塞另一个线程。require'socket'#Getsocketsfromstdlibserver=TCPServer.open(2000)#Sockettolistenonport2000defcommandsx=1whilex==1exitProgram=gets.chomp

  5. ruby - Rails 开发服务器、PDFKit 和多线程 - 2

    我有一个使用PDFKit呈现网页的pdf版本的Rails应用程序。我使用Thin作为开发服务器。问题是当我处于开发模式时。当我使用“bundleexecrailss”启动我的服务器并尝试呈现任何PDF时,整个过程会陷入僵局,因为当您呈现PDF时,会向服务器请求一些额外的资源,如图像和css,看起来只有一个线程.如何配置Rails开发服务器以运行多个工作线程?非常感谢。 最佳答案 我找到的最简单的解决方案是unicorn.geminstallunicorn创建一个unicorn.conf:worker_processes3然后使用它:

  6. ruby - Ruby 1.9.1 中的 native 线程,对我有什么好处? - 2

    所以,Ruby1.9.1现在是declaredstable.Rails应该与它一起工作,并且正在慢慢地将gem移植到它。它具有native线程和全局解释器锁(GIL)。自从GIL到位后,原生线程是否比1.9.1中的绿色线程有任何优势? 最佳答案 1.9中的线程是原生的,但它们被“放慢了速度”,一次只允许一个线程运行。这是因为如果线程真的并行运行,它会混淆现有代码。优点:IO现在在线程中是异步的。如果一个线程阻塞在IO上,那么另一个线程将继续执行直到IO完成。C扩展可以使用真正的线程。缺点:任何非线程安全的C扩展都可能存在使用Thre

  7. ruby - 使写入文件线程安全 - 2

    我在一个ruby​​文件中有一个函数可以像这样写入一个文件File.open("myfile",'a'){|f|f.puts("#{sometext}")}这个函数在不同的线程中被调用,使得像上面这样的文件写入不是线程安全的。有谁知道如何以最简单的方式使这个文件写入线程安全?更多信息:如果重要的话,我正在使用rspec框架。 最佳答案 您可以通过File#flock给锁File.open("myfile",'a'){|f|f.flock(File::LOCK_EX)f.puts("#{sometext}")}

  8. ruby-on-rails - 覆盖 Controller 中的 protect_from_forgery 策略 - 2

    我想使用两种不同的protect_from_forgery策略构建一个Rails应用程序:一种用于Web应用程序,一种用于API。在我的应用程序Controller中,我有这行代码:protect_from_forgerywith::exception为了防止CSRF攻击,它工作得很好。在我的API命名空间中,我创建了一个继承self的应用程序Controller的api_controller,它是API命名空间中所有其他Controller的父类,我将上面的代码更改为:protect_from_forgery:null_session.遗憾的是,我在尝试发出POST请求时遇到错误:“

  9. Ruby 线程与 Watir - 2

    我编写了几个类来控制我想如何处理多个网站,两者都使用类似的方法(即登录、刷新)。每个类都打开自己的WATIR浏览器实例。classSite1definitialize@ie=Watir::Browser.newenddeflogin@ie.goto"www.blah.com"endend无线程的main中的代码示例如下require'watir'require_relative'site1'agents=[]agents这工作正常,但在当前代理完成登录之前不会移动到下一个代理。我想合并多线程来处理这个问题,但似乎无法让它工作。require'watir'require_relative

  10. ruby - 在多个线程中引用类方法会导致自动加载循环依赖崩溃 - 2

    代码:threads=[]Thread.abort_on_exception=truebegin#throwexceptionsinthreadssowecanseethemthreadseputs"EXCEPTION:#{e.inspect}"puts"MESSAGE:#{e.message}"end崩溃:.rvm/gems/ruby-2.1.3@req/gems/activesupport-4.1.5/lib/active_support/dependencies.rb:478:inload_missing_constant':自动加载常量MyClass时检测到循环依赖稍加研究后,

随机推荐