AQS之ReentrantLock源码解析

目录

目录

ReentrantLock源码解析

AQS的基本概念

AQS保证线程安全的大致原理(伪代码)

AQS具备特性,共享模式等概念

AQS内部的大致数据结构图

ReentrantLock基本概念

ReentrantLock中Node节点的几个重要属性

ReentrantLock代码案例

ReentrantLock源码解析

ReentrantLock源码流程图

总结



ReentrantLock源码解析

 

在分析ReentrantLock前先来大致了解一下AQS

 

AQS的基本概念

Java并发编程核心在于java.concurrent.util包,juc当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。

 

AQS保证线程安全的大致原理(伪代码)

   //加锁
   lock;
   //为什么要死循环?因为只有加锁成功的线程才能跳出循环,其他的线程若是跳出循环了就会有线程问题了啊,因此需要一个死循环在这
for(;;){
       //一次多个线程同时来进行加锁操作,如何能够保证只有一个线程能够加锁成功呢?
       //通过CAS算法来加锁,他是一个原子操作。详情可见Unsafe魔法类
       if(加锁成功){
           //加锁成功的线程说明获取到了资源,跳出循环,执行真正的业务逻辑代码
           break;
       }
       //其他未加锁成功的线程,不能一直占着CPU资源,所以要想个办法让出CPU资源
       //将线程进行阻塞,让出资源。唤醒时调用LockSupport.unpark(thread)即可,唤醒线程需要在持有锁的线程释放锁才可以去唤醒,不然唤醒也没有意义。因此放在unlock后面
       LockSupport.park(thread);
       //向容器中放入阻塞的线程,为以后唤醒线程使用。
   }

   /**
    *代码逻辑
    */

   //解锁
   unlock;
   //唤醒之前阻塞的线程
   //唤醒操作需要唤醒指定的线程,因此unlock方法需要传入需唤醒的线程。那么这个线程从哪搞呢?
   //说明得在线程阻塞的时候就得有一个容器去放这个线程,可以是HashSet,LinkedList,数组等等,但是最后AQS用了一个双向链表。
   LockSupport.unpark(thread);

这段伪代码大致就是AQS加锁解锁的基本原理了,后面会以ReentrantLock为例来详细解析。

由此可见,要实现一个AQS同步器,主要有以下核心的几点:

1.死循环(自旋)

2.CAS加锁算法,原子操作的一个算法   (CAS依赖的汇编指令:cmpxchg()

3.线程的阻塞与唤醒(LockSupport)

4.容器queue(用来存放被阻塞的线程)

 

AQS具备特性,共享模式等概念

1.阻塞队列  2.共享/独占  3.公平/非公平  4.可重入  5.允许中断

 

AQS内部一个重要属性:volatile int state,表示资源是否被加锁,加了几次锁

State三种访问方式:getState()、setState()、compareAndSetState()

 

AQS定义的两种共享的模式

  1. 独占(Exclusive):只有一个线程能够执行,如ReentrantLock
  2. 共享(Share):多个线程可以同时执行,如CountDownLatch/Semaphore

AQS定义的两种队列:

1.同步等待队列:也叫CLH队列,是一种基于双向链表数据结构的队列,是先入先出(FIFO)的线程等待队列

2.条件等待队列: Condition是一个多线程间协调通信的工具类,一个或多个线程等待某个条件(Condition)满足,才会被重新唤醒,从而开始竞争锁。如:cyclicBarrier.await();

 

AQS内部的大致数据结构图

 

ReentrantLock基本概念

ReentrantLock是一种基于AQS框架的应用实现,是以java程序来实现的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。而且它具有比synchronized更多的特性,比如它支持手动加锁与解锁,支持加锁的公平性。

ReentrantLock如何实现synchronized不具备的公平与非公平性呢?

在ReentrantLock内部定义了一个Sync的内部类,该类继承AbstractQueuedSynchronized,对该抽象类的部分方法做了实现;并且还定义了两个子类:

1、FairSync 公平锁的实现

2、NonfairSync 非公平锁的实现

这两个类都继承自Sync,也就是间接继承了AbstractQueuedSynchronized,所以这一个ReentrantLock同时具备公平与非公平特性。

这里提一下,AQS用了模板方法的设计模式

 

ReentrantLock中Node节点的几个重要属性

在解析ReentranrLock源码之前,先了解一下AQS中等待队列中的重要角色Node。这里可以先混个眼熟,后面解析源码中会反复提到这个Node节点。

waitStatus:Node节点的状态,目前有5种状态,分别是:

                   0(初始状态);

                   1(CANCELLED 异常状态);

                   -1(SIGNAL 可被唤醒状态);

                   -2(CONDITION 条件等待状态);

                   -3(PROPAGATE 传播状态)

prev:节点的前置节点

next:节点的后置节点

thread:节点所对应的线程

nextWaiter:当前节点在Condition中等待队列上的下一个节点(给Condition等待队列使用),此次源码解析未用到

 

ReentrantLock代码案例

一个简单的demo,初步认识

public class ReentrantLockDemo {

    private static volatile int count = 0;
    private static ReentrantLock lock = new ReentrantLock(true);

    /**
     * count虽然被volatile关键字修饰,若没加锁结果并不是50000,而是小于等于50000
     * 加锁后无论怎样执行,结果都是50000
     **/
    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                for (int j = 0; j < 5000; j++) {
//                    count++;
                    lock.lock();
                    try{
                        count++;    //先读,再加,不是一个原子操作
                    }finally {
                        lock.unlock();
                    }
                }
            });
            thread.start();
        }

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count==" + count);
    }
}

 

ReentrantLock源码解析

此次解析的源码基于JDK1.8,以公平锁来解析

//实例化一个公平锁
private static ReentrantLock lock = new ReentrantLock(true);
lock.lock();
//进入锁,调用acquire方法,去获取锁,参数1的意思是给这个锁的计数器上加1
final void lock() {
    acquire(1);
}
/**
 * 三个核心方法:tryAcquire(尝试获取资源);
 *            addWaiter(将未获取到资源将线程添加至等待队列);
 *            acquireQueued(将等待队列中的线程取出来获取资源)
 */
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
//该方法在具体实现类中
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    //获取当前资源的状态,也可以当作为计数器,因为是重入锁嘛,为0则说明资源未被其他线程所持有
    int c = getState();
    if (c == 0) {
        //判断等待队列是否为空,若是第一次获取锁,队列必然是空的
        //或者判断等待队列中的第一个线程是否为空,是否是当前线程
        if (!hasQueuedPredecessors() &&
                //通过CAS算法(原子操作)来将state状态修改为1,从而使当前线程获取资源,并给资源加锁
                compareAndSetState(0, acquires)) {
            //把当前线程设置为持有锁线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //当state大于0时,且当前线程也是持有锁的线程,可以继续获取锁,可重入锁的体现就在这
    else if (current == getExclusiveOwnerThread()) {
        //将state状态再加1,这也是我为什么上面说可以当作计数器的原因,因为加了几次锁,state的值就为几
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //没有获取到资源,返回false
    return false;
}
//将未获取到资源将线程添加至等待队列,数据结构是一个双向链表
private Node addWaiter(Node mode) {
    //new一个节点
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    //获取tail(尾节点)
    Node pred = tail;
    //尾节点不为空时,说明这个队列已经有其他节点在排队了,需要往后面加节点
    if (pred != null) {
        //将新节点的前驱指针指向队列里的最后一个节点
        node.prev = pred;
        //通过cas算法来将tail指针指向新节点,因为有并发,所以需要cas算法来保证只有一个节点成为尾节点
        if (compareAndSetTail(pred, node)) {
            //将之前的尾节点的next指针指向新节点,那么此时新节点就成为了新的尾节点
            pred.next = node;
            //返回当前新节点
            return node;
        }
    }
    //说明还没有队列,需要建立一个队列,将新节点入队列
    enq(node);
    //返回当前新节点
    return node;
}
//建立一个队列,将节点入队列
//注意:这个CLH队列的head节点的thread值是空的,head节点的下一个节点才是真正排队的线程节点,为什么要这样设计,在线程节点出队列的时候在细品
private Node enq(final Node node) {
    //一个自旋操作,确保多个线程同时进来也能够都将所有节点加入至队列
    for (;;) {
        Node t = tail;
        //当尾节点是空时,初始化一个CLH队列
        if (t == null) { // Must initialize
            //通过CAS算法将head头节点指向一个新new的节点(这个节点的thread为空)
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //第二次进入循环,将参数传进来的新节点的前驱指针指向尾节点
            node.prev = t;
            //通过CAS算法将tail指针指向新节点
            if (compareAndSetTail(t, node)) {
                //老的尾节点的next指针就指向新节点,此时的新节点也就成为了新的尾节点了
                t.next = node;
                return t;
            }
        }
    }
}
//以独占不间断模式(自旋操作)获取队列中已存在的线程。由条件等待方法并且获取使用。
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        //一个死循环。自旋操作。确保并发的情况下,只能有一个线程能够获取锁资源。其他线程会在此进行阻塞,等待锁的释放并获取锁资源
        for (;;) {
            //获取此节点的前驱节点
            final Node p = node.predecessor();
            //如果当前节点的前驱节点是head头节点,那么就去尝试获取资源,调用tryAcquire
            //注意:此时获取资源的是当前节点中的thread
            if (p == head && tryAcquire(arg)) {
                //当前节点获取到资源后,那么就将当前节点设置尾head头节点,将thread置空,前驱指针置空
                //此时这个节点就成了head节点了,这里就对应上了之前head的thread为什么要为空了,这样的话每次都是将老的头结点扔掉,并不会对真正需要操作资源的thread做什么其他操作或处理,shouldParkAfterFailedAcquire方法里会详解
                setHead(node);
                //将老的头节点的next指针置空,那么这个老节点就被移除看这个CLH队列,会被下次的gc给清理掉
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //检查并更新节点的状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                    //将线程阻塞,释放CPU资源
                    parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            //当线程有中断处理时,将中断线程的Node的waitStatus标记成1(无效),然后给清理掉,队列中只保留正常状态的Node节点
            cancelAcquire(node);
    }
}
//注意:当前Node节点(队列中第二个节点)是否能够唤醒的标识量waitStatus是放在head节点上的,而不是在当前节点本身,当前节点的waitStatus一直都是0
//为什么这样呢?
//因为当Node中的thread唤醒时,只需将head指针指向队列中的第二个节点即可。自旋和线程获取到资源的原因会将第二个节点的thread置空,并且waitStatus会通过CAS算法将其变成-1,即说明第三个节点被认为是下一个可唤醒的节点,同时会将之前的第一个节点(原来的head节点)给剔除。结合FIFO队列原理和图来理解更好理解
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //第一次循环进来时,waitStatus的默认值是0
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        //第二次循环进入,由于第一次循环将waitStatus设为了SIGNAL状态
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        //若当前节点(第二个节点)的上一个节点(head节点)的waitStatus大于0,则认为该节点已经无效了,剔除队列
        //并且遍历队列,将其余无效的节点也剔除掉
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        //第一次循环的时候,通过CAS算法将当前节点(队列中第二个节点)的前驱节点(head节点)的waitStatus状态设为SIGNAL(-1,可唤醒)
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
//能进入到这里,说明资源已经被其他线程所占有,当前线程得阻塞等待
private final boolean parkAndCheckInterrupt() {
    //让当前线程park,释放CPU资源
    LockSupport.park(this);
    //重置线程中断状态
    return Thread.interrupted();
}

自此,加锁的代码基本解析完了。
最后的LockSupport.park(this);让线程进行阻塞,但是不能一直阻塞在那啊,因此当资源被释放时,需要一个操作来将线程进行唤醒。
这个就是解锁并且唤醒新线程的操作。下面来分析解锁

lock.unlock();
//解锁,参数1的意思是给这个锁的计数器减1
public void unlock() {
    //释放锁
    sync.release(1);
}
/**
 * 两个核心方法:tryRelease(尝试释放锁)
 *             unparkSuccessor(唤醒被阻塞的线程)
 */
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        //head节点不为空,说明有其他线程在等待中,且他的waitStatus状态是可被唤醒
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
//尝试释放锁
protected final boolean tryRelease(int releases) {
    //将state状态减1,但不一定减完后就是0,因为可重入锁会加几次锁
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        //此时state状态为0,说明当前线程已经执行完所有的逻辑,释放了资源,已经可以由其他线程再来获取资源
        free = true;
        //将持有锁资源线程重新置空,让新的线程来填补
        setExclusiveOwnerThread(null);
    }
    //写入状态值
    setState(c);
    return free;
}
//唤醒被阻塞的线程
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    //此时这个node是head节点
    int ws = node.waitStatus;
    if (ws < 0)
        //这个head节点的waitStatus若是-1(SIGNAL),说明他的下一个节点(真实存放thread的节点)是要被唤醒的节点
        //通过CAS算法将head节点的waitStatus从-1改为0
        //这里是为了给非公平锁用的,若是非公平锁的话,这个时候第二个节点的线程和新的线程一起去抢锁,此时第二个节点中的线程并不是能够直接获取到锁的,因此先将状态改为0,然后再来唤醒第二个节点的thread,如果第二个节点的线程没能够抢到锁,那么将会继续阻塞。
        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.
     */
    //获取到需要唤醒thread的节点s
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        //若队列的第二个节点是空或者waitStatus是失效状态,那就从队列尾部往头部遍历,一直找到到最靠前的那个节点为可唤醒的节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        //唤醒队列最靠前的那个节点中的thread
        LockSupport.unpark(s.thread);
}

 

到了这里。ReentrantLock的加锁与解锁的主要代码都已经解析完毕了。

通过分析我们可以看到,代码中多次用到了CAS算法,CAS算法是一个原子操作,无论在多高的并发情况下,有且仅有一个线程能够完成一次CAS的操作。AQS同步器中很多地方都会用到CAS算法。

ReentrantLock中使用的队列是CLH队列,数据结构大致如下图:

也多次使用了死循环(自旋),其作用是能够保证每个线程都能够执行到,这也是AQS的一处精髓所在吧。不止是ReentrantLock,其他的锁也都会用上这个死循环。

等待的线程通过LockSupport的park和unpark方法来进行阻塞与唤醒。

 

ReentrantLock源码流程图

这个是我画的一个公平锁的一个源码的流程图。可以参考一下。

 

总结

本文详细的介绍了ReentrantLock,解析了主要的源码。在AQS的其他同步锁中,大致实现的思想与ReentrantLock的大同小异,可能细节方面有很多需要处理的。ReentrantLock也是大家比较常用的一种锁,解析起来也比较有意义。

对于其他的锁,也可以按照上面的AQS保证线程安全的大致原理的思路去分析其源码。

 

最后,很感谢你能够耐着性子一直看到最后,希望本篇文章也能够给你带来一定程度上的帮助,谢谢。

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值