美文网首页
【Java核心基础知识】07 - 多线程并发(6)

【Java核心基础知识】07 - 多线程并发(6)

作者: Liuzz25 | 来源:发表于2023-11-06 13:52 被阅读0次

    多线程知识点目录

    多线程并发(1)- https://www.jianshu.com/p/8fcfcac74033
    多线程并发(2)-https://www.jianshu.com/p/a0c5095ad103
    多线程并发(3)-https://www.jianshu.com/p/c5c3bbd42c35
    多线程并发(4)-https://www.jianshu.com/p/e45807a9853e
    多线程并发(5)-https://www.jianshu.com/p/5217588d82ba
    多线程并发(6)-https://www.jianshu.com/p/d7c888a9c03c

    十九、CAS(比较并交换-乐观锁机制-锁自旋)

    19.1 概念及特性

    CAS(Compare And Swap/Set)比较并交换。
    CAS算法的过程:包含3个参数CAS(V,E,N),分别为:

    • V表示要更新的变量(内存值)
    • E表示预期值(旧的)
    • N表示当前值(新的)

    当且仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做,最后,CAS返回当前V的真实值。
    CAS操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

    19.2 原子包 java.util.concurrent.atomic(锁自旋)

    JDK1.5的原子包:java.util.concurrent.atomic这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实力包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他现成打断,而别的现成就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择另一个线程进入,这只是一种逻辑上的理解。

    相对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现(它在多线程环境中无需使用阻塞锁,从而减少了线程的阻塞和上下文切换,进而提高了程序的性能。)。由于一般CPU切换时间比CPU指令集操作更长,所以J.U.C在性能上有了很大的提升。

    19.3 ABA问题

    CAS的ABA问题是指在并发编程中,使用CAS算法时可能会出现的一种问题。

    ABA问题的原因是,CAS算法需要在操作值的时候,检查某地址的内容有没有发生变化(和旧值进行比较),如果没有发生变化则更新为新的值。但是,如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。即:这个线程在操作的时候,其他线程也在进行操作。

    例如,有两个线程T1和T2,T1从内存地址X中取出A,这时另一个线程T2也从内存地址X中取出A,并且线程T2进行了一系列操作将值改变成B,写回主物理内存。然后线程T2又将内存地址为X的数据变为A,这个时候线程T1进行了CAS操作发现内存中仍然是A,这时线程T1进行CAS操作成功。尽管线程T1的CAS操作成功,但并不代表这个过程就没有问题,这就是ABA问题。

    为了解决ABA问题,可以使用版本号。那么A→B→A就会变成1A→2B→3A。具体来说,可以在每个值前面加上一个版本号,每次更新时将版本号加一。当进行CAS操作时,除了比较值之外,还需要比较版本号。只有当版本号不变时,才说明值没有发生变化。这种方法可以确保在并发环境下正确地更新值。

    二十、AQS(抽象的队列同步器)

    20.1 概念及特性

    AbstractQueuedSynchronized类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReenTrantLock、Semaphore、CountDownLatch。

    AbstractQueuedSynchronized底层结构

    它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。

    这里的 volatile 是关键字,它确保了 state 的可见性。具体来说,volatile 关键字可以确保每个线程在读取 state 时都能得到最新的值,而不是读取到已经被其他线程修改过的旧值。

    state 的访问方式主要有三种:getState()、setState() 和 compareAndSetState():

    1. getState():这个方法用于获取当前的状态值。
    2. setState():这个方法用于设置新的状态值。
    3. compareAndSetState():这个方法比较当前状态值和期望的状态值,如果两者相等,则设置新的状态值。这是一种原子操作,可以确保在多线程环境下,状态的改变是原子的。

    当一个线程需要获取资源时,它首先会调用 getState() 方法来查看当前的状态。如果状态指示资源可用,那么线程就可以获取资源并进行操作。如果状态指示资源不可用,那么线程就会被放入等待队列中,直到状态变为可用。

    当线程释放资源时,它会调用 setState() 方法来改变状态值。如果此时没有其他线程在等待该资源,那么状态改变后,等待队列中的线程就可以按顺序获取资源并进行操作。如果有其他线程在等待该资源,那么状态改变后,等待队列中的线程将再次被放入队列中等待。

    20.2 AQS定义两种资源共享方式

    Exclusive 独占资源(ReenTrantLock)

    只有一个线程能执行,如 ReentrantLock。它又可分为公平锁和非公平锁:

    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁。
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的。

    资源共享和获取/释放的接口:tryAcquire() 和 tryRelease() 方法。当一个线程尝试获取资源时,如果资源当前不可用,那么该线程会被放入等待队列,直到资源可用。当线程释放资源时,会检查是否有其他线程在等待该资源。

    Share共享资源(Semaphore/CountDownLatch)

    多个线程可同时执行,如 CountDownLatch、Semaphore、CyclicBarrier、ReadWriteLock。例如,ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。

    资源共享和获取/释放的接口:tryAcquireShared() 和 tryReleaseShared() 方法。在共享模式下,多个线程可以同时获取资源,但通常会有一个限制,比如同时最多有几个线程可以获取资源。当一个线程尝试获取资源时,如果资源当前不可用,那么该线程会被放入等待队列,直到资源可用。当线程释放资源时,会唤醒等待队列中的一个线程。


    由于独占模式和共享模式的接口方法不同,因此 AQS 本身并没有定义成抽象类,而是定义了一个接口。这样可以允许实现类根据具体的需求来实现不同的接口方法。

    对于自定义同步器来说,只需要实现 state 的获取和释放方式即可。具体的线程等待队列的维护(如获取资源失败时的入队操作、唤醒出队等)已经由 AQS 在顶层实现了。这样可以让自定义同步器专注于实现具体的资源获取和释放策略,而不需要关心线程等待队列的维护细节。

    20.3 同步器的实现

    ReentrantLock为例,它的实现确实是基于 AQS(AbstractQueuedSynchronized)的,state 变量用来表示资源的状态。在 ReentrantLock 的 lock()方法中,会调用 tryAcquire()方法尝试获取资源,如果 state 为 0,表示资源未被占用,这时线程会将 state 加 1,并设置自己的状态为拥有资源状态。此后,其他线程再尝试 lock()时就会失败,直到当前拥有资源的线程调用 unlock()方法释放资源,将 state 设置为 0,其他线程才有机会获取该锁。

    CountDownLatch的例子中,state 变量也用来表示资源的状态,但它的作用是记录需要等待的子线程数量。在 CountDownLatch 的构造函数中,会将 state 初始化为 N,表示有 N 个子线程需要执行。然后,这 N 个子线程会并行执行任务,每个子线程执行完后会调用 countDown()方法将 state 减 1。当 state 减为 0 时,表示所有子线程都已执行完毕,这时就会唤醒主调用线程,使其从 await()方法返回,继续后续操作。

    无论是 ReentrantLock 还是 CountDownLatch,它们都利用了 AQS 提供的队列和 state 变量等核心机制来实现资源的获取和释放。通过这种方式,可以将同步器的实现与具体的资源获取和释放策略分离,从而提供更大的灵活性。

    20.4 ReentrantReadWriteLock 实现独占和共享两种方式

    ReentrantReadWriteLock 是 Java 中的一个读写锁,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。在 ReentrantReadWriteLock 中,读锁和写锁是互斥的,因此 ReentrantReadWriteLock 实现的是独占式的资源访问。

    在 ReentrantReadWriteLock 中,读锁和写锁的获取和释放都是通过内部的锁来实现的。读锁和写锁的状态都是通过 state 变量来维护的。当一个线程获取写锁时,它会先将 state 加 1,表示有一个写线程正在等待获取锁。如果此时还有读线程正在等待获取读锁,那么这些读线程可以继续等待获取锁。但是,如果此时没有读线程正在等待获取读锁,那么获取写锁的线程就可以获取到锁并执行。

    在实现 ReentrantReadWriteLock 时,需要同时实现独占和共享两种方式。具体来说,需要实现 tryAcquire()和 tryRelease()方法来支持独占式的资源访问,同时还需要实现 tryAcquireShared()和 tryReleaseShared()方法来支持共享式的资源访问。在 ReentrantReadWriteLock 中,获取读锁和写锁的过程是原子性的,因此可以实现精确的控制。

    总之,ReentrantReadWriteLock 是一种支持独占和共享两种方式的同步器,它通过内部的锁来实现精确的控制,允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。

    相关文章

      网友评论

          本文标题:【Java核心基础知识】07 - 多线程并发(6)

          本文链接:https://www.haomeiwen.com/subject/zfitwdtx.html