Semaphore ,aqs,ReentrantLock ,Condition,CopyOnWriteArrayList 源码解读

一、Semaphore

Semaphore 通过设置一个固定数值的信号量并发时线程通过 acquire() 获取一个信号量,如果能成功获得则可以继续执行,否则将阻塞等待,当某个线程使用 release() 释放一个信号量时,被阻塞的线程则可以被唤醒重新争抢信号量。根据该特征可以有效控制线程的并发数。

那 Semaphore 是如何控制并发的呢,本篇文章带领大家一起解读下 Semaphore 的源码。

在进行源码分析前,先回顾下 Semaphore 是如何使用的,例如下面一个案例:

public class Test {

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println("线程:" + Thread.currentThread().getName() + " 执行, 当前时间:" + LocalDateTime.now().toString());
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}

运行之后,可以看到下面日志:

在这里插入图片描述
可以看到每次都是 3 个并发。

在本专栏前面讲解 AQS 源码的时候提到 Semaphore 是基于 AQS 实现的,那是如何使用的 AQS 呢?

在 AQS 中,如果需要使用AQS的特征则需要子类根据使用的场景,重写下面方法:

//查询是否正在独占资源,condition会使用
boolean isHeldExclusively()	
//独占模式,尝试获取资源,成功则返回true,失败则返回false
boolean tryAcquire(int arg)
//独占模式,尝试释放资源,成功则返回true,失败则返回false
boolean tryRelease(int arg)
//共享模式,尝试获取资源,如果返回负数表示失败,否则表示成功。
int tryAcquireShared(int arg)
//共享模式,尝试释放资源,成功则返回true,失败则返回false。
boolean tryReleaseShared(int arg)

由于这里 Semaphore 的特性,所以下面我们只需关注共享模式下的几个方法即可。

说明:由于 Semaphore 的实现依赖于 AQS ,因此需要对 AQS 有一定的了解,不了解的小伙伴可以看下这篇对 AQS 源码分析的文章,和当前文章在同一专栏:

https://blog.csdn.net/qq_43692950/article/details/129367736

二、Semaphore 中 Sync、FairSync、NonfairSync

2.1 Sync、FairSync、NonfairSync

在声明 Semaphore 时,有两种方式,一种是使用只有一个 permits 参数的构造函数,一种则需要多增加一个 fair 参数:

new Semaphore(3);
new Semaphore(3, true);

当使用只有一个 permits 参数的构造函数声明时,则是创建了一个 NonfairSync 对象:

在这里插入图片描述

通过需要多增加一个 fair 参数的构造函数时,则可以根据传入的 fair 选择创建一个 FairSync 对象:

在这里插入图片描述

这里也不难理解 NonfairSync 和 FairSync 其实可以理解为 Semaphore 中的非公平锁和公平锁两种类型。

点到这两个类中,可以看到都继承自 Sync 类:

在这里插入图片描述
在这里插入图片描述

而 Sync 类,则继承自 AQS :

在这里插入图片描述
到这里,我们就可以寻找几个关键的方法,在AQS中共享模式下,两大关键的方法是交由子类进行实现的,分别是 tryAcquireShared 尝试获取资源,和 tryReleaseShared 尝试释放资源。

首先来看 tryAcquireShared 尝试获取资源:

通过 Sync 类的实现源码发现并没有重写 tryAcquireShared方法,那该方法肯定在下面的FairSync 和 NonfairSync 子类中,分别看下源码确实存在重写的方法:

在这里插入图片描述
在这里插入图片描述

2.2 NonfairSync 下的 tryAcquireShared

这里先分析下 NonfairSync 的 tryAcquireShared 实现逻辑,可以看到又调用了 nonfairTryAcquireShared 就是 Sync 类中的 nonfairTryAcquireShared ,从命名上可以分析出就是非公平锁的尝试获取资源的操作,直观就是非公平锁下获取锁的操作:

在这里插入图片描述

进入到 Sync 类中的 nonfairTryAcquireShared 方法中,可以明显看到一个自旋的操作,在循环中首先获取到 AQS 中的共享资源 state ,并对其进行 - acquires (默认为 1 ,后面会进行说明)操作,其实就是 -1 操作,如果减去的值小于 0 或者修改 state 成功,就返回当前减去的值,否则就自旋的方式再次重试:

在这里插入图片描述

上一步的操作主要做了什么目的呢,其实从 Sync 的构造方法就可以看出,创建 Semaphore 传递的 permits 参数被赋值给了 AQS 中的 state ,那此时 state 就记录的当前剩余信号量的大小,获取资源就要进行 -1 标识消耗了一个,最后将减去的值返回出去表示剩余的资源,如果信号量小于 0 了,则表示获取资源失败,直观就是获取锁失败。因为在 AQS 中对 tryAcquireShared 方法的判断是小于 0 时,进行线程的入列和挂起等待。

在这里插入图片描述
在这里插入图片描述

2.3 FairSync下的 tryAcquireShared

在 FairSync 类下的 tryAcquireShared 方法中,和前面 NonfairSync 类似,但不同的是,会首先进行 hasQueuedPredecessors 方法的判断:

在这里插入图片描述

下面进到 hasQueuedPredecessors 的方法中,可以看到是由 AQS 提供的方法,主要就是判断当前节点线程的前面是否还有等待的线程,因为 FairSync 实现的是公平锁的原则,如果当前线程前面还有等待线程,则获取锁资源也轮不到自个,让前面的老大先来,所以直接返回 -1 表示获取资源失败:

在这里插入图片描述

2.4 tryReleaseShared

到这里已经了解到了 tryAcquireShared尝试获取资源的逻辑,上面提到了两个重要方法,还有一个 tryReleaseShared 没有分析,还是首先看 Sync 类中是否有重写该方法:

通过源码可以看到,在 Sync 类中就已经对 tryReleaseShared 进行了重写,而 NonfairSync 和 FairSync 中都没有重写该方法,那释放资源就是走的 Sync 类下的 tryReleaseShared 方法:

在这里插入图片描述
在该方法同样使用了自旋,首先获取到 AQS 中的共享资源 state ,然后进行 + releases (默认情况下为 1 ,后面会说明),其实就是进行 +1 操作,并使用新的值修改 state ,如果修改失败的话则在自旋中继续修改,直到成功后返回 true ,表示释放资源成功。

看到这里就会发现获取资源和释放资源,无非就是对 AQS 中的共享资源 state 进行操作。理解了这两大核心的方法后,下面就可以看如何运用在 Semaphore 中的了。

三、semaphore.acquire()

通过 semaphore.acquire() 可以获取一个信号量,如果获取不到则阻塞等待,那semaphore.acquire() 主要做了什么呢?

下面点到该方法中,可以看到又调用了 sync.acquireSharedInterruptibly 方法,其实就是 AQS 中的 acquireSharedInterruptibly 方法,注意这里传递的参数为 1 ,对应上面括号中的说明:

在这里插入图片描述

在 AQS 的 acquireSharedInterruptibly 方法中,首先会使用子类的 tryAcquireShared 方法获取资源,如果资源数小于 0 ,则认为获取失败,下面使用 doAcquireSharedInterruptibly 进行加入队列并挂起阻塞:

在这里插入图片描述

关于AQS如何加入队列和挂起,可以参考文章开始的链接中对 AQS 源码的解读。

四、semaphore.release()

上面在获取不到可用的资源时,则会被 AQS 挂起,因此这里还需要进行释放资源。

下面点到 semaphore.release() 方法中,可以看到又调用了 sync.releaseShared ,其实就是 AQS 中的 releaseShared 方法,注意这里参数默认为 1 ,对应上面括号中的说明:

在这里插入图片描述
在 AQS 的 releaseShared 方法中,会首先调用子类的 tryReleaseShared 释放资源,释放成功后,会使用 doReleaseShared 进行挂起线程的唤醒:

在这里插入图片描述

关于releaseShared方法的源码解读可以参考文章开始的链接中对 AQS 源码的解读。

三、总结

通过阅读 Semaphore的源码可以发现,大量依赖于 AQS 中提供的方法,如果有阅读过本专栏对 ReentrantLock 锁源码的分析,可以发现相似度极高,都是使用 AQS 所提供的的特征实现某些场景的应用。

 

AQS 源码解读

一、AQS

AQS 是 AbstractQueuedSynchronizer 的简称,又称为同步阻塞队列,是 Java 中的一个抽象类。在其内部维护了一个由双向链表实现的 FIFO 线程等待队列,同时又提供和维护了一个共享资源 state ,像我们平常使用的 ReentrantLock、Semaphore、ReentrantReadWriteLock、SynchronousQueue、FutureTask等都是基于AQS 进行实现的。

AQS 可以实现什么功能呢?在 AQS 中不考虑资源的获取和释放,主要关注资源获取不到时,如何将线程加入队列以及阻塞,当释放资源后,如何再进行线程的出列和唤醒。而对于资源的操作则交予具体实现的子类进行完成。

在此基础上 AQS 为了使线程的控制可以更灵活,又提供了两种同步模型独占模式共享模式

  • 独占模式:表示并发情况下只有一个线程能执行,其余则需等待,例如 Lock 锁,一次只能有一个线程获取到锁。

  • 共享模式:允许多线程根据规则执行,例如 Semaphore 进行多个线程的协调。

AQS 已经帮我们实现了队列的维护,以及线程的等待和唤醒,但是具体资源的获取和释放都需要由继承类实现,对于资源的获取和释放也是区分了独占模式和共享模式,相应方法如下:

//查询是否正在独占资源,condition会使用
boolean isHeldExclusively()	
//独占模式,尝试获取资源,成功则返回true,失败则返回false
boolean tryAcquire(int arg)
//独占模式,尝试释放资源,成功则返回true,失败则返回false
boolean tryRelease(int arg)
//共享模式,尝试获取资源,如果返回负数表示失败,否则表示成功。
int tryAcquireShared(int arg)
//共享模式,尝试释放资源,成功则返回true,失败则返回false。
boolean tryReleaseShared(int arg)

例如在 ReentrantLock 公平锁中,tryAcquire 的实现逻辑如下:

protected final boolean tryAcquire(int acquires) {
        // 当前线程
        final Thread current = Thread.currentThread();
        // AQS 中共享 state
        int c = getState();
        if (c == 0) {
            // 如果队列中没有其他线程,并对state进行修改,
            // 如果修改成功则设置独占锁的线程为当前线程
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            // 如果独占线程就是当前线程,则是重入的场景,对 state + 1
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        // 如果都没成功,则获取锁失败
        return false;
    }
}

可以看到在 ReentrantLock 公平锁中,通过 state 的值来标识是否有锁资源可用,并且重入情况下也是对 state 的值进行修改标识 ,对于 state 的修改和判断是否有等待队列线程, AQS 中都提供了相应的方法。

在 AQS 中几个核心的方法如下,同样区分了独占模式和共享模式:

// 返回共享资源的当前值
final int getState()
// 设置共享资源的值
final void setState(int newState)
// CAS设置共享资源的值
final boolean compareAndSetState(int expect, int update)

// 独占模式获取同步资源,会调用重写的tryAcquire(int arg),
// 如果获取成功,则不做任何处理,否则将会加入同步队列并挂起线程等待
final void acquire(int arg)
// 独占模式式获取同步资源,但是可以响应中断
final void acquireInterruptibly(int arg)
// 独占模式获取同步资源,但多出了超时时间,
// 如果当前线程在 nanosTimeout 时间内没有获取到同步资源,
// 那么将会返回false,否则返回true
final boolean tryAcquireNanos(int arg, long nanosTimeout)
// 独占模式式释放同步资源,会调用重写的 tryRelease(int arg) 方法,
// 在释放同步资源之后,会将同步队列中第一个节点包含的线程唤醒
final boolean release(int arg)

// 共享模式式获取同步资源,会调用重写的 tryAcquireShared(int arg) ,
// 如果当前线程未获取到同步资源,会加入同步队列等待,
// 和独占式的区别这里 tryAcquireShared(int arg) < 0 时才认为未获取到资源
final void acquireShared(int arg)
// 共享模式式获取同步资源,可以响应中断
final void acquireSharedInterruptibly(int arg)
// 共享模式获取同步资源,但多出了超时时间
final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
// 共享式释放同步资源,会调用重写的 tryReleaseShared(int arg) 方法,
// 在释放同步资源之后,会将同步队列中第一个节点包含的线程唤醒
final boolean releaseShared(int arg)

下面一起从源码的角度,分析 AQS 是如何实现线程的协调和管理的。

二、共享资源 state

共享资源 state 就是 AQS 中的一个 int 类型的全局变量,使用了 volatile 进行修饰,保证了多线程下的数据可见性,并且 AQS 为其提供了普通和 CAS 方式的修改方法,该共享资源主要用来做资源的标记。

例如:

在 ReentrantLock 锁中用来表示是否获取到锁,默认情况 0 表示无锁状态,获取到锁后进行 +1 ,如果是重入的场景下同样进行 +1 ,最后释放锁后再进行 -1

在 Semaphore 中用来表示信号量的标记,当获取信号量时 state 进行 -1 ,释放信号量再进行 +1 :

在这里插入图片描述
在这里插入图片描述

三、FIFO 阻塞队列

在 AQS 中阻塞队列采用双向链表进行实现,具体源码如下:

	//等待队列节点类,双向链表
    static final class Node {
        // 标记,指示节点正在共享模式下等待
        static final Node SHARED = new Node();
        // 标记,指示节点正在独占模式下等待
        static final Node EXCLUSIVE = null;

        //  waitStatus值表示线程已取消
        static final int CANCELLED = 1;
        //  waitStatus值表示后继线程需要唤醒
        static final int SIGNAL = -1;
        //  waitStatus值,表示线程正在等待状态
        static final int CONDITION = -2;
        // waitStatus值指示下一个被获取的应该无条件的传播
        static final int PROPAGATE = -3;

        // 线程等待状态
        volatile int waitStatus;

        // 上一个节点
        volatile Node prev;

        // 下一个节点
        volatile Node next;

        // 当前线程
        volatile Thread thread;

        // 节点的模式,独占还是贡献
        Node nextWaiter;

        // 是否为共享模型
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        // 获取上一个节点
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

其中通过 nextWaiter 表示当前线程是独占模型还是共享模式,线程的所属状态使用 waitStatus 来进行表示,其中包括:

  • 默认值为 0,表示当前节点在 sync 队列中,等待着获取资源。
  • CANCELLED,值为1,表示当前的线程被取消。
  • SIGNAL,值为-1,释放资源后需唤醒后继节点。
  • CONDITION,值为-2, 等待condition唤醒。
  • PROPAGATE,值为-3,工作于共享锁状态,需要向后传播,比如根据资源是否剩余,唤醒后继节点。

四、独占模式 acquire 获取资源

在 AQS acquire() 方法中,首先调用子类的 tryAcquire 获取资源,如果资源获取成功则不做任何处理,如果失败则首先使用 addWaiter 将当前线程加入到队列中,并指定 Node 的类型为独占模式:

在这里插入图片描述

在 addWaiter 方法中,会将当前线程的 Node 加入到队列的尾端,如果尾节点为空或修改尾节点失败则进入到 enq 中使用自旋的方式修改:

在这里插入图片描述

在 enq 方法中可以看出,当为节点为空时,也就是队列中无数据时,会初始化一个空的 Head 节点。

在这里插入图片描述

再回到 acquire() 方法,加入队列后会进入到 acquireQueued 方法中,在该方法循环中如果当前节点 的pred 上一个节点是 head 节点的话,那该节点不就是第一个节点吗,因为从上面就可以看出,初始情况下 head 是一个空的 node ,那 head 的下一个节点不就是第一个进入到队列的节点了,这种情况下遵循队列先进先出的原则,再次尝试是否能获取到资源,如果可以成功获取资源到则将当前节点置为 head 节点,同时再次将 head 节点置为空 node,此时线程也无需阻塞可以直接执行:

在这里插入图片描述
在这里插入图片描述

但是如果当前节点的上一个节点不是 head 节点,或者没有获取到资源,则此时需要进行挂起阻塞,下面首先会触发 shouldParkAfterFailedAcquire 方法,这里先看后面的 parkAndCheckInterrupt 方法,该方法主要做了将线程挂起阻塞的作用,采用 LockSupport.park 进行线程的阻塞:

在这里插入图片描述

在这里插入图片描述

再来看 shouldParkAfterFailedAcquire 方法就是控制当前线程是否需要挂起,这里就需要使用到 Node 中的 waitStatus,在该方法中有三种类型的判断:

  • 如果当前是 SIGNAL 状态则可以直接挂起
  • waitStatus大于 0 时,在 Node 中 waitStatus 大于 0 的状态就是 CANCELLED 状态,也就是标识线程被取消了,此时这种线程进行阻塞也就没有意义了,那就一直循环向上取线程未被取消的作为当前节点,继续执行。
  • waitStatus小于等于 0 时,将状态置为 SIGNAL 类型

在这里插入图片描述

后面当阻塞的线程被唤醒后,会继续在 acquireQueued 的循环中,不断找寻第一个入队的线程进行尝试获取资源操作。

五、独占模式 release 释放资源

在 release 方法中,首先会调用子类的 tryRelease 方法释放资源:

在这里插入图片描述
然后会将当前的 head 节点传入 unparkSuccessor 方法中,在该方法中首先将该Node节点的 waitStatus 修改到默认的 0 值,然后获取到下一个节点,因为 head 节点始终保持为空节点,下一个节点才是真正的队列中第一个线程。但如果下一个节点为空的话,或者已经被取消了,则循环从 tail 节点向上找最前面正常的节点,最后直接使用 LockSupport.unpark 唤醒该节点的线程:

在这里插入图片描述

六、共享模式 acquireShared 获取资源

在 acquireShared 方法中,会首先调用子类的 tryAcquireShared 方法获取资源,但与独占模式不同的是,这里当资源的数量小于 0 时,则认为获取资源失败:

在这里插入图片描述
当资源获取失败时,会进入到 doAcquireShared 方法,在该方法中同样先将自己加入到阻塞队列中,将 Node 的类型设为 Node.SHARED 共享模式:

在这里插入图片描述

下面的判断逻辑和独占模式差不多,取当前节点的上一个节点,如果是 head 节点,那当前节点便是队列的第一个线程,此时则可以尝试获取资源,如果资源大于 0 认为获取资源成功,则将当前节点置为 head 节点:

在这里插入图片描述

在 setHeadAndPropagate 方法中,与独占模式不同,将当前节点置为 head 节点后并没有进行置空操作,而且又会判断资源大于 0 的话,通过 doReleaseShared 唤醒更多的线程继续执行:

这里 doReleaseShared 方法的逻辑,在下面 releaseShared解读时进行解释:

在这里插入图片描述

回到 doAcquireShared 方法中,下面 shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 则和独占模式调用方法相同,将符合条件的线程进行阻塞:

在这里插入图片描述
后面当阻塞的线程被唤醒后,会继续在 doAcquireShared 的循环中,不断找寻第一个入队的线程进行尝试获取资源操作。

七、共享模式 releaseShared 释放资源

在 releaseShared 方法中,会首先调用子类的 tryReleaseShared 方法释放资源:

在这里插入图片描述

释放资源后会进到 doReleaseShared 方法唤醒等待的线程,对 head 节点进行唤醒:

在这里插入图片描述

当 head 节点唤醒后,会和 doAcquireShared 的方法中的 setHeadAndPropagate 形成呼应,如果获取到的资源数大于 0 则继续使用 doReleaseShared 进行唤醒,从而控制多个线程执行。

八、总结

AQS 没有限制具体某个场景的应用,但通过其内部维护的 FIFO 队列和共享资源 state便可以实现很多种不同的场景,在阅读了 AQS 源码后,应该有了更深入的理解,后面再去看 ReentrantLock、Semaphore 等的源码会发现很容易理解。

 

ReentrantLock 源码解读

一、ReentrantLock

ReentrantLock 是 java JUC 中的一个可重入锁,在上篇文章讲解 AQS 源码的时候提到 ReentrantLock 锁是基于 AQS 实现的,那是如何使用的 AQS 呢,本篇文章一起带大家看下 ReentrantLock 的源码。

在 AQS 中,如果需要使用AQS的特征则需要子类根据使用的场景,重写下面方法,

//查询是否正在独占资源,condition会使用
boolean isHeldExclusively()	
//独占模式,尝试获取资源,成功则返回true,失败则返回false
boolean tryAcquire(int arg)
//独占模式,尝试释放资源,成功则返回true,失败则返回false
boolean tryRelease(int arg)
//共享模式,尝试获取资源,如果返回负数表示失败,否则表示成功。
int tryAcquireShared(int arg)
//共享模式,尝试释放资源,成功则返回true,失败则返回false。
boolean tryReleaseShared(int arg)

由于这里 ReentrantLock 锁的特性,所以下面我们只需关注独占模式下的几个方法即可。

说明:本文中关于 AQS 中的方法没有做过多的解释,不了解的小伙伴可以看下这篇对 AQS 源码分析的文章,和当前文章在同一专栏:

https://blog.csdn.net/qq_43692950/article/details/129367736

下面一起开始 ReentrantLock 源码的分析:

二、ReentrantLock 的 Sync、FairSync、NonfairSync

2.1 Sync、FairSync、NonfairSync

在声明 ReentrantLock 锁时,有两种方式,一种是无参构造函数,一种则需要指定一个 fair 参数:

new ReentrantLock();
new ReentrantLock(false);

当使用无参构造函数声明时,则是创建了一个 NonfairSync 对象:

在这里插入图片描述

通过有参的构造函数,则根据传入的 fair 可以选择创建一个 FairSync 对象:

在这里插入图片描述

其实这里也不难理解 NonfairSync 和 FairSync 其实就是ReentrantLock 锁中的非公平锁和公平锁两种类型。

点到这两个类中,可以看到都继承自 Sync 类:

在这里插入图片描述
在这里插入图片描述

而 Sync 类,则继承了 AQS :

在这里插入图片描述

到这里了,我们寻找几个关键的方法,在AQS中独占模式下,两大关键的方法是交由子类进行实现的,分别是 tryAcquire 尝试获取资源,和 tryRelease 尝试释放资源。

首先来看 tryAcquire 尝试获取资源:

通过 Sync 类的实现源码发现并没有重写 tryAcquire 方法,那该方法肯定在下面的子类FairSync 和 NonfairSync ,分别看下源码确实存在重写的方法:

在这里插入图片描述
在这里插入图片描述

2.2 NonfairSync 下的 tryAcquire

首先看下 NonfairSync 的 tryAcquire 实现逻辑,可以看到又调用了 nonfairTryAcquire 就是 Sync 类中的 nonfairTryAcquire ,从命名上可以分析出就是非公平锁的尝试获取资源,直观就是非公平锁下获取锁操作:

在这里插入图片描述
进入到 Sync 类中的 nonfairTryAcquire中,可以看到首先获取到 AQS 中的共享资源 state,如果 state 等于 0 ,则将 state 的值修改为 acquires(默认为1,下面会分析到),并设置AQS的独占线程为当前线程,并返回 true ,说白了不就是获取到锁了吗,那就可以理解为 state 等于 0 即是无锁的状态,下面将 state 的值修改为 acquires 就是获取到锁了,改变资源的状态:

在这里插入图片描述

接着如果 state 的值不是 0 ,则当前锁已经被别的线程持有了,这里又判断了下,如果持有锁的线程正好是当前的线程,那不就是锁的重入吗,这种情况下可以直接获得锁,不过这里为了记录重入的次数,对 state 共享资源进行了 + acquires 操作,其实就是 +1 操作。

在这里插入图片描述

如果都没有成功,那此时则获取锁失败,返回 false

2.3 FairSync下的 tryAcquire

在 FairSync 类下的 tryAcquire 方法中,和前面 NonfairSync 类似,但不同的是,在获取到锁时,也就是拿到 state 等于 0 ,进行修改资源时,多了步 hasQueuedPredecessors 的判断:

在这里插入图片描述

下面可以进到 hasQueuedPredecessors 的方法中,可以看到是由 AQS 提供的方法,主要就是判断当前节点线程的前面是否还有等待的线程,因为 FairSync 实现的是公平锁的原则,如果当前线程前面还有等待线程,则获取锁资源也轮不到自个,让前面的老大先来:

在这里插入图片描述
hasQueuedPredecessors 方法理解后,其余的逻辑则和 NonfairSync 中的一致了。

2.4 tryRelease

到这里已经了解到了tryAcquire 尝试获取资源的逻辑,上面提到了两个重要方法,还有一个 tryRelease 没有分析逻辑,还是首先看 Sync 类中是否有重写该方法:

通过源码可以看到,在 Sync 类中就已经对 tryRelease 进行了重写,而 NonfairSync 和 FairSync 中都没有重写该方法,那释放资源就是走的 Sync 类下的 tryRelease 方法:
在这里插入图片描述

在该方法中,可以看到首先还是获取到了 AQS 中的 state 共享资源,然后对该资源进行 - releases (默认releases1,下面会提到 )操作,其实就是 -1 操作:

在这里插入图片描述
接着判断了下,如果当前线程不是持有锁线程,就抛出异常,也好理解,没有持有锁的线程跑过来释放锁,那肯定有问题了呀。

接着再进行判断 state 是不是等于 0 ,上面讲到在锁重入的情况下,记录重入的次数是对 state 进行 +1 操作,而这边又对 state 进行 -1 操作,如果减到最后 state 有成了最初的 0 ,那不就是重入的锁和当前持有的锁都释放完了吗,这个时候就可以将持有锁的线程置为空了,并修改最新的 state :

在这里插入图片描述

看到这里就会发现获取锁和释放锁,无非就是对 AQS 中的共享资源进行操作。理解了这两大核心的方法后,下面就可以看如何运用在 ReentrantLock 中的了。

三、lock.lock()

在 ReentrantLock 中,需要获取锁时,直接使用 lock.lock() 即可,那 lock.lock() 到底做了什么呢,点到该方法中,可以看到是调用的 Sync 的 lock 方法,而 Sync 中的lock 方法是抽象方法,具体实现肯定在子类的 NonfairSync、 FairSync 中。

在这里插入图片描述

3.1 NonfairSync.lock()

首先看点 NonfairSync 非公平锁中的 lock 方法,直接进行了将 AQS 中的共享资源 state 由 0 改为 1 ,如果修改成功,根据上面分析的结论不就是获取锁成功了吗,可以将AQS中的独占线程设为自己了。但是如果其他线程修改成功了,这里使用 CAS 就会修改失败,因此就会进到 acquire 方法,注意这里传递的参数默认就是 1 ,对应着前面括号中的说明:

在这里插入图片描述

而 acquire 方法,就是 AQS 中的独占模式获取同步资源的逻辑,会调用当前方法的 tryAcquire 尝试获取资源,如果获取不到,则加入到 AQS 的阻塞队列并阻塞挂起线程。

在这里插入图片描述

关于acquire 方法的源码解读可以参考文章开始的链接中对 AQS 源码的解读。

3.2 FairSync.lock()

在 FairSync 公平锁中,由于需要遵循先进先出的原则,这里没有直接已粗暴的形式对 state 进行修改,而是直接调用了 AQS 中的 acquire 方法,而 acquire 方法又会调用当前类的 tryAcquire 获取资源。

但在当前类的 tryAcquire 方法中,如果获取到了资源,会接着进行判断当前线程的前面是否还有等待的线程,如果有则让出来让别人获取资源,因此就遵循了公平锁的原则,注意这里传递的参数默认就是 1 ,同样对应着前面括号中的说明:

在这里插入图片描述
同样 tryAcquire 尝试获取资源,如果获取不到,则加入到 AQS 的阻塞队列并阻塞挂起线程。

四、lock.unlock()

上面了解到了 lock 的逻辑,既然上锁了肯定需要解锁,下面点到 unlock() 方法中,可以看到直接使用了 Sync 的 release 方法释放资源,其实是 AQS 中的 release 方法,注意这里传递的参数默认就是 1 ,同样对应着前面括号中的说明:

在这里插入图片描述

在 AQS 的release 方法中,首先会调用 Sync 类的 tryRelease 释放资源,然后对已阻塞的线程进行唤醒:

在这里插入图片描述

关于release 方法的源码解读可以参考文章开始的链接中对 AQS 源码的解读。

五、总结

通过阅读 ReentrantLock 的源码可以发现,大量依赖于 AQS 中提供的方法,所以在阅读前一定要理解下 AQS 的作用和功能。

Condition 源码解读

一、Condition

并发情况下进行线程间的协调,如果是使用的 synchronized 锁,我们可以使用 wait/notify 进行唤醒,如果是使用的 Lock 锁的方式,则可以使用 Condition 进行针对性的阻塞和唤醒,相较于 wait/notify 使用起来更灵活。那 Condition 是如何实现线程的等待和唤醒的呢,本篇文章带领大家一起解读下 Condition 的源码。

在进行源码分析前,先回顾下 Condition 是如何使用的,例如下面一个案例:

public class Test {

    public synchronized static void main(String[] args) throws InterruptedException {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        new Thread(() -> {
            lock.lock();
            System.out.println("线程1开始等待!");
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程1被唤醒继续执行结束!");
            lock.unlock();
        }, "1").start();

        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.lock();
            System.out.println("开始唤醒线程!");
            condition.signal();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程2执行结束!");
            lock.unlock();
        }, "2").start();
    }
}

运行之后,可以看到下面日志:

在这里插入图片描述

由于在第一个线程中,使用的 condition.await() 因此当前线程会被阻塞挂起,而第二个线程,在 1s 后进行了 condition.signal() 操作,因此第一个线程会被唤醒继续执行。这里细心的小伙伴应该可以发现,第一个线程阻塞时锁并没有释放,而第二个线程在1s后也成功拿到锁了,所以表明在 condition.await() 时会自动释放当前锁,这点和 wait 相同,在第二个线程进行了 condition.signal() 操作,第一个线程并没有继续向下执行,而是等待第二个线程处理完才会继续执行,由此可以表明被唤醒的线程会重新获取锁,成功获取锁后继续执行。

下面通过源码看下 Condition 是如何实现的等待唤醒。

二、Condition 源码解读

2.1. lock.newCondition() 获取 Condition 对象

首先看下在使用 lock.newCondition() 获取一个Condition 对象时,具体做了什么,这里以 ReentrantLock 为例,进入到 ReentrantLock 的 newCondition() 方法中,又执行了 Sync 的 newCondition() 方法,再进去就会发现其实是 new 了一个 ConditionObject 类对象:

在这里插入图片描述
在这里插入图片描述

下面点到这个类中,可以看到其实是 AQS 下的一个子类:

在这里插入图片描述

2.2. condition.await() 阻塞过程

了解到 Condition 的对象后,可以看到是 AQS 下的一个子类,那下面其他的方法也肯定依赖于 AQS ,下面看下 condition.await() 方法,点到 await() 方法中:

在这里插入图片描述

其中 addConditionWaiter() 则是将自己加入到 AQS队列中,并获取到当前线程所在的 Node ,这里注意下 Node 的状态是 Node.CONDITION 也就是 -2,后面会依赖于该状态。

在这里插入图片描述
在这里插入图片描述

下面再回到 await() 方法继续向下看,接着使用了 fullyRelease 方法传入了当前的 Node ,这里的 fullyRelease 方法主要做了释放当前线程锁的操作,可以看到又调用了 AQS 的 release 进行释放资源,也就是释放了当前所持有的锁。

在这里插入图片描述

下面继续回到 await() 方法中,当释放锁后,下面进入到了 while 循环中,通过查看 isOnSyncQueue 方法,可以看到是符合while的条件也就可以进入到循环中:

在这里插入图片描述
在这里插入图片描述

在循环中可以明显的看到 LockSupport.park(this) ,将当前线程进行了阻塞。

2.3. condition.signal() 唤醒过程

上面已经看到线程被阻塞了,如果需要被唤醒则需要通过condition.signal(),这个方法是如何唤醒的呢?

下面来到 AbstractQueuedSynchronizer 类的 signal() 方法中:
在这里插入图片描述
主要执行了 doSignal 方法,再点到 doSignal 中,可以看到这里开启了一个循环,对链表的每一个元素都进行了 transferForSignal 操作,这里也比较好理解,就是要唤醒等待中的线程。

在这里插入图片描述
下面点到 transferForSignal 中,看下对每个 Node 都做了什么操作。点进去之后也比较好理解,如果状态是 Node.CONDITION 也就是 -2,刚才在解读 await 方法时就提到这个状态了,这里正好形成了呼应,下面有个非常显眼的操作 LockSupport.unpark(node.thread) 直接唤醒了目标线程。也就是唤醒了 2.2 中的最后一步操作。

在这里插入图片描述

2.4. condition.await() 被唤醒后

当 await() 方法中的 LockSupport.park(this) 被唤醒后,继续向下执行,下面会判断下当前线程有没有被打断,如果没被打断则 break 终止循环继续执行。

在这里插入图片描述
在这里插入图片描述

下面会使用 AQS 的 acquireQueued 方法,将先进入队列的线程进行抢占锁资源,如果成功获取锁后就会继续执行,如果抢占失败则继续被挂起阻塞。

在这里插入图片描述
在这里插入图片描述

三、总结

通过上面的源码分析,应该对 Condition 有了新的理解和掌握,细心地小伙伴应该可以发现在源码中好多地方都使用了 CAS ,因此当竞争资源非常激烈时, Lock 的性能要远远优于 synchronized

 

CopyOnWriteArrayList 

一、CopyOnWriteArrayList 源码解读

在 JUC 中,对于 ArrayList 的线程安全用法,比较推崇于使用 CopyOnWriteArrayList ,那 CopyOnWriteArrayList是怎么解决线程安全问题的呢,本文带领大家一起解读下 CopyOnWriteArrayList 的源码,主要对几个常用的函数进行讲解。

在进行 CopyOnWriteArrayList 的源码讲解之前,先看下同样实现了线程安全的 Vector ,很多文章都说不推荐使用 Vector ,其主要原因是性能太差了,那性能为什么这么差呢?可以看下 Vector add 和 get 的源码:

在这里插入图片描述
在这里插入图片描述

Vector 的添加和读取操作都被加上了 synchronized 锁,当并发情况下,因为锁的存在相当于变成了单线程的操作,所以效率肯定低,同样这样的优点就是保证了数据的唯一性,不会读取到脏数据。

下面再看下 CopyOnWriteArrayList 是如何解决并发问题的呢。

首先看下 CopyOnWriteArrayList 的全局变量有哪些:

在这里插入图片描述

其中 lock 锁就是每次在做写操作时,锁的句柄,array 就是具体存储数据的数组,注意这里的 array 被 volatile 所修饰,因此可以在并发情况下实现数据的可见性。

当 new 创建了一个 CopyOnWriteArrayList 时,如果是使用无参的构造函数,则将 array 的长度默认成 0 ,创建了一个空的数组。

在这里插入图片描述
在这里插入图片描述

在使用 add 添加数据时,先使用 lock 上锁,并获取到当前的 array 数组,然后对 array 进行 copyOf,新的数组的长度是之前的长度 +1 ,这样才能存放当前新的值,将新值填充后,再替换掉旧的 array 数组后,释放当前锁。

在这里插入图片描述

在使用 get 获取指定下边数据时,直接对当前的 array 进行操作:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在进行 remove 删除时,先使用 lock 上锁,然后再获取当前的 array 数组,如果传入的 index 正好是最后一个,那么 numMoved 计算出来就是 0 ,则使用 copyOf ,长度进行 -1 去除最后一个数据。否则传入的不是最后一个,先声明一个新的 array 数组,数组的长度就是旧的 array 的 len - 1,再将 0 到 index 的数据 arraycopy 至新的 array 数组,然后再将 index + 1 后的再 arraycopy 至新的 array 数组,最后将新的 array 数组替换旧的,然后释放锁。

在这里插入图片描述

二、总结

  1. 当 new 新建一个 CopyOnWriteArrayList 后会生成一个数组 array 来存放添加的内容,如果是无参的构造函数,则 array 的长度为 0 ,添加数据时再进行扩容。同时会声明一个 ReentrantLock 锁。
  2. 当进行 add 操作时,先进行上锁,然后对当前的 array 进行 copyOf,并且新的长度是之前的长度 +1 ,这样才能存放当前新的值,将新值填充后,再替换掉旧的 array 数组后,释放当前锁。
  3. 当使用 get 获取数据时,无需上锁,直接读取当前 array 数组的指定位置。
  4. 当使用 remove 时,同样先进行上锁,然后再获取当前的 array 数组,如果传入的 index 正好是最后一个,则使用 copyOf ,长度进行 -1 ,否则的话先声明一个新的 array 数组,现将0 到 index 的数据 arraycopy 至新的 array 数组,然后再将 index + 1 后的再 arraycopy 至新的 array 数组,最后将新的 array 数组替换旧的,然后释放锁。

读下来之后可以感觉出来 CopyOnWriteArrayList 的源码非常容易理解和阅读,同时我们也可以看出一些问题,CopyOnWriteArrayList 实现了写写隔离,但读读是可以共享的,这就有可能出现当某个数据再修改时,读进行了操作,导致读取到的还是旧的数据。还有就是每次写操作都对数组进行 Copy ,假如数据量非常大的情况下,进行 Copy 消耗的资源则会进行 x 2 ,因此使用 CopyOnWriteArrayList 时,需要考虑下自己的数据量以及读写的频次。

 

posted @ 2024-12-12 09:31  CharyGao  阅读(8)  评论(0编辑  收藏  举报