常见锁策略,synchronized内部原理以及CAS

常见的锁策略

一些常见的锁策略可以帮助我们在实际开发中更合理的使用锁:

  1. 乐观锁 vs 悲观锁

    • 乐观锁:不加锁进行读取,适用于读操作频繁、写操作较少的情况,性能较高。
    • 悲观锁:读写都加锁,适用于写操作频繁的情况,保证了数据的一致性,但性能较低。
  2. 轻量级锁 vs 重量级锁

    • 轻量级锁:采用 CAS 操作尝试获取锁,适用于多线程竞争不激烈的情况,性能较高。
    • 重量级锁:多线程竞争激烈时,会将后续线程阻塞挂起,性能较低。
  3. 自旋锁 vs 传统阻塞锁

    • 自旋锁:不立即进入阻塞状态,而是采用循环等待的方式尝试获取锁,适用于锁的持有时间短、线程竞争不激烈的情况。
    • 传统阻塞锁:线程在获取锁失败时立即进入阻塞状态,直到获取到锁才能执行,适用于多线程竞争激烈、锁持有时间长的情况。
  4. 可重入锁 vs 不可重入锁

    • 可重入锁:同一个线程可以多次获得同一把锁,避免了死锁的发生,适用于需要支持递归锁的情况。
    • 不可重入锁:同一个线程多次获得同一把锁会造成死锁,适用于不需要支持递归锁的情况。
  5. 公平锁 vs 非公平锁

    • 公平锁:保证线程获取锁的顺序与线程请求锁的顺序相同,先到先得,适用于要求公平性的场景。
    • 非公平锁:不保证线程获取锁的顺序与线程请求锁的顺序相同,可能会导致某些线程长时间等待,适用于性能优先的场景。
  6. 普通互斥锁 vs 读写锁

    • 普通互斥锁:读写都加锁,同一时刻只允许一个线程访问共享资源,适用于对临界区的并发访问进行保护。
    • 读写锁:允许多个线程同时读取共享资源,但在写入时会阻塞其他线程的读写操作,适用于读操作频繁、写操作较少的情况,提高了读取性能。

synchronized内部原理

那作为Java中锁的老大哥,synchronized有哪些特点呢 :

  1. 是否为乐观锁:synchronized 关键字其乐观或者悲观会取决于当下锁冲突的激烈情况

  2. 是否为轻量级锁synchronized 在 JDK 6 之后引入了偏向锁、轻量级锁和重量级锁三种锁机制,其中的轻量级锁就是 synchronized 在竞争不激烈时的锁机制。但需要注意,synchronized 并不总是轻量级锁,具体使用哪种锁取决于竞争情况。

  3. 是否为自旋锁:synchronized 在获取锁失败时,会发生阻塞而不是自旋等待。但在 JDK 6 之后,它引入了偏向锁、轻量级锁和自旋锁,当线程在获取锁时,会先尝试自旋一段时间,如果在这个时间段内能够成功获取锁,则不会发生阻塞。

  4. 是否为普通互斥锁synchronized 是一种普通的互斥锁,即同一时刻只允许一个线程访问共享资源,其他线程需要等待释放锁后才能访问。

  5. 是否为可重入锁synchronized 是可重入锁,同一个线程可以多次获取同一把锁,而不会发生死锁。

  6. 是否为公平锁synchronized 关键字默认情况下是非公平锁,即不保证等待队列中的线程获取锁的顺序。但可以使用 synchronized 关键字的特定构造形式来实现公平锁。

  7. 内部原理

    • 锁的升级synchronized 在 JDK 6 之后引入了偏向锁、轻量级锁和重量级锁三种锁机制,会根据竞争情况进行锁的升级。
    • 锁的消除:JVM 在 JIT 编译过程中会对部分同步代码块进行逃逸分析,如果发现该锁对象不可能被其他线程访问,就会消除这些锁操作。
    • 锁的粗化:JVM 会对连续的独占锁进行粗化优化,将多个连续的加锁解锁操作合并为一次锁操作,减少锁的竞争。

总的来说,synchronized 关键字是 Java 中用于实现线程同步的基本手段,它具有悲观锁、普通互斥锁、可重入锁等特性,同时在 JDK 6 之后引入了偏向锁、轻量级锁和重量级锁等高级锁机制,通过锁的升级、消除和粗化等优化手段,提高了并发性能。

CAS

CAS(Compare And Swap)是一种乐观锁的实现方式,它是一种原子操作,用于实现多线程环境下的并发控制。CAS 操作包含三个操作数:内存地址 V、旧的预期值 A 和新值 B。如果当前内存地址 V 的值与预期值 A 相等,则将内存地址 V 的值更新为新值 B,否则不做任何操作。CAS 操作通常是通过底层硬件提供的原子指令(CPU指令)来实现的,比如 Intel 处理器提供的 cmpxchg 指令。

CAS 的特点包括:

  1. 原子性:CAS 是一种原子操作,能够确保在多线程环境下的并发安全性。
  2. 无阻塞:CAS 操作是非阻塞的,即不会因为竞争而阻塞线程,而是通过不断重试直到成功或者达到重试次数上限。
  3. 无锁:CAS 不需要加锁,因此减少了线程竞争时的上下文切换开销和线程间的同步等待时间。
  4. 自旋次数限制:为了避免出现长时间自旋导致性能下降,通常会对 CAS 操作的自旋次数进行限制,超过限制后将会采取其他方式来处理并发冲突。

CAS 常用于实现无锁数据结构、并发算法以及原子操作类等,在 Java 中 Atomic 系列的类(比如 AtomicIntegerAtomicReference 等)就是基于 CAS 实现的原子操作类。CAS 也是很多并发框架和库的底层实现基础,比如 Java 中的 java.util.concurrent 包中的并发工具类。其本身位于unsafe这个类中,我们一般不直接使用。在 Java 中,java.util.concurrent.atomic 包提供了一系列原子类,这些类封装了 CAS 操作,提供了一种线程安全的方式来更新共享变量的值,而不需要显式地使用锁。

以下是一些常用的原子类及其功能:

  1. AtomicInteger:提供了对整型变量的原子操作,比如增加、减少、获取当前值等。
  2. AtomicLong:提供了对长整型变量的原子操作,类似于 AtomicInteger。
  3. AtomicReference:提供了对引用类型变量的原子操作,可以原子性地设置和获取引用类型的值。
  4. AtomicBoolean:提供了对布尔型变量的原子操作,支持原子性地设置和获取布尔型的值。
  5. AtomicIntegerArray:提供了对整型数组的原子操作,可以原子性地更新数组中的元素值。
  6. AtomicLongArray:提供了对长整型数组的原子操作,类似于 AtomicIntegerArray。
  7. AtomicReferenceArray:提供了对引用类型数组的原子操作,类似于 AtomicIntegerArray。

这些原子类都是通过底层的 CAS 操作实现的,保证了对共享变量的操作是线程安全的,不需要额外的同步措施。这在并发编程中非常有用,可以提高程序的性能和可伸缩性,同时减少了编写线程安全代码的复杂性。

我将结合AtomicInteger中的getAndIncrement方法的伪代码来具体阐述一下其如何实现不加锁,但不破环线程安全的:

class AtomicInteger {
    private volatile int value;//模拟内存中的值

    // 获取当前值并递增
    public int getAndIncrement() {
        // 读取当前值
        int oldValue = value;//oldvalue为寄存器上的值
        
        // 使用 CAS 操作递增值
        while (!compareAndSwap(value,oldValue, oldValue + 1)) {
            // CAS 操作失败,重新读取当前值
            oldValue = value;
        }

        // 返回旧值
        return oldValue;
    }

    // CAS 操作实现递增
    private boolean compareAndSwap(int value,int expect, int update) {
        //value 位于内存,expect与update位于寄存器中
        // 利用底层硬件提供的原子指令实现 CAS 操作
        if (expect == value) {
            value = update;
            return true;
        } else {
            return false;
        }
    }
}

在这段代码中,getAndIncrement() 方法首先读取当前值,然后通过 compareAndSwap() 方法尝试将当前值递增。如果CAS操作失败,说明在读取当前值后,其他线程已经修改了该值,因此会重新读取当前值并再次尝试递增,直到成功为止。这种重试的过程保证了递增操作的原子性。因为在 compareAndSwap() 方法中,对 value 的修改是原子的,即在同一时刻只有一个线程能够修改成功。

所以,尽管多个线程同时调用 getAndIncrement() 方法,但由于 CAS 操作的原子性,每个线程都能够正确地获取到独一无二的递增值,不会出现数据不一致的情况。

CAS的ABA问题

ABA问题是在并发编程中经常遇到的一种情况,它指的是在执行CAS操作时,由于多线程的并发执行,导致一个值从A变成了B,再变回A,而检测这个值的线程在执行CAS时仍然认为这个值没有发生变化。虽然值在中间的过程发生了变化,但是在最终的状态上仍然和原始值相同,这就是ABA问题。

下面是一个典型的ABA问题的场景:

  1. 假设某个线程T1读取了一个共享变量的值为A。
  2. 然后线程T1被挂起,另一个线程T2将共享变量的值从A改为B,然后再改回A。
  3. 线程T1恢复执行,并且进行CAS操作,认为共享变量的值仍然是A,并执行成功。

在这个过程中,虽然共享变量的值在中间的过程发生了变化,但是最终的状态和线程T1最初读取的值A相同,因此线程T1的CAS操作会成功,但实际上共享变量的状态已经发生了变化,可能会引发意想不到的问题。

一个经典的例子是在使用CAS操作来实现线程安全的栈时可能出现的问题:

假设有一个栈,初始状态为空,某个线程T1想要向栈中压入元素A,另一个线程T2想要弹出栈顶元素。下面是可能导致ABA问题的具体步骤:

  1. 初始状态下,栈为空。
  2. 线程T2读取栈顶元素为null,准备执行弹出操作。
  3. 线程T1执行CAS操作,向栈中压入元素A。此时栈的状态为A -> null。
  4. 线程T2执行CAS操作,将栈顶元素弹出。由于CAS操作只能比较当前值是否与预期值相等,但并不能检测到值的变化过程,所以CAS操作成功,栈顶元素为A,弹出成功。
  5. 线程T1又将栈顶元素弹出,此时栈为空。

在上述过程中,线程T2执行了两次弹出操作,虽然在第一次弹出操作时CAS操作成功,但实际上栈的状态在期间发生了变化(由A -> null),然后又变回了A。这就是ABA问题。虽然在最终状态上栈顶元素和预期值相同,但实际上栈的状态已经发生了变化,导致了意外的结果

 为了解决ABA问题,通常可以使用版本号或者标记位等方法来辅助CAS操作,确保在进行CAS操作时不仅比较值是否相等,还要比较版本号或者标记位等附加信息,从而避免了ABA问题的发生。

你可能感兴趣的:(java,算法,开发语言)