JDK1.8源码学习篇一——AbstractQueuedSynchronizer学习笔记

5 篇文章 0 订阅
本文详细介绍了Java并发库中的核心类AbstractQueuedSynchronizer(AQS),它是ReentrantLock等同步组件的基础。AQS通过管理同步状态和线程的FIFO等待队列,实现线程的阻塞和唤醒。文章重点讲解了AQS的基本数据结构、获取和释放锁的源码分析,以及公平锁和非公平锁的区别。
摘要由CSDN通过智能技术生成

一、引言

      最近想深入学习一下java并发的基础知识,总感觉在对java.util.concurrent一知半解,很多东西好像只有点印象,这次接着学习源码的机会来深入了解一下。那么为什么把AbstractQueuedSynchronizer这个类放在最前面呢?其实相信很多人跟我有同样的经历,首先学习的是ExecuteService线程池,然后学习里面的工厂类Executors,和其中的submit、execute方法,但是学到一半的时候,发现里面很多东西用的都是锁的概念,比如我们最常用的synchronized关键字和ReentrantLock类,这里对这个也不是很了解,然后想继续学习下去,又对ReentrantLock这个类源码学习一下,最后,最后发现其中一个很核心的类AbstractQueuedSynchronizer,学习完之后,又回过头来进行总结一下,发现这个类有很多的实现子类,都是我们常用的,那么索性先把这个类放在最前面,把其中相关的方法都学习一遍。

二、AQS详解

   首先,什么是AQS呢?AQS(AbstractQueuedSynchronizer)框架就是提供了一个自动管理同步状态、阻塞和非阻塞线程,以及等待队列的通用机制。内部主要包含了两个内部类一个是Node和ConditionObject,其中基于node节点构建了一个FIFO的队列,采用的是链表的数据结构进行存储。首先我们看下该类的主要结构:

public abstract class AbstractQueuedSynchronizer  extends AbstractOwnableSynchronizer implements java.io.Serializable {
     static final class Node {}
     public class ConditionObject implements Condition, java.io.Serializable {}
}

可以看到其中包含了两个内部类,Node主要用作数据结构,用来保存对应的节点信息。这里面我们接着看这个抽象方法中几个比较重要的方法,tryAcquire/tryRelease/tryAcquireShared/tryReleaseShared/isHeldExclusively这个5个方法,可以看到这个5个方法都是protected类型的,所以可以让子类直接去实现它,这几个方法都没有对应的方法体,因此对应的逻辑可以写在子类中。通过tryAcquire可以获取锁,而通过tryAcquire可以去直接释放锁。AOS是一个典型的模板方法设计模式的典型运用案例,AQS为一个抽象类,却没有抽象方法,所有的方法具体实现都要子类去实现。它提供了:如何让现场如队列,如何让现场出队列,现场如何等待和转移等模板方法,而子类需要做的事情,就是如何决定线程出入队列。

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

    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }

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

    protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }

三 AQS的原理

     AQS使用的是一个int类型的成员变量state来表示当前的同步状态,当state>0的时候表示已经获取到了锁,当state=0的时候,表示释放了锁,它共提供了3个方法getState(),setState()和compareAndSetState()来对同步的状态state进行操作,整个过程采用的是原子操作,保证安全性。

      AQS通过内置的同步队列来完成线程的排队工作,如果当前线程获取锁失败的时候,会将当前线程以及等待状态信息的构造成一个节点,加入同步队列中,同时会阻塞当前线程,当同步锁释放的时候,则会把节点中的线程唤醒,使得其获得锁。

四、ReentrantLock简介

      ReentrantLock的功能是实现代码的并发访问控制,实际上是一种排他锁,这里我们要用到的其中两个不同的锁,就NofaiSync和FairSync,就是通常所说的公平锁和非公平锁,默认的是采用非公平的锁。这里我们可以这个类的结构,其中几个几个内部类,其中Sync是一个抽象类,并且继承了AbstractQueuedSynchronizer,而NonfairSync和FairSync则是Sync的子类。两个子类都实现了tryAcquire方法用来获取锁,下图就是其中包含的主要方法和内部类名。


五、AQS的源码解析

   1. 基本数据结构和知识点介绍

这里借助ReentrantLock的入口,先来了解下AQS的原理,首先来看下比较重要的一个数据结构Node节点的定义,这个用来存储等待线程的状态和信息,形成一个等待队列,具体的代码如下面所示

 static final class Node {
     
        static final Node SHARED = new Node();  //标识当前节点是共享模式
        static final Node EXCLUSIVE = null;      //标识当前节点是排他模式

      
        //标识线程的等待状态,一共有四种状态,
        static final int CANCELLED =  1;   //表示等待线程节点已经取消
        static final int SIGNAL    = -1;   //表示当前节点需要被激活
        static final int CONDITION = -2;   //表示当前节点等待在条件队列
        static final int PROPAGATE = -3;   //表示当前节点的后续节点的获取acquireShared被无条件执行

        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() {   
        }

        Node(Thread thread, Node mode) {     等待队列使用的节点
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // 条件队列使用的节点
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

以上是使用的数据结构的定义,在AQS还有结构非常重要的内部变量,在初始化的AQS的队列中,head和tail默认是null。

    private transient volatile Node head;  //等待队列的head节点,只有当调用的setHead方法的时候才进行加载,属于懒加载
  
    private transient volatile Node tail; //等待状态的尾节点
  
    private volatile int state; //AQS队列的状态

其实上面构造的就是一个节点,这个我们可以用数据结构中的链表来表示,大致构造的如下图所示,在等待队列中有个头结点head和一个尾节点tail分别指向队列的头和尾,这是一个双向链表。


    在学习源码的时候还遇到很多地方使用到了函数CompareAndSwap或者CompareAndSet方法,这些方法都是调用JDK的内部类unsafe,是基于硬件保证原子更新的,如果这个值是expect就更新为update,是无锁并发的基础。

protected final boolean compareAndSetState(int expect, int update) {
     return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

2. 获取锁的源码分析

  首先看下程序的入口,先构建一个锁,只要执行方法lock()即可获取锁,首先来看下ReetrantLock这个锁的构造函数

ReentrantLock lock =  new ReentrantLock();
lock.lock();
lock.unlock();
  public ReentrantLock() {
        sync = new NonfairSync();  //非公平锁
    }

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync(); //公平锁
    }
   //这个方法获取锁,这里用的syn变量,这个公平锁和非公平锁的父类
    public void lock() {
        sync.lock();
    }

在这里,有公平锁和非公平锁之分,其中公平锁中,每个线程抢占锁的顺序为先后调用lock()方法的顺序依次获取锁,而非公平锁中每个线程抢占锁的顺序不确定,先获取锁,获取不到,加入到等待队列中,和调用方法lock()的顺序无关。所以这个地方在公平锁中,等待最短的时间肯定是最先获取到锁,反之在非公平锁中,等待最长的时间的线程,可能更加的有机会获取到锁。这里,我们以公平锁的源码进行分析,其实调用的是Sync的子类,FairSync和NonfairSync

 abstract static class Sync extends AbstractQueuedSynchronizer {}
 static final class FairSync extends Sync {}
 static final class NonfairSync extends Sync {}

可以看到,这个方法里面调用了acquire()方法,并传递了一个参数1,表示需要获取锁

 final void lock() {
     acquire(1);
  }

接着往下看这个方法的内容,这个方法用了调用了两个方法作为判断是否获取到锁,其中tryAcquire 方法是AbstractQueuedSynchronizer 这个类的protected方法,由其子类去实现它,从这个&&操作符可以判断,只有当获取锁失败的的时候才会去执行下面一个方法acquireQueued(),构造一个Node,并添加到等待队列中。其实这个方法是阻塞式的。

AQS获取独占锁

1. 尝试去获取锁,tryAcquire(),获取成功,直接返回,获取失败,进行后一步操作

2.没有获取成功,把线程加入到等待队列中,当前线程中断

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

首先tryAcquire(int arg)这个方法的具体实现如下

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();  //获取当前队列的状态
    if (c == 0) { //发现所没有被获取
    if (!hasQueuedPredecessors() &&  //这个方法点进去很简单,只有一个判断,是否存在比当前线程等待更加的线程,其实就是head==tail是否成立
        compareAndSetState(0, acquires)) {  //没有的话调用CAS进行更新状态,自己获取锁
           setExclusiveOwnerThread(current);  //
           return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {  //这种状态需要判断一下,此时如果获取锁的线程等于exclusiveownerThread,则将state+1
            int nextc = c + acquires;                 //这就是重入锁的实现方式
            if (nextc < 0)
               throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
     }
     return false;
 }

其中三个方法hasQueuedPredecessors/compareAndSetState分别用来判断和更新状态,如果此处获取失败,回到上个方法,需要进行阻塞,首先是把当前节点加入到等待队列中,首先来看下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) {  //当前队列的尾节点,如果不为null,说明已经存在了等待节点,则把该节点加入到等待队列中
            node.prev = pred;    //这个就是链表尾部添加节点很常用的操作了,把tail节点设置成当前节点的前驱,并把tail的next属性指向自己
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);  //上一步是判断尾部节点是否null,如果为null的情况下该怎么加入呢
        return node;
    }
  private Node enq(final Node node) {
        for (;;) { //for循环,一定要保证等待队列构造成功
            Node t = tail;
            if (t == null) { // 发现尾部节点为null一定要构造节点设置成头节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {    //第二次循环的时候,此时已经有了头节点,把当前节点的前驱节点设置成头节点,并把自己设置成tail节点,添加到队列的最后面
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
这就要用到上面的enq()方法了,开始构造等待队列,通过上面的方法,当前节点会被存放到等待队列中,接下来看下AQS中的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);
        }
    }
下面来说下shouldParkAfterFailedAcquire这个方法,这个方法有点自旋锁的味道,他会先判断你是不是头结点,如果不是头节点的时候,在判断你的前驱节点的状态,判断是否需要挂起,如果前驱节点的状态是signle,这个线程将被park。直到另外的线程release的时候,并且next节点等于当前节点的时候,才进行unpark操作。
 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)  //判断前驱节点的状态是否是single,来觉得是否挂起当前线程,前置节点执行完,即可唤醒当前节点,此时只要安全的挂起即可
            return true;
        if (ws > 0) {
            do {//ws>0,说明前置节点自己取消同步状态,不需要获取锁,所以通过while循环一直往前寻找,知道找到>0的节点
                node.prev = pred = pred.prev;  //前驱节点被取消,跳过所有取消的前驱节点
            } while (pred.waitStatus > 0);   //这样做的目的是去掉同步队列中已经取消了同步的节点
            pred.next = node;
        } else {
            //如果是其他的状态,则需要把当前的节点设置成signle,所以需要确保挂起之前不能获得同步状态
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

通过上面的操作的操作,如果一个新的线程过来,是否获取到锁,如果没有,则进入等待队列中等,获取锁则返回,这里还有一个方法cancelAcquire没讲?当初看到这里有个疑问,每个线程调用一次lock()方法去尝试获取锁,然后获取失败之后,加入到等待队列中,然后中断自己本身,那么问题来了?后面等待的线程如何又唤醒自己呢?在代码中也没看到这方面的逻辑,这里先不说,先看锁的释放逻辑。

AQS释放所独占锁

这里我们先看下锁的释放逻辑,调用unlock()方法,起调用的是tryRelease方法来释放锁。这里根据先后调用的顺序,先看下tryRelease方法

public void unlock() {
    sync.release(1);
 }
public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h); //释放锁成功,唤醒后稷节点
            return true;
        }
        return false;
    }

具体的处理过程如下:

1. 首先等待队列的状态减1,然后判断是否是自己

2. 更新状态,释放锁

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;   //释放一个锁,当前的状态减掉1
            if (Thread.currentThread() != getExclusiveOwnerThread())  //只有自己才能释放自己
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) { //说明已经无锁
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c); //释放一个锁,减掉1,也就是说加锁多少次,就要释放多少次
            return free;
        }
接下来看下另外一个方法unparkSuccessor来唤醒后继线程
private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;  获取当前节点的状态
        if (ws < 0) //说明当前节点的状态并不是已经取消,那么把节点状态设置成0
            compareAndSetWaitStatus(node, ws, 0);

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

总结下AQS释放锁的过程,释放的过程分为2步,第一是首先释放自己占有的锁,调用tryrelease方法,如果失败,则返回,如果释放锁成功,就拿到队列的头结点。第二是根据拿到的头结点,找到第一个后继的有效结点,然后将其从等待队列中移除,最后激活对应的线程。

节点的取消

下面看下节点的取消过程,主要是方法cancelAcquire,看下起主要的源码,其实主要就是状态设置和链表中取消一个节点的设置操作。

 private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;
        node.thread = null;

        // Skip cancelled predecessors
        Node pred = node.prev;  //这个过程和之前的类似,都是获取节点的有效想的前驱节点
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev; 
        Node predNext = pred.next;   //找到node节点的后继节点

        node.waitStatus = Node.CANCELLED;  //同时把状态设置成取消

        // If we are the tail, remove ourselves.
        if (node == tail && compareAndSetTail(node, pred)) {//如果node是尾节点,就把node的前驱节点设置为尾节点,同时把对应的next设置成null
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {  //如果node的前驱节点不是头结点,则把状态设置成signle
                Node next = node.next;  //找到node的候机节点
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next); //同时进行连接,取消node的节点
            } else {
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

     以上就是AbstractQueuedSynchronizer这个类中关于获取锁和释放锁的主要的方法和流程,他是通过构造一个队列来获取锁和释放锁,当当前线程执行完毕之后,会释放当前锁,同时在激活下一个线程,还有一部分方法这里没有列出来,不如非公平锁,共享锁等,其中就是主体方法都是一样的,不过是加了一些逻辑判断,这里不再一一列出了...











评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值