Java并发AQS原理分析(一)

我们说的AQS就是AbstractQueuedSynchronizer,他在java.util.concurrent.locks包下,这个类是Java并发的一个核心类。第一次知道有这个类是在看可重入锁ReentrantLock中,在ReentrantLock中有一个内部类Sync继承于AbstractQueuedSynchronizer,是ReentrantLock的核心实现。在并发包中的锁几乎都是基于AQS来构建的,但是在看源码的时候就会发现他们并没有直接继承AbstractQueuedSynchronizer,而是通过内部类Sync实现。

abstract static class Sync extends AbstractQueuedSynchronizer

这里注意的是AbstractQueuedSynchronizer是一个抽象类,定义了基本的框架。AQS核心是用一个变量state来表示状态.

AQS也就是AbstractQueuedSynchronizer这个类只是定义了一个队列管理线程,对于线程的状态是子类维护的,我们可以理解为师一个同步队列,当有线程获取锁失败时(多线程争用资源被阻塞时会进入此队列),线程会被添加到队列的队尾

总结:

  • AQS只是负责管理线程阻塞队列。
  • 线程的阻塞和唤醒

同步器是实现锁的关键(例如AQS队列同步器),利用同步器实现锁的定义。锁匙面向用户的,它定义了使用者和锁交互的接口,但是隐藏了实现的细节。同步器则是锁的实现,所以他是在锁的背后默默做着贡献,用户不能直接的接触到他,他简化了锁的实现方式,屏蔽了同步状态管理、线程之间的排队、等待、唤醒等操作。这样设计很好的隔离了使用者和实现者关注的领域。

上面的表示了队列的形态,head表示队列的头节点,tail表示队列的尾节点。在源码中他们的定义使用volatile定义的。使用volatile关键字保证了变量在内存中的可见性,详见:volatile关键字解析。保证某个线程在出队入队时被其他线程看到。

private transient volatile Node head;//头节点
private transient volatile Node tail;//尾节点

AbstractQueuedSynchronizer这个类中还有一个内部类Node,用于构建队列元素的节点类。



在AQS中定义了两种资源共享方式:

  • Exclusive:独占式
  • Share:共享式

    当以独占模式获取时,尝试通过其他线程获取不能成功。 多线程获取的共享模式可能(但不需要)成功。 当共享模式获取成功时,下一个等待线程(如果存在)也必须确定它是否也可以获取。 在不同模式下等待的线程共享相同的FIFO队列。

在不同的实现类中为了实现不同的功能,会采用不同的共享方式,例如可重入锁ReentrantLock采用的就是独占锁。

AQS的不同实现类,不需要关注线程等待队列的维护和管理(线程阻塞入队、唤醒出队),在AQS中这些是已经定义好的,不同的同步器只需要对以下方法进行实现即可:

//独占方式尝试获取资源
protected boolean tryAcquire(int arg)
//独占方式尝试释放资源
protected boolean tryRelease(int arg)
//共享方式尝试获取资源,返回值0表示成功但是没有剩余资源,负数表示失败,正数表示成功且有剩余资源
protected int tryAcquireShared(int arg)
//共享方式尝试释放资源
protected boolean tryReleaseShared(int arg)

所有自定义的同步器只需要确定自己是那种资源贡献方式即可:共享式、独占式。也可以同时实现共享式和独占式ReentrantReadWriteLock读写锁,多个线程可以同时进行读操作,但是只能有一个线程进行写操作。


独占模式同步状态获取:

首先先从代码开始执行的地方看:

以独占模式获取资源,忽略中断。(如果获取到资源,直接返回结果,否则进入等待队列,等待再次获取资源。) 通过调用至少一次tryAcquire(int)实现,成功返回。 否则线程排队,可能会重复阻塞和解除阻塞,直到成功才调用tryAcquire(int)

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

方法执行的顺序:

  • 调用tryAcquire()方法尝试去获取资源,具体在子类中进行实现
  • 调用addWaiter()方法把当前线程标记为独占式,并加入到队列的尾部

这里需要讲一下addWaiter()方法中的第一个参数,线程等待队列中的元素都是利用Node这个内部类存储的,在Node中有两个成员变量分别声明了资源共享方式:

        static final Node SHARED = new Node();//共享式
        static final Node EXCLUSIVE = null;//独占式
  • 调用acquireQueued()方法,让线程在队列中等待获取资源,获取资源后返回,如果在这个等待过程中线程被中断过,返回true,否则返回false


在方法中首先调用tryAcquire(int)方法,该方法在AbstractQueuedSynchronizer并没有实现,需要子类去实现:

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

第二步调用addWaiter()方法:该方法是负责维护线程等待队列的方法,所以在AbstractQueuedSynchronizer中实现了该方法:具体是创建了一个节点类,把节点放在队尾,如果失败调用enq(node)方法(队尾节点为空)。

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;
    }

上面的方法判断,如果添加到队尾失败

enq()方法:

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //如果队列为空(队尾元素为空)创建节点添加进去
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    //把tail指向head
                    tail = head;
            } else {
                //正常添加到队尾
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

在上面的代码中添加节点都用到了比较和交换(CAS,可以说是一种在并发环境下的解决方法),compareAndSetTail()方法能够确保节点能被安全的添加进队列中,在多线程环境下无法保证一个元素被正确的添加到队列的尾部。因为进入队列的元素都是放在队尾的,为了保证数据的正确性,所以在设置尾节点的时候使用CAS

第三步调用acquireQueued()方法,目的是为了在队列中等待被唤醒使用资源,因为之前的操作失败后,线程会被放入队尾,队列是先进先出的结构,所以在队尾的线程必须等待被唤醒。方法中主要有一个死循环,我们称他叫自旋,只有当条件满足的时候,获得同步状态,退出自旋。

acquireQueued()方法:

final boolean acquireQueued(final Node node, int arg) {
        //设置成功标记
        boolean failed = true;
        try {
            //设置中断标记
            boolean interrupted = false;
            for (;;) {
                //获得node的前驱节点
                final Node p = node.predecessor();
                //判断前驱结点是否是头节点
                if (p == head && tryAcquire(arg)) {
                    //把node设置为头结点
                    setHead(node);
                    //把p节点的前驱设置为null,见下面的解释
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //判断是否继续等待
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

把p节点的前驱设置为null,也就是之前的head节点,在上面源码中后面的注释标记为help GC功能,解释一下:在调用上面的setHead()方法的时候,方法的内部已经将当前节点的前驱结点设置为null,在这里再次设置一遍,为了保证当前节点的前驱结点顺利被回收(当前节点设置为头节点,那么之前的头节点就要被释放,模拟一个正常的出队过程)。自己画图更好理解。

setHead()方法:

    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

这里分析上面调用的acquireQueued()方法

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //获取前驱节点的状态
        int ws = pred.waitStatus;
        //如果当前节点状态值为SIGNAL这个值,代表当前线程应该被挂起,等待被唤醒
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            //如果大于0代表将当前节点的前驱节点移除
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //小于0时把前驱结点状态值设置为SIGNAL,目的是为了前驱判断后将当前节点挂起(通知自己一下)
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

在这里我们需要看一下Node这个类中定义的关于状态值的定义:

        //表示线程已取消,作废状态
        static final int CANCELLED =  1;
        //表示后继节点应该等待当前节点释放资源后唤醒其后继节点
        static final int SIGNAL    = -1;
        //表示当前正处于等待状态
        static final int CONDITION = -2;
        //表示状态需要向后传播
        static final int PROPAGATE = -3;
  • CANCELLED 取消状态
  • SIGNAL 等待触发状态
  • CONDITION 等待条件状态
  • PROPAGATE 状态需要向后传播

等待队列是FIFO先进先出,只有前一个节点的状态为SIGNAL时,当前节点的线程才能被挂起。 所以在方法调用的时候把前驱结点设置为SIGNAL。

因为前一节点被置为SIGNAL说明后面有线程需要执行,但是还轮不到它后面的线程执行,后面线程一定要找一个前驱节点不为CANCEL的节点,然后把它设置为SIGNAL然后原地挂起,等待唤醒。 因为SIGNAL执行完了会唤醒紧接着的后面一个。



总结:

AQS中定义的acquire()模板方法,具体通过调用子类中的tryAcquire()方法尝试去获取资源,成功则返回,失败调用addWaiter()将当前线程添加到阻塞队列的队尾,同时标记为独占状态。acquireQueued()方法通过自旋获取同步状态(该方法使线程在等待队列中等待休息,当有机会时尝试获取资源),节点尝试获取资源的条件是当前节点的前驱节点是头节点,尝试获取到资源后才返回,在整个等待过程中如果发生过中断,不做响应,在获取资源后调用selfInterrupt()方法设置中断。


独占模式下同步状态的释放:

上面根据源码分析了独占模式下获得锁的过程主要调用了模板方法acquire()方法向下分析,接着我们分析它的相反的方法,独占模式下释放锁的过程,还是一个模板方法release()

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            //得到头节点
            Node h = head;
            //判断头节点不为空,状态值符合条件
            if (h != null && h.waitStatus != 0)
                //唤醒下一个等待线程
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease()方法依然需要子类去自己实现

protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

unparkSuccessor()方法:

private void unparkSuccessor(Node node) {
        //获得当前线程的状态值
        int ws = node.waitStatus;
        if (ws < 0)
            //小于0时置零
            compareAndSetWaitStatus(node, ws, 0);
        //获得当前节点的后继节点
        Node s = node.next;
        //判断为空和状态值是否大于0
        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);
    }

unpark()方法唤醒的是等待队列中最前面的线程,之后会再次执行上面的过程。

总结:在获取同步状时,在使用者的角度看在使用锁时,同步器会维护一个同步队列,获取状态失败的线程会被加入这个队列并进行自旋;当该节点的前驱节点是头节点的时候并且获得了同步状态时移出队列。在释放的时候,调用tryRelease()释放并唤醒后继节点。

原文地址:https://www.cnblogs.com/duzhentong/p/8822568.html

时间: 2024-10-08 01:30:22

Java并发AQS原理分析(一)的相关文章

Java并发编程原理与实战

Java并发编程原理与实战网盘地址:https://pan.baidu.com/s/1c3mpC7A 密码: pe62备用地址(腾讯微云):https://share.weiyun.com/11ea938c7ad43783a934ed1d492eed8d 密码:ogHukS 原文地址:http://blog.51cto.com/13406637/2071116

Java并发编程原理与实战视频教程

14套java精品高级架构课,缓存架构,深入Jvm虚拟机,全文检索Elasticsearch,Dubbo分布式Restful 服务,并发原理编程,SpringBoot,SpringCloud,RocketMQ中间件,Mysql分布式集群,服务架构,运 维架构视频教程 14套精品课程介绍: 1.14套精 品是最新整理的课程,都是当下最火的技术,最火的课程,也是全网课程的精品: 2.14套资 源包含:全套完整高清视频.完整源码.配套文档: 3.知识也 是需要投资的,有投入才会有产出(保证投入产出比是

Java并发编程原理与实战十九:AQS 剖析

一.引言在JDK1.5之前,一般是靠synchronized关键字来实现线程对共享变量的互斥访问.synchronized是在字节码上加指令,依赖于底层操作系统的Mutex Lock实现.而从JDK1.5以后java界的一位大神—— Doug Lea 开发了AbstractQueuedSynchronizer(AQS)组件,使用原生java代码实现了synchronized语义.换句话说,Doug Lea没有使用更“高级”的机器指令,也不依靠JDK编译时的特殊处理,仅用一个普普通通的类就完成了代

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

前面我们说到多线程带来的风险,其中一个很重要的就是安全性,因为其重要性因此,放到本章来进行讲解,那么线程安全性问题产生的原因,我们这节将从底层字节码来进行分析. 一.问题引出 先看一段代码 package com.roocon.thread.t3; public class Sequence { private int value; public int getNext(){ return value++; } public static void main(String[] args) { S

[Java并发] AQS抽象队列同步器源码解析--锁获取过程

要深入了解java并发知识,AbstractQueuedSynchronizer(AQS)是必须要拿出来深入学习的,AQS可以说是贯穿了整个JUC并发包,例如ReentrantLock,CountDownLatch,CyclicBarrier等并发类都涉及到了AQS.接下来就对AQS的实现原理进行分析. 在开始分析之前,势必先将CLH同步队列了解一下 CLH同步队列 CLH自旋锁: CLH(Craig, Landin, and Hagersten locks): 是一个自旋锁,能确保无饥饿性,提

Java 中 ConcurrentHashMap 原理分析

一.Java并发基础 当一个对象或变量可以被多个线程共享的时候,就有可能使得程序的逻辑出现问题. 在一个对象中有一个变量i=0,有两个线程A,B都想对i加1,这个时候便有问题显现出来,关键就是对i加1的这个过程不是原子操作.要想对i进行递增,第一步就是获取i的值,当A获取i的值为0,在A将新的值写入A之前,B也获取了A的值0,然后A写入,i变成1,然后B也写入i,i这个时候依然是1. 当然java的内存模型没有上面这么简单,在Java Memory Model中,Memory分为两类,main

Java 线程池原理分析

1.简介 线程池可以简单看做是一组线程的集合,通过使用线程池,我们可以方便的复用线程,避免了频繁创建和销毁线程所带来的开销.在应用上,线程池可应用在后端相关服务中.比如 Web 服务器,数据库服务器等.以 Web 服务器为例,假如 Web 服务器会收到大量短时的 HTTP 请求,如果此时我们简单的为每个 HTTP 请求创建一个处理线程,那么服务器的资源将会很快被耗尽.当然我们也可以自己去管理并复用已创建的线程,以限制资源的消耗量,但这样会使用程序的逻辑变复杂.好在,幸运的是,我们不必那样做.在

AQS原理分析

一,AQS原理 lock最常用的类就是ReentrantLock,其底层实现使用的是AbstractQueuedSynchronizer(AQS) 简单来说AQS会把所有的请求线程构成一个CLH队列,当一个线程执行完毕(lock.unlock())时会激活自己的后继节点,但正在执行的线程并不在队列中,而那些等待执行的线程全部处于阻塞状态,经过调查线程的显式阻塞是通过调用LockSupport.park()完成,而LockSupport.park()则调用sun.misc.Unsafe.park(

Java程序运行原理分析

class文件内容 class文件包含Java程序执行的字节码 数据严格按照格式紧凑排列在class文件的二进制流,中间无分割符 文件开头有一个0xcafebabe(16进制)特殊的标志 JVM运行时数据区 线程独占: 每个线程都会有它独立的空间,随线程的生命周而创建和销毁 线程共享: 所有线程都能访问这块内存数据,随虚拟机或GC而创建和销毁 方法区 方法区是各个线程共享的内存区域 用于存储已被虚拟机加载的类信息, 常量,静态变量, 即时编译后的代码等数据 虽然Java虚拟机规范把方法区描述为堆