美文网首页JavaJava
ReentrantLock(1) —— AQS简介

ReentrantLock(1) —— AQS简介

作者: 若琳丶 | 来源:发表于2019-11-04 23:55 被阅读0次

    前言

    在Java并发相关知识中,对锁的了解一直非常粗浅。面试也只知道 synchronized 关键字和 ReentrantLock 之间的区别,各自有什么优点,使用场景分别是什么等,但是对这两种并发解决方案的底层实现和原理一知半解。近日结合相关源码和解析博客,慢慢增加对两者的深层理解。有幸看到这篇文章,看之需细细咀嚼,讲解颇为细致,故以此为基础,对比源码,再附上对原文的理解,进行学习。

    原文地址: https://www.cnblogs.com/takumicx/p/9402021.html

    一、介绍

    ReentrantLock可以有公平锁和非公平锁的不同实现,只要在构造它的时候传入不同的布尔值,继续跟进下源码我们就能发现,关键在于实例化内部变量 sync 的方式不同,如下所示


    NonfairSync的类继承关系
    FairSync的类继承关系

    该抽象类为我们的加锁和解锁过程提供了统一的模板方法,只是一些细节的处理由该抽象类的实现类自己决定。所以在解读ReentrantLock(重入锁)的源码之前,有必要了解下AbstractQueuedSynchronizer。

    二、AbstractQueuedSynchronizer介绍

    2.1、AQS是构建同步组件的基础

    AbstractQueuedSynchronizer,简称AQS,为构建不同的同步组件(重入锁,读写锁,CountDownLatch等)提供了可扩展的基础框架,如下图所示。


    组件相关类

    AQS以模板方法模式在内部定义了获取和释放同步状态的模板方法,并留下钩子函数供子类继承时进行扩展,由子类决定在获取和释放同步状态时的细节,从而实现满足自身功能特性的需求。除此之外,AQS通过内部的同步队列管理获取同步状态失败的线程,向实现者屏蔽了线程阻塞和唤醒的细节。

    这样介绍未免有些官方。那AQS究竟是什么,我们提一个问题来侧面解释AQS的本质:我继承了AbstractQueuedSynchronizer,那我能干什么?
    我继承了AQS,当然也继承了AQS的一部分方法,我们只看最关键的两个方法:

       //申请获取锁
        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
        //进行释放锁
        public final boolean release(int arg) {
            if (tryRelease(arg)) {
                Node h = head;
                if (h != null && h.waitStatus != 0)
                    unparkSuccessor(h);
                return true;
            }
            return false;
        }
    

    从方法上看,什么角色才会有加锁和解锁的行为?锁的本身。也就是说,我只要继承了AQS,我就是一把锁。加锁和解锁的模板和关键步骤已经定义好,具体每个步骤如何定义,不同的锁的类别有不同的实现。你公平锁有公平的实现方式,我非公平锁有非公平的解决方案。但无论AQS下的任何类型的锁,关键步骤已经定义好了。

    在AQS 中使用了模板方法的设计模式

    2.2、AQS的内部结构(ReentrantLock的语境下)

    AQS的内部结构主要由同步等待队列构成

    2.2.1、同步等待队列

    AQS中同步等待队列的实现是一个带头尾指针(这里用指针表示引用是为了后面讲解源码时可以更直观形象,况且引用本身是一种受限的指针)且不带哨兵结点(后文中的头结点表示队列首元素结点,不是指哨兵结点)的双向链表。

    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;//指向队列首元素的头指针
    
    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;//指向队列尾元素的尾指针
    

    head是头指针,指向队列的首元素;tail是尾指针,指向队列的尾元素。而队列的元素结点Node定义在AQS内部,主要有如下几个成员变量

    volatile Node prev; //指向前一个结点的指针
    volatile Node next; //指向后一个结点的指针
    volatile Thread thread; //当前结点代表的线程
    volatile int waitStatus; //等待状态
    
    • prev:指向前一个结点的指针
    • next:指向后一个结点的指针
    • thread:当前结点表示的线程,因为同步队列中的结点内部封装了之前竞争锁失败的线程,故而结点内部必然有一个对应线程实例的引用
    • waitStatus:对于重入锁而言,主要有3个值。0:初始化状态;-1(SIGNAL):当前结点表示的线程在释放锁后需要唤醒后续节点的线程;1(CANCELLED):在同步队列中等待的线程等待超时或者被中断,取消继续等待。(其他状态暂且不管)

    同步队列的结构如下图所示


    AQS同步队列结构

    了接下来能够更好的理解加锁和解锁过程的源码,对该同步队列的特性进行简单的讲解:

    1. 同步队列是个先进先出(FIFO)队列,获取锁失败的线程将构造结点并加入队列的尾部,并阻塞自己。如何才能线程安全的实现入队是后面讲解的重点,毕竟我们在讲锁的实现,这部分代码肯定是不能用锁的。
    2. 队列首结点可以用来表示当前正获取锁的线程。
    3. 当前线程释放锁后将尝试唤醒后续处结点中处于阻塞状态的线程。

    为了加深理解,还可以在阅读源码的过程中思考下这个问题:
    这个同步队列是FIFO队列,也就是说先在队列中等待的线程将比后面的线程更早的得到锁,那ReentrantLock是如何基于这个FIFO队列实现非公平锁的?

    2.2.2 AQS中的其他数据结构(ReentrantLock的语境下)

    同步状态变量:

    /**
     * The synchronization state.
     */
    private volatile int state;
    

    这是一个带volatile前缀的int值,是一个类似计数器的东西。在不同的同步组件中有不同的含义。以ReentrantLock为例,state可以用来表示 该锁被线程重入的次数。当state为0表示该锁不被任何线程持有;当state为1表示线程恰好持有该锁1次(未重入);当state大于1则表示锁被线程重入state次。因为这是一个会被并发访问的量,为了防止出现可见性问题要用volatile进行修饰。

    • 原文是当state大于1时,则表示重入state次,但我感觉是state - 1次,因为 state为1时表示的是未重入。
    • state AQS中的属性,AQS中只能有一个线程节点获取到锁,所以 state 是服务于当前获得锁的线程,获取锁的线程是队列中的 head,所以 state是head的重入情况。

    持有同步状态的线程标志:

    /**
     * The current owner of exclusive mode synchronization.
     */
    private transient Thread exclusiveOwnerThread;
    

    如注释所言,这是在独占同步模式下标记持有同步状态线程的。ReentrantLock就是典型的独占同步模式,该变量用来标识锁被哪个线程持有

    了解AQS的主要结构后,就可以开始进行ReentrantLock的源码解读了。由于非公平锁在实际开发中用的比较多,故以讲解非公平锁的源码为主。以下面这段对非公平锁使用的代码为例:

    /**
     * @author: takumiCX
     * @create: 2018-08-01
     **/
    public class NoFairLockTest {
        public static void main(String[] args) {
            //创建非公平锁
            ReentrantLock lock = new ReentrantLock(false);
            try {
                //加锁
                lock.lock();
                //模拟业务处理用时
                TimeUnit.SECONDS.sleep(1);
    
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //释放锁
                lock.unlock();
            }
        }
    }
    

    三、总结

    回过头来,我们再问自己之前的这个问题:我继承了AbstractQueuedSynchronizer,那我能干什么?
    我能加锁和解锁,我,就是锁。像我们的主角 —— ReentrantLock,它的特殊性就是可重入。

    以上都是个人愚见,如有理解不对的地方还望指出,大家一起交流,一起进步。
    下一节:ReentrantLock(二) —— 非公平模式加锁流程

    相关文章

      网友评论

        本文标题:ReentrantLock(1) —— AQS简介

        本文链接:https://www.haomeiwen.com/subject/snujbctx.html