java并发编程的艺术(6)深入挖掘aqs独占锁源码

简介: java并发编程的艺术(6)深入挖掘aqs独占锁源码

并发编程里面的很多常用的类,例如ReentrantLock,Semaphore,CountDownLatch实际上底层都是通过使用AbstractQueuedSynchronizer(AQS)来进行实现的。那么今天我们就来仔细聊聊AQS这样东西。


底层的核心主要是维护一个volatile int waitStatus的状态值,以及一个FIFO线程等待队列。对于waitStatus变量,AQS里面提供了三种方式:


  • getState()
  • setState()
  • compareAndSetState()


同时在AQS里面定义了两种对于资源访问的方式,独占模式和共享模式。简单来说,独占模式就是一次只能由一个线程执行,例如常见了ReetrantLock,共享模式则是允许多个线程同时执行,例如说Semaphore,CountDownLatch。


AQS同步器里面经常会用到以下的几种方法:


//独占模式中常用的一种获取资源的函数
boolean tryAcquire(int arg)
//独占模式中常用的一种释放资源的函数
boolean tryRelease(int arg)
//该线程是否正在独占资源。只有用到condition才需要去实现它
boolean isHeldExclusively()
//共享模式中常用到的获取资源的方式
int tryAcquireShared(int arg)
//共享模式中常用到的释放资源的方式
boolean tryReleaseShared(int arg)
复制代码


源码分析模块:


我们首先来模拟一个场景进行思维导向。


在独占模式下一个线程请求获取资源的过程:


1.首先是进入aqs里面的public final void acquire(int arg) 这个函数中

来看看里面的源码先:


public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
复制代码


首先是通过tryAcquire()请求获取资源,如果成功则直接结束。


addWaiter()函数通过阅读代码可以明白,它实际上是将当前线程放在了请求的队列最尾端


private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}
复制代码


acquireQueued()函数里面的内容,通过阅读源码也可以明白其中的含义:


final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
复制代码


通过一个for循环来不断地tryAcquire()来获取资源


再继续看源码tryAcquire()部分,这里面的代码内容是抛出一个异常,因为AQS本身是一个框架,对于具体的获取资源处理,它将其交给了开发者去自定义处理。


同步队列


同步器内部的数据结构其实是一个基于FIFO原则来设计的双向队列:


网络异常,图片无法展示
|


对于其中的每个node节点,都有一个叫做waitStatus的变量来表示当前线程的状态。查看源码可以看到这四种状态:


网络异常,图片无法展示
|


  1. CANCELLED:表示被中断或者取消的状态
  2. SIGNAL:表示前面的节点已经释放了同步锁,等待被唤醒的一个状态
  3. CONDITION:表示处于等待状态
  4. PROPAGATE:表示处于共享模式中的运行状态


AQS里面对于插入队尾的操作和我们常用的list集合插入操作有所出入,为了保证队列操作的原子性和有序性,源码里面采用了cas自旋的方式来实现。


private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
复制代码


这一段代码和核心还是使用了unsafe类里面的接口来指定内存进行分配。

通过对于上述代码的分析,我们来一段小结先:


当多个线程同时发出请求的时候,会发生资源竞争(tryAcquire()和addWaiter()),所有请求的线程会按照先来后到的规矩排成一条队列,刚刚上述的分析主要是针对于获取锁的过程而言。那么对于未获取到资源的线程而言,下一步又应该处理什么呢?


接着我们来看到上边提到的acquireQueued()函数:


final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // 将引用对象置空,有利于GC             
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
复制代码


final Node p = node.predecessor();是获取当前等待节点的前一个节点,也就是即将释放锁的节点元素。这里面的核心主要还是通过for循环的方式进行自旋操作,不断地请求资源,一方获取资源成功之后,便将该节点的next和pre还有thread先置空,然后进入shouldParkAfterFailedAcquire(p, node)当中,继续深入源码分析:


这里可以结合我们前边提及到的waitStatus状态数值来进行分析


private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * 节点处于待唤醒状态
         */
        return true;
    if (ws > 0) {
        /*
         * 前驱节点被中断了,于是继续查找前驱节点,查看是否有元素满足要求
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * 这里面也是调用了unsafe类里面的cas操作来进行节点状态的更新
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
复制代码


直到进行到parkAndCheckInterrupt()函数里面,才算是真正的让该线程进入休眠状态。


private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}
复制代码


说完了获取锁的整个思路,再来看看锁的释放吧。


在同步队列里面的node节点元素汇总,有个waitStatus变量:


volatile int waitStatus;


这里之所以用volatile来进行修饰的原因,我个人认为是volatile修饰的关键字在进行修改了之后,会向其他线程发送信号,重新从主存中读取该值,保证了该变量在各个线程之间的可见性。


private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}
复制代码


这段代码中的核心部分为LockSupport.unpark(s.thread);主要是起到解除阻塞线程的一个作用,通俗点来说,就是唤醒队列中正在等待的下一个节点。


结合上述的独占式模式的一个分析思路,对于共享模式的分析也是大同小异了。

目录
相关文章
|
2月前
|
Java 程序员
Java编程中的异常处理:从基础到高级
在Java的世界中,异常处理是代码健壮性的守护神。本文将带你从异常的基本概念出发,逐步深入到高级用法,探索如何优雅地处理程序中的错误和异常情况。通过实际案例,我们将一起学习如何编写更可靠、更易于维护的Java代码。准备好了吗?让我们一起踏上这段旅程,解锁Java异常处理的秘密!
|
2月前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
2月前
|
算法 Java 调度
java并发编程中Monitor里的waitSet和EntryList都是做什么的
在Java并发编程中,Monitor内部包含两个重要队列:等待集(Wait Set)和入口列表(Entry List)。Wait Set用于线程的条件等待和协作,线程调用`wait()`后进入此集合,通过`notify()`或`notifyAll()`唤醒。Entry List则管理锁的竞争,未能获取锁的线程在此排队,等待锁释放后重新竞争。理解两者区别有助于设计高效的多线程程序。 - **Wait Set**:线程调用`wait()`后进入,等待条件满足被唤醒,需重新竞争锁。 - **Entry List**:多个线程竞争锁时,未获锁的线程在此排队,等待锁释放后获取锁继续执行。
85 12
|
2月前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
223 2
|
2月前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
2月前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
72 3
|
2月前
|
开发框架 安全 Java
Java 反射机制:动态编程的强大利器
Java反射机制允许程序在运行时检查类、接口、字段和方法的信息,并能操作对象。它提供了一种动态编程的方式,使得代码更加灵活,能够适应未知的或变化的需求,是开发框架和库的重要工具。
87 4
|
2天前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
35 14
|
5天前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
34 13
|
6天前
|
安全 Java 开发者
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。