Java并发知识点总结(二)

127 阅读10分钟

1. AQS(AbstractQueuedSynchronizer)

1. 概述

AQS是 Java 并发编程中的一个抽象类,它提供了实现同步器的基本框架和算法。它是许多同步器的基础,例如 ReentrantLock、Semaphore 和 CountDownLatch 等。

AQS的作用是定义了一种同步机制,允许多个线程在访问共享资源时进行协调和同步。它基于等待/通知模式,其中等待线程会进入等待队列,直到满足特定的条件,然后才能获取资源或执行特定操作。

2. 工作流程

  1. AQS 内部维护了一个等待队列(CLH 队列),用于存储等待获取锁的线程。每个等待线程都被封装为一个节点(Node)对象,并按照先进先出的顺序排列在队列中。
  2. 当一个线程尝试获取锁时,如果发现锁已被占用,它将创建一个节点对象,并将其加入等待队列的尾部,并进入等待状态。
  3. 当锁的释放操作发生时,即持有锁的线程调用了 release() 方法释放锁时,AQS 将会尝试将锁分配给队列中的下一个线程。
  4. AQS 会从队列的头部开始,寻找一个能够获取锁的线程(根据具体的同步策略)。如果找到了这样的线程,AQS 会通过唤醒操作(unpark)将其从等待状态转换为可运行状态。
  5. 被唤醒的线程会重新尝试获取锁。如果获取成功,它将从等待队列中移除,并继续执行。如果获取失败,它将再次进入等待状态,直到再次被唤醒并成功获取锁。
  6. 当线程成功获取锁后,它将执行相应的同步操作,完成后释放锁,将锁分配给下一个等待线程。

学习资料:

Java并发AQS详解

一行一行源码分析清楚AbstractQueuedSynchronizer

2. ReentrantLock

1. 概述

ReentrantLock 是 Java 并发包(java.util.concurrent)中提供的一种可重入锁(Reentrant Lock),它扩展了内置的synchronized关键字,提供了更多的功能和灵活性。以下是 ReentrantLock 的主要特点和用法介绍:

  1. 可重入性:ReentrantLock 具备可重入性,即同一个线程可以重复获取同一把锁,而不会发生死锁。这意味着线程可以多次调用 lock() 方法,每次成功获取锁,然后通过相应次数的 unlock() 方法来释放锁。

  2. 公平性:ReentrantLock 支持公平性保证,通过在构造函数中传入 true,可以创建一个公平锁,它会按照线程的请求顺序来分配锁的获取权限。公平锁避免了线程饥饿现象,但可能会降低整体吞吐量。

  3. 锁的获取与释放:使用 ReentrantLock,线程可以显式地调用 lock() 方法获取锁,并通过 unlock() 方法释放锁。通常使用 try-finally 语句块来确保在获取锁后一定会释放锁,以避免锁泄漏。

  4. 可中断的锁等待:ReentrantLock 提供了可中断的锁等待机制。通过调用 lockInterruptibly() 方法,线程可以在等待锁的过程中响应中断,并选择是否继续等待或放弃锁的获取。

  5. 条件变量:ReentrantLock 提供了与之关联的 Condition 接口的实现,可以通过 newCondition() 方法创建条件变量。条件变量可以用于实现线程间的协作和通信,通过 await()signal() 等方法,线程可以在满足特定条件前等待或唤醒其他线程。

  6. 可选择的非公平性:ReentrantLock 默认使用非公平策略来分配锁的获取权限,这样可以获得更高的吞吐量。但也可以通过在构造函数中传入 false 来创建一个非公平锁,它不保证按照请求顺序分配锁。

总的来说,ReentrantLock 提供了比内置的synchronized关键字更多的功能和灵活性。它是一种可重入的、可中断的、可公平的锁,同时支持条件变量和可选择的非公平性。ReentrantLock 在需要更高级别的锁控制和线程间协作的场景下非常有用,并可以提供更细粒度的线程同步和控制。

3. Condition

  • 在 Java 并发包中,常用的 Condition 实现类是 AbstractQueuedSynchronizer.ConditionObject。它是在 AbstractQueuedSynchronizer(AQS)类中定义的一个内部类,用于与 ReentrantLock 的实例关联并实现线程的等待和唤醒操作。

  • Condition 内部维护了一个 等待队列(基于链表实现的单向队列),所有调用condition.await方法的线程会加入到等待队列中,并且线程状态转换为等待状态。

  • 调用condition的signal或者signalAll方法可以将等待队列中等待时间最长的节点(头节点)移动到AQS的同步队列中,使得该节点能够有机会获得lock。

  • Condition 是依赖于 ReentrantLock 的(准确说应该是ConditionObject依赖Lock),不管是调用 await 进入等待还是 signal 唤醒,都必须获取到锁才能进行操作,否则会报错。

4. CAS(Compare and Swap)

1. 基本概念

  • CAS是一种并发算法,用于实现原子操作。它是一种乐观并发控制机制,用于解决多线程环境下共享变量的并发访问问题。

  • CAS操作包含三个基本操作:比较(Compare)、读取(Read)和替换(Swap)。它的基本思想是,先比较共享变量的当前值与期望值是否相等,如果相等,则进行替换操作,否则不做任何操作。

  • CAS操作的过程如下:

    1. 读取共享变量的当前值。
    2. 比较当前值与期望值是否相等。
    3. 如果相等,则将新值替换当前值,操作成功。
    4. 如果不相等,则说明共享变量的值已经被其他线程修改,操作失败。
  • CAS操作在执行过程中是原子的,即它是不可中断的。它利用硬件指令提供的原子性操作(例如CMPXCHG指令)来实现。当多个线程同时执行CAS操作时,只有一个线程能成功执行替换操作,其他线程的CAS操作会失败。

  • CAS操作常用于实现无锁数据结构和并发算法,例如线程安全的计数器、并发队列等。它提供了一种高效且线程安全的方式来处理共享变量的并发访问问题。

  • 需要注意的是,CAS操作虽然能够避免了锁的开销,但在高并发情况下,由于可能存在多个线程同时执行CAS操作并竞争共享变量,导致CAS操作的失败和重试,从而增加了系统的开销。因此,在使用CAS操作时需要权衡其适用性和性能。

2. CAS执行过程中,其它线程可以修改共享变量的值吗?(来自chatGPT的回答)

  CAS操作通常包括三个步骤:读取当前值、比较当前值与期望值、替换新值。在执行这些步骤期间,其他线程可能会修改共享变量的值。

  CAS操作的原子性指的是CAS操作的执行过程是不可中断的,不会被其他线程的操作中断。在单个线程执行CAS操作时,它是原子的,要么全部成功执行,要么全部不执行。

  然而,并发环境下多个线程同时执行CAS操作,并且彼此竞争修改共享变量的值,可能导致CAS操作失败。这并不是CAS操作本身不是原子的,而是由于竞争条件引起的问题。因此,在使用CAS操作时,需要合理设计并发算法或采用其他机制来保证数据的一致性和正确性。

以下是一个可能导致CAS替换操作失败的情况:

  1. 线程A读取共享变量的当前值,并与期望值进行比较,发现它们相等。
  2. 在线程A执行CAS替换操作之前,线程B也读取了共享变量的当前值,并修改了共享变量的值。
  3. 线程A执行CAS替换操作,但由于当前值已经被线程B修改,所以替换操作失败。CAS操作会返回失败的结果。

  这种情况下,CAS操作的失败表示当前线程没有成功修改共享变量的值,需要根据具体情况采取适当的处理策略。

  CAS操作的失败并不会阻塞线程,它会立即返回失败的结果。因此,在使用CAS操作时,需要谨慎处理失败的情况,可能需要进行重试、放弃当前操作,或者执行其他的补偿逻辑。

3. AtomicInteger 是如何保证原子性的?

  • AtomicInteger 使用 volatile 修饰内部的 value 变量,这确保了在一个线程修改 value 后,其他线程能够立即看到最新的值
  • 使用CAS、自旋(while循环)

4. 缺点

  • ABA问题
    • ABA问题发生在以下情况:

      1. 初始状态下,共享变量的值为A。
      2. 线程1读取共享变量的值A。
      3. 线程1被挂起,使得其他线程有机会修改共享变量的值。
      4. 线程2将共享变量的值从A修改为B,然后又将其修改回A。

      此时,线程1被唤醒,继续执行CAS操作,它会发现共享变量的当前值仍然为A,符合CAS的预期值,因此线程1会错误地认为没有其他线程修改过共享变量,而继续执行CAS并将其更新为新的值。这样,线程1可能会不知情地覆盖了其他线程所做的修改。

    • 解决ABA问题的常见方法是使用版本号或时间戳来跟踪共享变量的变化,确保CAS操作不仅比较值是否相等,还比较版本号或时间戳是否匹配。在Java中,AtomicStampedReference类和AtomicMarkableReference类是为了解决ABA问题而提供的原子类,它们允许将版本号或标记与共享变量一起传递

  • CAS操作需要硬件的配合
  • 保证各个CPU的缓存(L1、L2、L3、跨CPU Socket、主存)的数据一致性,通讯开销很大,在多处理器系统上更严重

Java实现CAS的原理

通过AtomicInteger来理解CAS

5. 自旋

1. 概述
  • CAS操作中的自旋是指在CAS操作失败时,线程会反复尝试执行CAS操作,直到成功为止,而不是立即阻塞或放弃执行

  • 自旋是为了解决竞争条件的问题。在并发环境中,多个线程可能同时执行CAS操作并竞争修改共享变量的值。当CAS操作失败时,意味着当前线程没有成功修改共享变量的值,可能是因为其他线程在此期间修改了共享变量

  • 为了避免阻塞线程或频繁地上下文切换,CAS操作通常采用自旋的方式。线程会反复执行CAS操作,即反复读取共享变量的当前值、比较当前值与期望值,并尝试替换新值。如果CAS操作成功,线程可以继续执行后续的操作;如果CAS操作失败,线程会再次尝试执行CAS操作,直到成功或达到某个条件

  • 自旋的优势在于避免了线程的阻塞和上下文切换的开销,可以更快地完成操作。然而,如果自旋时间过长,会占用CPU资源,降低系统的效率。因此,合理设置自旋次数和退出条件非常重要

  • 在Java中,可以使用循环结构(如while循环)来实现CAS操作的自旋。线程会在循环内反复执行CAS操作,直到成功或满足退出条件。同时,可以结合线程的yield()或sleep()等方法来让出CPU资源,避免过度自旋