前言:

众所周知,JDK中提供并发工具类:java.util.concurrent。里面提供了很多并发编程中常见的工具类,比如automic原子操作,比如lock同步锁等,这里涉及到的并发工具类都跟AQS有关系。

从Lock作为切入点

这里以Lock作为切入点来讲解AQS,毕竟同步锁是解决线程安全问题的通用手段,也是我们工作中用得比较多的方式。

Lock API

Lock是一个接口,方法定义如下:






void lock() // 如果锁可用就获得锁,如果锁不可用就阻塞直到锁释放void lockInterruptibly() // 和 lock()方法相似, 但阻塞的线程可中断,抛出 java.lang.InterruptedException异常boolean tryLock() // 非阻塞获取锁;尝试获取锁,如果成功返回trueboolean tryLock(long timeout, TimeUnit timeUnit) //带有超时时间的获取锁方法void unlock() // 释放锁

Lock的实现

实现Lock接口的类有很多,以下为几个常见的锁实现


1.ReentrantLock:表示重入锁,它是唯一一个实现了Lock接口的类。重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数

2.ReentrantReadWriteLock:重入读写锁,它实现了ReadWriteLock接口,在这个类中维护了两个锁,一个是ReadLock,一个是WriteLock,他们都分别实现了Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则是:读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的操作都会存在互斥。

3.StampedLock:stampedLock是JDK8引入的新的锁机制,可以简单认为是读写锁的一个改进版本,读写锁虽然通过分离读和写的功能使得读和读之间可以完全并发,但是读和写是有冲突的,如果大量的读线程存在,可能会引起写线程的饥饿。stampedLock是一种乐观的读策略,使得乐观锁完全不会阻塞写线程。

ReentrantLock的简单实用

我们通过一个简单的demo来演示一下
















public class Demo {  private static int count = 0;  static Lock lock = new ReentrantLock();  public static void  inc() {    lock.lock();    try {      Thread.sleep(1000);      count++;    } catch (InterruptedException e) {      e.printStackTrace();    }finally {      lock.unlock();    }  }}

这段代码主要做一件事,就是通过一个静态的incr()方法对共享变量count做连续递增,在没有加同步锁的情况下多线程访问这个方法一定会存在线程安全问题。所以用到了ReentrantLock来实现同步锁,并且在finally语句块中释放锁。

那么我来引出一个问题,大家思考一下


多个线程通过lock竞争锁时,当竞争失败的锁是如何实现等待以及被唤醒的呢?

什么是AQS

AQS全称为AbstractQueuedSynchronizer,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock、CountDownLatch等。

AQS是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件。

可以这么说,只要搞懂了AQS,那么J.U.C中绝大部分的api都能轻松掌握。

AQS的两种功能

从使用层面来说,AQS的功能分为两种:独占和共享

  • 独占锁,每次只能有一个线程持有锁,比如前面给大家演示的ReentrantLock就是以独占方式实现的互斥锁

  • 共享锁,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock


给大家画一个图,看一下ReentrantLock和AQS之间的关系。

JAVA并发基础:深入分析AQS实现原理_java


如图可知,ReentrantLock内部其实包含了一个AQS对象,这个AQS对象就是ReentrantLock可以实现加锁和释放锁的关键性的核心组件。ReentrantLock加锁和释放锁的底层原理

如上面代码示例中,当一个线程使用ReentrantLock的lock()方法进行加锁,会发生什么事情?

主要逻辑为:

再AQS对象内部有一个核心的变量叫做state,是int类型的,代表了加锁的状态。

初始状态下,这个state的值是0。

另外,这个AQS内部还有一个关键变量,用来记录当前加锁的是哪个线程,初始化状态下,这个变量是null。

JAVA并发基础:深入分析AQS实现原理_java_02

接着线程跑过来调用ReentrantLock的lock()方法尝试进行加锁,这个加锁的过程,直接就是用CAS操作将state值从0变为1。


(对于CAS,可能有些人不太熟悉,这里不做介绍,明白它是底层控制独占性的一套机制即可。后续会推出相关文章)

如果之前没人加过锁,那么state的值肯定是0,此时线程1就可以加锁成功。


一旦线程1加锁成功了之后,就可以设置当前加锁线程是自己。所以大家看下面的图,就是线程1跑过来加锁的一个过程。

其实看到这儿,大家应该对所谓的AQS有感觉了。说白了,就是并发包里的一个核心组件,里面有state变量、加锁线程变量等核心的东西,维护了加锁状态。

你会发现,ReentrantLock这种东西只是一个外层的API,内核中的锁机制实现都是依赖AQS组件的

这个ReentrantLock之所以用Reentrant打头,意思就是他是一个可重入锁。

可重入锁的意思,就是你可以对一个ReentrantLock对象多次执行lock()加锁和unlock()释放锁,也就是可以对一个锁加多次,叫做可重入加锁。

大家看明白了那个state变量之后,就知道了如何进行可重入加锁!

其实每次线程1可重入加锁一次,会判断一下当前加锁线程就是自己,那么他自己就可以可重入多次加锁,每次加锁就是把state的值给累加1,别的没啥变化。

接着,如果线程1加锁了之后,线程2跑过来加锁会怎么样呢?

我们来看看锁的互斥是如何实现的?

线程2跑过来一下看到,哎呀!state的值不是0啊?所以CAS操作将state从0变为1的过程会失败,因为state的值当前为1,说明已经有人加锁了!

接着线程2会看一下,是不是自己之前加的锁啊?当然不是了,“加锁线程”这个变量明确记录了是线程1占用了这个锁,所以线程2此时就是加锁失败。

给大家来一张图,一起来感受一下这个过程:

JAVA并发基础:深入分析AQS实现原理_java_03

接着,线程2会将自己放入AQS中的一个等待队列,因为自己尝试加锁失败了,此时就要将自己放入队列中来等待,等待线程1释放锁之后,自己就可以重新尝试加锁了

所以大家可以看到,AQS是如此的核心!AQS内部还有一个等待队列,专门放那些加锁失败的线程!

同样,给大家来一张图,一起感受一下:

JAVA并发基础:深入分析AQS实现原理_java_04

接着,线程1在执行完自己的业务逻辑代码之后,就会释放锁!他释放锁的过程非常的简单,就是将AQS内的state变量的值递减1,如果state值为0,则彻底释放锁,会将“加锁线程”变量也设置为null!

接下来,会从等待队列的队头唤醒线程2重新尝试加锁。

好!线程2现在就重新尝试加锁,这时还是用CAS操作将state从0变为1,此时就会成功,成功之后代表加锁成功,就会将state设置为1。

此外,还要把“加锁线程”设置为线程2自己,同时线程2自己就从等待队列中出队了。

最后再来一张图,大家来看看这个过程。

JAVA并发基础:深入分析AQS实现原理_java_05


总结

AQS就是一个并发包的基础组件,用来实现各种锁,各种同步组件的。

它包含了state变量、加锁线程、等待队列等并发中的核心组件。

以上为全部内容。