欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

AQS源码分析-以ReentrantLock为例(青铜)

程序员文章站 2024-03-18 10:13:10
...

AQS源码分析-以ReentrantLock为例(青铜)

前言

摘要

java实现线程同步最主要的2种方式是synchronized同步和工具锁,这两种同步机制在原理上有着较大的区别,在之前的一篇博客中,已经对synchronized原理做了简要描述,本文的主要目的是分析工具锁的原理。

java提供的工具锁主要有ReentrantLock、CountDownLatch、CyclicBarrier、Semaphore等,这些工具锁的实现都依赖于java提供的一个同步器框架AbstractQueuedSynchronizer,简称AQS。最近刚好学习了这部分的内容,感觉又很多东西值得总结,所以决定先从最核心的lock方法入手,争取对AQS有一个全面的认识。

本文要解决的问题
  1. 什么是AQS?AQS有什么作用?如何基于AQS实现自定义工具锁?
  2. ReentrantLock的非公平锁lock()方法源码过程是怎样的?核心步骤是怎样的?
  3. AQS的核心是什么?
  4. AQS中使用了什么设计模式?

正文

什么是AQS?

通过阅读JAVA API中,对AQS类的定义,主要的内容如下:

  1. Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues.
    1. 提供了一个基于队列的阻塞锁和响应的同步器
  2. This class is designed to be a useful basis for most kinds of synchronizers that rely on a single atomic int value to represent state
    1. AQS可以非常好地支持大部分能够用一个原子整数表达状态的同步器。
  3. Subclasses must define the protected methods that change this state, and which define what that state means in terms of this object being acquired or released。
    1. 实现AQS的子类,必须定义更改stat的方法,并且明确在获取锁或者释放锁时,stat对应的意义。
  4. AQS支持排他锁和共享锁机制

以上是java API中官方对AQS的定义,那么个人对AQS的理解和简单定义如下:

定义:AQS是一个支持自定义同步规则的框架,基于AQS,用户可以定义各种各样的同步锁。

AQS有什么作用?

从定义中,也可以直接推导出,AQS的作用如下:

  1. AQS最核心的用途就是作为一个同步器的基础。基于AQS可以实现自定义的同步器,比如ReentrantLock。
如何基于AQS实现自定义工具锁?

这个问题在学习AQS之前感觉不可思议,但是其实并不难,只要阅读AQS的JAVA api文档,就能快速实现一个简单的工具锁。

官方文档中说明,要实现一个自定义锁,关键步骤如下:

  1. 要基于AQS实现一把锁,首先需要定义一个内部类Sync,继承AQS

  2. Sync需要实现以下方法,这也是使用AQS唯一的方式

    这些方法的默认实现,都是抛出一个异常UnsupportedOperationException。

官方提供的样例如下:

基于AQS实现一个不可重入锁,参考链接:https://docs.oracle.com/javase/8/docs/api/。

最核心的工作有2个,分别是:

  1. 设计stat代表的含义
    1. 这里0代表无锁状态,1代表被占用状态
  2. 然后封装一个内部类Sync,实现tryAcquire,tryRelease等方法
    1. 这是自定义锁的核心逻辑
AQS框架主流程分析

在深入学习源码之前,我们先梳理一下AQS框架的主流程,宏观地了解整个运行机制。

  1. 对一个自定义锁而言,我们首先会调用lock方法

  2. 然后lock方法会调用AQS的acquire方法,调用逻辑如下

    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
    
  3. 在上面的逻辑中,tryAcquire是用户自定义的锁机制,也就是说,AQS框架调用子类Sync的tryAcquire方法,返回true或者false,true代表获得锁,false代表没获得。

    1. 如果获得锁,tryAcquire会修改stat,在逻辑上实现线程独占锁
  4. 如果线程没有获得锁,之后的逻辑就不需要用户关心了,直接进入AQS预先编织好的模板流程,线程进入等待队列,到队列中获得锁。

总的来说,我们可以发现,AQS把锁机制开放给用户,把复杂的同步逻辑留给了自己。如果是学过大数据的同学应该可以感觉到,这一点和MapReduce框架非常类似,用户只需要实现计算逻辑,大数据的调度等内容就全部交给框架了。其实这种设计背后是一个设计模式,我们通常称之为模板方法模式,或者回调函数模式。

lock方法源码分析

了解了AQS的核心流程之后,我们开始逐步分析源码。在本文中,我将尝试用泳道图来描述源码,这种方式感觉是目前为止,分析源码最好的方式,结构清晰,逻辑严谨,用起来特别爽。好,不废话了,源码过程如下:

AQS源码分析-以ReentrantLock为例(青铜)

提醒:如果纯粹是看上面的图,肯定会一脸懵逼,其实上面的图只是我个人用的一个记录,只要从lock方法一直跟进去,就能非常清晰的把这个图画出来。

对于上面的过程,再进行提炼,可以总结核心要点如下:

  1. 除了tryAcquire方法以外,其他的逻辑全部是AQS内置的
  2. 首先判断stat是否为0,如果是,则直接cas抢占锁,如果不是,则尝试判断这个锁是否是当前线程已经占用,如果是,则stat+x,也能成功获得锁。
  3. 如果以上2种情况获取锁失败,则会尝试进入队列,排队抢占锁。在入队的过程中,又有2个核心要点
    1. 首先,需要通过CAS的方式,将当前线程节点添加到双向链表的尾部
    2. 第二,当前线程在进入park状态前,通过CAS设置waitStatus的状态,确保前置节点在release时,会唤醒当前节点,否则会出现死锁的情况。
    3. 一旦以上2点得以保证,当前线程就可以安心地进入park,等待唤醒,从而获取锁了。如果当前线程是第二个节点,则将头节点的waitStatus置为SIGNAL。
      1. 此时头节点中并没有存放线程,因为第一个线程不进入队列,从而可以推测,当线程release时,无论是否在队列中,都会自动唤醒头结点中的next线程。
AQS的核心原理?

有了总体流程的把握,也有了过程代码的研究,下面提炼一下AQS的核心原理。

  1. AQS核心1:通过stat的来表达锁的占有与空闲,实现获取锁的失败与成功,这部分是实现者自定义的。
  2. AQS核心2:当线程通过自定义逻辑获取锁失败,则基于等待队列去获取锁,这部分是框架预制的功能。

AQS的核心原理就是一个atomic stat + 双向链表队列。用一个图来表示,则如下图

AQS源码分析-以ReentrantLock为例(青铜)

结论

  1. 通过全文的论述,在一定程度上回答了前言中的全部问题,当然,回答的深度、粒度、广度还都不够,只是入门性质的,后续还需优化和补充。
相关标签: Java