Java并发编程知识点总结(八)——AQS源码分析之独占锁

(一)、AQS概述

AbstractQueuedSynchronizer简称AQS,它是java.util.concurrent包中,它提供了一套完整的同步编程框架。我们常用的ReentrantLock、CountDownLatch都是基于AQS实现的。
AQS的实现分为两种形式,一种是独占锁,另一种则是共享锁。

  • 独占锁:每次只能有一个线程持有锁。我们比较熟悉的ReentrantLock就是通过独占锁实现互斥性的。
  • 共享锁:允许多个线程同时获取锁,并发地访问资源。例如ReentrantReadWriteLock。

在这篇文章中,我们先来分析一下AQS的独占锁机制。

(二)、AQS内部实现

  1. AQS的实现是底层底层维护了一个先进先出(FIFO)的双向队列,这个队列是基于链表实现的。如果线程竞争锁失败,那么就会进入到这个同步队列中进行等待。当获得锁的线程释放锁之后,会从队列中唤醒一个线程。
  2. 双向队列是基于Node节点实现的,当线程需要入队列的时候,会将线程的信息封装成一个Node对象,进行入队操作。
  3. 属性head就标记头节点,属性tail标记尾节点。
 static final class Node {
        //方式一:标记为共享锁(mode)
        static final Node SHARED = new Node();
        //方式二:标记为独占锁(mode)
        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;

		//在同步队列中的等待状态
        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) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }
	//头节点
    private transient volatile Node head;

	//尾节点
    private transient volatile Node tail;

	//标记状态
    private volatile int state;

在这里插入图片描述

(三)、锁的获取

获得独占锁的源头就是从acquire()方法开始的。这个方法中分别调用了三个方法:tryAcquire()、addWaiter()、acquireQueued()。

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

首先调用的是tryAcquire(arg),这个方法只抛出了一个异常,因为它的具体实现是交给子类去完成的。这个方法的主要功能是:尝试获得锁,进入临界区。 我们只要知道这个即可。

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

根据acquire()方法中,tryAcquire()方法获取锁失败,就会进入addWaiter()方法,这个方法的主要功能是:将线程封装成Node节点并加入到同步队列中。 注意acquire方法中传入的参数Node.EXCLUSIVE,代表使用独占锁。

private Node addWaiter(Node mode) {
		//将线程的信息封装成Node节点
        Node node = new Node(Thread.currentThread(), mode);
        // 获得队尾节点
        Node pred = tail;
        //如果队尾节点不为null
        if (pred != null) {
        	//设置新节点的前驱节点是队尾节点
            node.prev = pred;
            //通过CAS操作将新节点设置为队尾节点
            if (compareAndSetTail(pred, node)) {
                //如果成功,就将队尾节点的后继节点设置为新节点
                pred.next = node;
                return node;
            }
        }
        //如果这个同步队列是空队列或者CAS失败,那就调用enq()方法
        enq(node);
        return node;
    }

通过上面的代码我们知道,如果这个同步队列是空队列或者CAS失败,那就调用enq()方法。

private Node enq(final Node node) {
		//死循环,相当于自旋操作
        for (;;) {
        	//获取队列的尾节点
            Node t = tail;
            //如果是个空队列
            if (t == null) { // Must initialize
            	//CAS操作创建一个新节点为头节点
                if (compareAndSetHead(new Node()))
                    //设置为尾节点
                    tail = head;
            } else {
            	//设置新节点的前驱节点为尾节点
                node.prev = t;
                //尝试将node节点设置为尾节点
                if (compareAndSetTail(t, node)) {
                    //设置t节点的后继节点为node
                    t.next = node;
                    return t;
                }
            }
        }
    }

然后到这里为止我们就走完了addWaiter,然后又会回到acquire()方法,调用acquireQueued(),这个方法的功能是:尝试成为头节点,也就是尝试获得锁。

final boolean acquireQueued(final Node node, int arg) {
		//标记是否失败
        boolean failed = true;
        try {
        	//标记是否被中断
            boolean interrupted = false;
            //死循环,自旋操作
            for (;;) {
            	//获得node节点的前驱节点
                final Node p = node.predecessor();
                //判断p是否是头节点,当p是头节点,p才有可能参加锁竞争
                //如果p是头节点了,那就会调用tryAcquire()方法尝试获得锁
                if (p == head && tryAcquire(arg)) {
                	//获得锁成功,将node设置为头节点
                    setHead(node);
                    //将p节点出队列
                    p.next = null; // help GC
                    //标记为成功
                    failed = false;
                    return interrupted;
                }
     //shouldParkAfterFailedAcquire()根据节点的waitStatus()来决定是否挂起线程
     //parkAndCheckInterrupt()是将线程挂起的方法
     //这两个方法后面都会解释
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
        	//抛出异常
            if (failed)
            	//进行出队列操作。
                cancelAcquire(node);
        }
    }

分析完上面的代码,我们来进入shouldParkAfterFailedAcquire(p, node)方法,这个方法的主要功能是:根据节点的waitStatus()来决定是否挂起线程

	private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
		//获取前驱节点的waitStatus
        int ws = pred.waitStatus;
    //如果ws等于signal,这个在前面提到过,如果为signal,那么就标记为出队列时会唤醒下一个线程
        if (ws == Node.SIGNAL)
			//那就不用修改,直接返回true
            return true;
        if (ws > 0) {
			//如果状态值大于0,那么表示该节点为取消状态
            do {
            	//将pred的前驱节点设置为node的前驱节点,相当于pred出队列
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            //将pred的后继节点设置为node
            pred.next = node;
        } else {
			//如果为其他状态,就用CAS操作转换为SIGNAL状态
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

如果上面的代码返回true,也就是前驱节点被标记为SIGNAL,这样当前驱节点释放锁的时候,会去通知当前节点node。这样node节点就可以进行挂起。 parkAndCheckInterrupt()方法的主要功能是:将线程挂起,等待其他线程唤醒。

	private final boolean parkAndCheckInterrupt() {
		//将线程挂起
        LockSupport.park(this);
        //返回中断状态
        return Thread.interrupted();
    }

这里对LockSupport的park的方法解释一下:

LockSupport
LockSupport类是Java6引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数:

public native void unpark(Thread jthread);  
public native void park(boolean isAbsolute, long time);  

unpark函数为线程提供“许可(permit)”,线程调用park函数则等待“许可”。这个有点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。
permit相当于0/1的开关,默认是0,调用一次unpark就加1变成了1.调用一次park会消费permit,又会变成0。 如果再调用一次park会阻塞,因为permit已经是0了。直到permit变成1.这时调用unpark会把permit设置为1.每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark不会累积

(三)、锁的获取方法总结

锁的获取,其实就是将获取锁的线程放在同步队列的首部,然后其他等待的线程就通过LockSupport的park()方法进行阻塞。
acquire方法:

  1. 通过tryAcquire尝试获取独占锁,如果成功返回true,失败返回false
  2. 如果tryAcquire失败,先通过addWaiter方法将当前线程的信息封装成Node节点添加到AQS队列尾部
  3. acquireQueued,将Node作为参数,通过自旋去尝试获取锁。

addWaiter方法:

  1. 将当前线程的信息封装成Node
  2. 判断当前链表中的tail节点是否为空,如果不为空,则通过cas操作把当前线程的node添加到AQS队列。
  3. 如果为空或者cas失败,调用enq方法将节点添加到AQS队列

enq方法:

  1. 获得尾部节点tail,并赋值给t
  2. 如果尾部节点为null,表示空队列,就新建一个头节点,并设置为尾节点
  3. 如果不是空队列,将node的前驱节点设置为尾节点,尝试用CAS添加为尾节点的后继节点

acquireQueued方法:

  1. 获取当前节点的前驱节点p
  2. 如果前驱节点p是头节点,然后就尝试去获得锁
  3. 如果获得锁成功,就将当前节点node设置为头节点
  4. 如果获得锁失败,调用shouldParkAfterFailedAcquire看是否能够挂起线程
  5. 如果能够挂起线程,通过parkAndCheckInterrupt对线程进行阻塞

shouldParkAfterFailedAcquire方法:

  1. 获取当前节点的prev节点
  2. 如果prev节点的状态是SINGINAL,那么就无需操作,直接返回true
  3. 如果状态大于0,那就是CANCEL状态,就将prev节点出队列,并重新整理链表
  4. 最后,使用CAS操作尝试将前驱节点的状态设置为SIGNAL

parkAndCheckInterrupt方法:

  1. 调用LockSupport的park()方法进行阻塞操作。

(四)、可中断式获取锁

可中断式获取锁和普通获取锁的区别就是在尝试获取锁的过程中,可以响应中断,从而停止尝试获取锁。
下面来看源代码,方法是acquireInterruptibly()的功能是:可以响应中断地尝试获取锁。

	public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //尝试获取锁
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

上面的源码中调用了doAcquireInterruptibly方法,它的主要功能是:响应中断地尝试获取锁。

private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        //将节点添加到同步队列中
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
        	//死循环,进行自旋
            for (;;) {
                final Node p = node.predecessor();
                //获取锁成功
                if (p == head && tryAcquire(arg)) {
                    //设置为头节点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                //判断线程是否能够挂起
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //线程中断,抛出异常
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

这个方法和之前的doAcquire实现几乎是一样的,**唯一的区别就是parkAndCheckInterrupt返回true时即线程阻塞时该线程被中断,代码抛出被中断异常。**这里就不做过多的解释了。

(五)、超时等待获取锁

这个方法主要的作用是限制线程等待锁的时间,如果超过时间就直接返回,不再进行等待。下面来看一下源码:

	public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //尝试获得锁
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

方法中调用了doAcquireNanos()方法,这个方法的主要功能是:限定等待锁释放的时间,如果超时就直接返回。

private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        //判断是否合法
        if (nanosTimeout <= 0L)
            return false;
        //计算最晚结束时间
        final long deadline = System.nanoTime() + nanosTimeout;
        //将线程添加进队列
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
        	//死循环,自旋操作
            for (;;) {
            	//获得当前节点的前驱节点
                final Node p = node.predecessor();
                //判断p是不是头节点,并且尝试获得锁 
                if (p == head && tryAcquire(arg)) {
                	//获得锁成功,设置为头节点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                //计算剩余等待时间
                nanosTimeout = deadline - System.nanoTime();
                //如果超时了,直接返回false,获取失败
                if (nanosTimeout <= 0L)
                    return false;
                //shouldParkAfterFailedAcquire判断是否能够阻塞
                //spinForTimeoutThreadhold是最小自旋时间 
                //调用LockSupport.parkNanos进行限时阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

主要逻辑如下图:
在这里插入图片描述

(六)、锁的释放

锁的释放涉及的方法是release(),下面我们来详细分析一下源码

	public final boolean release(int arg) {
		//尝试释放锁
        if (tryRelease(arg)) {
        	//如果成功,获取头节点并赋值给h
            Node h = head;
            //如果头节点不为null
            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;
        //如果状态小于0,使用CAS修改状态为0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

		//获得下一个节点,标记为s
        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;
        }
        //如果s节点存在
        if (s != null)
        	//唤醒线程
            LockSupport.unpark(s.thread);
    }

(七)、总结

通过以上分析AQS的独占锁,我们可以清楚地了解到他是如果通过acquire方法来保证只有一个线程能够执行同步代码块的,那就是LockSupport的park()方法,我们也更加清晰地了解了底层数据结构。之后我还会继续写关于共享锁的分析,其实原理也差不多。谢谢大家的阅读!

参考资料:AQS实现原理
     深入理解AQS

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值