Java并发编程之AQS以及ReentrantLock源码解析

1 ReentrantLock与synchronized的区别

我们知道锁的基本原理是,基于将多线程并行任务通过某一种机制实现线程的串行执行,从而达到线程安全性的目的。在synchronized 中,我们分析了偏向锁、轻量级锁、乐观锁。基于乐观锁以及自旋锁来优化了 synchronized 的加锁开销,同时在重量级锁阶段,通过线程的阻塞以及唤醒来达到线程竞争和同步的目的。
ReentrantLock与synchronized的区别如下:
1.原始构成
Synchronized是关键字属于JVM层面
Lock是具体类(java.util.concurrent.locks.Lock) 是api层面的锁,依赖AQS
2.使用方法
synchronized不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放锁的占用
ReetrantLock则需要用户去手动释放锁若没有主动释放锁,就有可能导致死锁现象需要Lock() 和unLock()方法配合try/finally语句块完成。
3.等待是否可中断
synchronized不可中断,除非抛出异常或者正常运行完成
ReetrantLock可中断:
- 设置超时方法tryLock(Long timeout, TimeUnit unit)
- lockInterruptibly() 放代码块中,调用interrupt方法中断
4.加锁是否公平
synchronized非公平锁
ReetrantLock两者都可以,默认是非公平锁,构造方法传入Boolean值,true为公平锁,false为非公平锁
5.锁绑定多个条件Condition
synchronized没有
ReetrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像syncronized要么随机一个线程要么唤醒全部。
下面通过伪代码,进行更加直观的比较:

// **************************Synchronized的使用方式**************************
// 1.用于代码块
synchronized (this) {}
// 2.用于对象
synchronized (object) {}
// 3.用于方法
public synchronized void test () {}
// 4.可重入
for (int i = 0; i < 100; i++) {
    synchronized (this) {}
}
// **************************ReentrantLock的使用方式**************************
public void test () throw Exception {
    // 1.初始化选择公平锁、非公平锁
    ReentrantLock lock = new ReentrantLock(true);
    // 2.可用于代码块
    lock.lock();
    try {
        try {
            // 3.支持多种加锁方式,比较灵活; 具有可重入特性
            if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }
        } finally {
            // 4.手动释放锁
            lock.unlock()
        }
    } finally {
        lock.unlock();
    }
}

2 AQS原理

2.1 AQS是什么

              AQS全称 是AbstractQueuedSynchronizer, 是 JUC 提供的一个用于构建锁和同步容器的基础类,。同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的 方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些 模板方法将会调用使用者重写的方法。JUC 包内许多类都是基于 AQS 构建,例如 ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock、FutureTask 等。
AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 AQS 队列中去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。

2.2 AQS的核心成员

2.2.1 状态标识位

AQS 中维持了一个单一的 volatile 修饰的状态信息 state,AQS 使用 int 类型的 state 标示锁的状态,可以理解为锁的同步状态。


state 标示锁的状态

state 因为用 volatile 因此保证了我们操作的可见性,所以任何线程通过 getState()获得状态都是可以得到最新值。AQS 提供了 getState( )、setState( )来获取和设置同步状态,具体如下:


获取同步的状态

设置同步的状态

通过 CAS 设置同步的状态

由于 setState()无法保证原子性,因此 AQS 给我们提供了 compareAndSetState 方法利用底层UnSafe 的 CAS 机制来实现原子性。compareAndSetState()函数实际上调用的是 unsafe 成员的compareAndSwapInt 方法。

2.2.2 队列节点类(Node 内部类)

AQS 是一个虚拟队列不存在队列实例,仅存在节点之间的前后关系。节点类型通过内部类Node 定义,其核心的成员如下:

static final class Node {
 /**节点等待状态值 1:取消状态*/
 static final int CANCELLED = 1;
 /**节点等待状态值-1:标识后继线程处于等待状态*/
 static final int SIGNAL = -1;
 /**节点等待状态值-2:标识当前线程正在进行条件等待*/
 static final int CONDITION = -2;
 /**节点等待状态值-3:标识下一次共享锁的 acquireShared 操作需要无条件传播*/
 static final int PROPAGATE = -3;
 //节点状态:值为 SIGNAL、CANCELLED、CONDITION、PROPAGATE、0 
 //普通的同步节点的初始值为 0,条件等待节点的初始值为 CONDITION (-2)
 volatile int waitStatus;
 //节点所对应的线程,为抢锁线程或者条件等待线程
 volatile Thread thread;
 //前驱节点,当前节点会在前驱节点上自旋,循环检查前驱节点的 waitStatus 状态
 volatile Node prev;
 //后继节点
 volatile Node next;
 //如果当前 Node 不是普通节点而是条件等待节点,则节点处于某个条件的等待队列上
//此属性指向下一个条件等待节点,即其条件队列上的后继节点。
 Node nextWaiter;
 ...
}
2.2.2.1 waitStatus 属性

每个 Node 节点与等待线程关联,每个节点维护一个状态 waitStatus,waitStatus 的各种值以常量的形式进行定义。waitStatus 的各常量值,具体如下:
(1)static final int CANCELLED = 1
waitStatus 值为 1 时表示该线程节点已释放(超时、中断),已取消的节点不会再阻塞。表示线程因为中断或者等待超时,需要从等待队列中取消等待;由于该节点线程等待超时或者被中断,需要从同步队列中取消等待,则该线程被置 1。节点进入了取消状态,该类型节点不会参与竞争,且会一直保持取消状态。
(2)static final int SIGNAL = -1
waitStatus 为 SIGNAL(-1)时表示其后继的节点处于等待状态,当前节点对应的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行。
(3)static final int CONDITION =-2;
waitStatus 为-2 时,表示该线程在条件队列(Condition queues)中阻塞(Condition 有使用),表示结点在等待队列中(这里指的是等待在某个 lock 的 condition 上),当持有锁的线程调用了 Condition 的 signal()方法之后,结点会从该 condition 的等待队列转
移到该 lock 的同步队列上,去竞争 lock。(注意:这里的同步队列就是我们说的 AQS 维护的 FIFO
队列,等待队列则是每个 condition 关联的队列)节点处于等待队列中,节点线程等待在 Condition 上,当其他线程对 Condition 调用了 signal()方法后,该节点从等待队列中转移到同步队列中,加入到对同步状态的获取中。
(4)static final int PROPAGATE = -3;
waitStatus 为-3 时,表示该下一个线程获取共享锁后,自己的共享状态会被无条件的传播下去,因为共享锁可能出现同时有 N 个锁可以用,这时候直接让后面的 N 个节点都来工作。这种状态在 CountDownLatch 中有使用到。
(5)waiteStatus 为 0
waitStatus 为 0 时,表示当前节点处于初始状态。
Node 节点的 waitStatus 状态,为以上 5 种状态的一种。

2.2.2.2 thread 成员

Node 的 thread 成员用来存放进入 AQS 队列中的线程引用;Node 的 nextWaiter 成员用来指向自己的后继等待节点,此成员只有线程处于条件等待队列中的时候使用。

2.2.2.3 抢占类型常量标识

Node 结点还定义了两个抢占类型常量标识:SHARED、EXCLUSIVE,具体如下:

static final class Node {
 //标识节点在抢占共享锁
 static final Node SHARED = new Node(); 
 //标识节点在抢占独占锁
 static final Node EXCLUSIVE = null;
 ...
}

SHARED 表示线程是因为获取共享资源时阻塞,而被添加到队列中的;EXCLUSIVE 表示线程因为获取独占资源时阻塞,而被添加到队列中的。

2.2.3 FIFO 双向同步队列

AQS 的内部队列是 CLH 队列变种,每当线程通过 AQS 获取锁失败,线程将被封装成一个Node 节点,通过 CAS 原子操作插入队列尾。当有线程释放锁时,AQS 会尝试让队头的后继节点占用锁。
AQS 是一个通过内置的 FIFO 双向队列来完成线程的排队工作,内部通过结点 head 和 tail 记录队首和队尾元素,元素的结点类型为 Node 类型,具体如下:

/*首节点的引用*/
private transient volatile Node head;
/*尾节点的引用*/
private transient volatile Node tail;

AQS 的首结点和尾节点,都是懒加载的。懒加载的意思是,在需要的时候才真正创建。只有在线程竞争失败的情况下,有新线程加入同步队列时,AQS 才创建一个 Head 结点。Head 结点只能被 setHead 方法修改,并且结点的 waitStatus 不能为 CANCELLED。尾节点只在有新线程阻塞时,才被创建。


一个包含 4 个节点的 AQS 同步队列

2.2.4 JUC 显式锁与 AQS 的关系

java.util.concurrent.locks 包中显示锁如 ReentrantLock、ReentrantReadWriteLock,线程同步工具如 Semaphore,异步回调工具如 FutureTask 等,内部都使用了 AQS 作为等待队列。通过开发工具去进行 AQS 的子类导航,会发现大量的 AQS 子类以内部类的形式使用:


大量的 AQS 子类以内部类的形式使用

3. ReentrantLock 的源码分析

3.1 ReentrantLock抢占锁

调用 ReentrantLock 中的 lock()方法,源码的调用过程我使用了时序图来展现。


ReentrantLock抢占锁的时序图

3.1.1 ReentrantLock.lock()

这个是 ReentrantLock获取锁的入口:

public void lock() {
 sync.lock();
}

sync 实际上是一个抽象的静态内部类,它继承了 AQS 来实现重入锁的逻辑,我们前面说过 AQS 是一个同步队列,它能够实现线程的阻塞以及唤醒,但它并不具备业务功能,所以在不同的同步场景中,会继承 AQS 来实现对应场景的功能。
Sync 有两个具体的实现类,分别是:
NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁
FailSync: 表示所有线程严格按照 FIFO 来获取锁。

3.1.2 非公平锁的同步器子类

ReentrantLock 为非公平锁实现了一个内部的同步器——NonfairSync,其显示锁获取方法 lock的源码如下:

static final class NonfairSync extends Sync {
 //非公平锁抢占
 final void lock() {
 if (compareAndSetState(0, 1))
 setExclusiveOwnerThread(Thread.currentThread());
 else
 acquire(1);
 }
 //...省略其他
 }

首先用一个 CAS 操作,判断 state 是否是 0(表示当前锁未被占用),如果是 0 则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS 操作只能保证一个线程操作成功,剩下的只能乖乖的去排队。
ReentrantLock“非公平”性即体现在这里:如果占用锁的线程刚释放锁,state 置为 0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。举个例子:当前有三个线程 A、B、C 去竞争锁,假设线程 A、B 在排队,但是后来的 C 直接进行 CAS 操作成功了,拿到了锁开开心心的返回了,那么线程 A、B 只能乖乖看着。

3.1.3 非公平抢占的钩子方法:tryAcquire(arg)

如果非公平抢占没有成功,非公平锁的 lock 会执行模板方法 acquire,首先会调用到钩子方法 tryAcquire(arg)。非公平抢占的钩子方法实现如下:

    static final class NonfairSync extends Sync {
        //非公平锁抢占的钩子方法
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
        //...省略其他
    }
    abstract static class Sync extends AbstractQueuedSynchronizer {
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 先直接获得锁的状态
            int c = getState();
            if (c == 0) {
                // 如果出现了任务队列 首节点线程完工了,将 state 设置为 0,
                // 下一步就进行交接仪式了。这个时候,首先抢占,不管不顾
                // 发现 state 是空的,那就直接拿来加锁使用。根本不考虑后面继承者的存在。
                if (compareAndSetState(0, acquires)) {
                    // 1、利用 CAS 自旋方式,判断当前 state 确实为 0,然后设置成 acquire(1)
                    // 这是原子性的操作,可以保证线程安全
                    setExclusiveOwnerThread(current);
                    // 设置当前执行的线程,直接返回为 true
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                // 2、当前的线程和执行中的线程是同一个,也就意味着可重入操作
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                // 表示当前锁被 1 个线程重复获取了 nextc 次
                return true;
            }
            // 否则就是返回 false,表示没有尝试成功获取当前锁,进入排队过程
            return false;
        }
//...省略其他
    }

非公平同步器 ReentrantLock.NonfairSync 的核心思想就是当前进程尝试获取锁的时候,如果发现锁的状态位是 0 的话就直接尝试将锁拿过来,然后 setExclusiveOwnerThread,根本不管同步队列中的排队节点。

3.1.4 ReentrantLock 公平锁的抢占流程

ReentrantLock 为公平锁实现了一个内部的同步器——FairSync ,其显示锁获取方法 lock 的源码如下:

    static final class FairSync extends Sync {
        //公平锁抢占的钩子方法
        final void lock() {
            acquire(1);
        }
        //...省略其他
    }

公平同步器 ReentrantLock.FairSync 的核心思想:通过 AQS 模板方法去进行队列入队操作。

3.1.5 公平抢占的钩子方法:tryAcquire(arg)

公平锁的 lock 会执行模板方法 acquire,该方法首先会调用到钩子方法 tryAcquire(arg)。公平抢占的钩子方法实现如下:

    static final class FairSync extends Sync {
        //公平抢占的钩子方法
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState(); //锁状态
            if (c == 0) {
                if (!hasQueuedPredecessors() && //有后继,就返回
                        compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

公平抢占的钩子方法中,首先判断是否有后继,如果有后继,并且当前线程不是锁的占有线程,那么钩子方法就返回 false,模板方法会进入排队的执行流程,可见,公平锁是真正公平的。

3.1.6 是否有后继节点的判断

FairSync 进行是否有后继节点的判断的代码,具体如下:

    public final boolean hasQueuedPredecessors() {
        Node t = tail;
        Node h = head;
        Node s;
        return h != t &&
                ((s = h.next) == null || s.thread != Thread.currentThread());
    }

hasQueuedPredecessors 的执行场景,大致如下:
(1)当 h!=t 不成立的时候,说明 h 头结点、t 尾结点要么是同一个节点,要么都是 null,此时 hasQueuedPredecessors 返回 false 表示没有后继。
(2)当 h!=t 成立的时候,进一步检查 head.next 是否为 null,如果为 null,返回 true。什么情况下 h!=t 同时 h.next==null 呢,有其他线程第一次正在入队时,可能会出现。其他线程执行 AQS的 enq 方法,compareAndSetHead(node)完成,还没执行 tail=head 语句时,此时 t=null,head=new Node(),head.next=null。
(3)如果 h!=t 成立,head.next != null,则判断 head.next 是否是当前线程,如果是返回 false,否则返回 true。
head 节点是获取到锁的节点,但是任意时刻 head 节点可能占用着锁,也可能释放了锁,如果释放了锁则此时 state=0 了,未被阻塞的 head.next 节点对应的线程在任意时刻都是在自旋的在尝试获取锁。

3.2 ReentrantLock释放锁

3.2.1 ReentrantLock.unlock

在 unlock 中,会调用 release 方法来释放锁

    public final boolean release(int arg) {
        if (tryRelease(arg)) { //释放锁成功
            Node h = head; //得到 aqs 中 head 节点
            if (h != null && h.waitStatus != 0)
            //如果 head 节点不为空并且状态!=0.调用 unparkSuccessor(h)唤醒后续节点
            unparkSuccessor(h);
            return true;
        }
        return false; 
    }

3.2.2 ReentrantLock.tryRelease

这个方法可以认为是一个设置锁状态的操作,通过将 state 状态减掉传入的参数值(参数是 1),如果结果状态为 0,就将排它锁的 Owner 设置为 null,以使得其它的线程有机会进行执行。在排它锁中,加锁的时候状态会增加 1(当然可以自己修改这个值),在解锁的时候减掉 1,同一个锁,在可以重入后,可能会被叠加为 2、3、4 这些值,只有 unlock()的次数与 lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会返回 true。

    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() !=
                getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

为什么在释放锁的时候是从 tail 进行扫描?
在else的代码块中来看一个新的节点是如何加入到链表中的:

  1. 将新的节点的 prev 指向 tail
  2. 通过 cas 将 tail 设置为新的节点,因为 cas 是原子操作所以能够保证线程安全性
  3. t.next=node;设置原 tail 的 next 节点指向新的节点
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        } 
    }
image.png

在 cas 操作之后,t.next=node 操作之前。存在其他线程调用 unlock 方法从 head开始往后遍历,由于 t.next=node 还没执行意味着链表的关系还没有建立完整。就会导致遍历到 t 节点的时候被中断。所以从后往前遍历,一定不会存在这个问题。

参考来源:
《Java并发编程的艺术》
《深入理解Java虚拟机》
  不可不说的Java“锁”事
  从ReentrantLock的实现看AQS的原理及应用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容