JUC(一)-AQS源码分析

Doris ·
更新时间:2024-09-21
· 633 次阅读

AQS源码分析一、锁的介绍1.1 乐观锁/悲观锁1.2 共享锁/独占锁1.3 公平锁/非公平锁1.4 小结二、AQS框架结构介绍2.1 类图2.2 AQS数据结构三、源码详解3.1 acquire源码详解3.2 release源码详解四、从ReentranLock看公平锁和非公平锁的实现区别4.1 公平锁4.2 非公平锁4.3 小结五、实战
为了学习JUC,AQS是基础中的基础,所以我们首先深入了解下AQS。 一、锁的介绍

为了了解AQS的源码,我们需要先大概下锁中的一些功能

1.1 乐观锁/悲观锁

乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度,在java和数据库中都有应用。

概念来讲。对于悲观锁,不管当前是否有线程竞争,都会对当前数据进行加锁,确保数据不会被别的线程修改,在java中,synchronized就是悲观锁

而乐观锁任务自己在使用数据时,不会有别的线程修改数据,所以不会添加锁,只会在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改过的数据成功写入,如果数据已经被其他线程修改,则自动重试。

1.2 共享锁/独占锁

独占锁也叫排他锁,我们在对数据A加排他锁后,其他线程则不能对A加任何类型的锁。

共享锁是指该锁可以被多个线程持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排他锁。获得共享锁的线程只能读数据,不能修改数据,可以参考ReadLock。

1.3 公平锁/非公平锁

对于线程来讲,他最关心什么?线程最关心的就是啥时候能够执行,那么这个线程是否能够按照先来后到的顺序执行就是公平锁和非公平锁的区别。

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程能获取到锁。公平锁的优点就是等待锁的线程不会饿死。缺点就是整体吞吐效率相对非公平锁要低。

非公平锁就是多个线程加锁时直接尝试获取锁,获取不到锁才会到等待队列的队尾等待。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有可能不阻塞就可以锁。缺点是处于等待队列中的线程有可能饿死,可能这个线程永远也轮不到他获取锁。

1.4 小结

AQS是根据CAS来完成锁操作的乐观锁,锁是否被持有,根据state值来判断,内部维护了锁的等待队列,单个锁节点,可以声明为独占节点或者共享节点,是否为公平锁,则要根据不同的获取锁的情况来自定义。

二、AQS框架结构介绍 2.1 类图

在这里插入图片描述
exclusiveOwnerThread:保存当前持有独占锁的线程

Node:队列中的结点

ConditionObject:继承Node,是Condition条件下使用的Node结点

Node head:队列的头部节点

Node tail:队列的尾部节点

Int State:锁的状态值。如果0则代表当前锁没有被持有,如果state>0那么就代表当前锁已经被其他线程获取了

SpinForTimeoutThreshold:自旋的超时时间

2.2 AQS数据结构

AQS内部维护了一个队列,来存储要获取锁的线程(这里先不考虑Condition)。他们都去监控state的值,如果state值为0,则代表当前锁没有被获取,如果state>0,则代表当前锁被获取了。
在这里插入图片描述
队列:是保存Node节点的双向链表,它是CLH的变形队列

Node节点的内部结构:

Node pre:当前节点的前一个节点

Node next:当前节点的下一个节点

Thread:节点所需要执行的线程

nextWaiter:链接到下一个等待的条件节点。这个节点和Condition有关系。

WaitStatus:节点的状态

0:初始化的值 1:Cancelled,由于超时或者中断,该节点被取消,节点永远不会离开此状态,特别的是,具有取消节点的线程将永远不会被再次阻塞 -1:Signal,此节点的后继节点(或即将被阻止)阻塞(通过Park),因此当前节点必须在其释放或取消时取消其后续节点。为了避免竞争,获取方法必须首先表示它们需要一个信号,然后重试原子获取,然后在失败时阻塞。 -2:condition,该节点当前在条件队列中,不会作用于同步队列节点,直到转移,此时状态设置为0。(此时使用此值与字段的其他用途无关,但简化了机制) -3:Propagate,一个realeaseShared节点应该传播到其他节点,在(仅用于头部节点)中设置doReleaseShared以确保传播继续,即使有其他操作介入 三、源码详解 3.1 acquire源码详解

在这里插入图片描述
1.调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;

protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //state为0代表没有加锁 if (c == 0) { //判断Queue中是否有对象等待获取锁,如果对象中没人获得锁则,默认自己获取锁 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //state != 0 的情况下,代表当前锁已经被其他线程占用了,因为是可重入锁,所以需要判断持有锁的线程,和当前线程是否相同,如果相同则对state值进行处理 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } //在state != 0,且并不是当前线程持有当前锁,直接返回false return false; }

2.如果直接获取锁失败,则从队列中获取锁。使用addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
3.acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。

final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { //是否中断默认为false boolean interrupted = false; //无限for循环,直到获得锁 for (;;) { //获得当前节点的上节点 final Node p = node.predecessor(); //如果上个节点是head节点,那么就尝试获得锁 if (p == head && tryAcquire(arg)) { //成功获得锁,设置head节点为当前node setHead(node); p.next = null; // help GC failed = false; //返回中断状态值 return interrupted; } //判断是否要park当前线程,park后再判断是否被中断 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //中断值,标记为true interrupted = true; } } finally { //如果try里面有异常,那么根据falied值,判断是否需要cancelAcquire if (failed) cancelAcquire(node); } }

4.如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } static void selfInterrupt() { Thread.currentThread().interrupt(); }

在if判断满足后,才会把当前线程设置为中断的状态。

3.2 release源码详解

在这里插入图片描述

ReentranLock是独占锁的可重入锁,如果当前线程不是拥有独占锁的线程的直接报错,如果是的话,则判断state的状态是否为0,如果为0的话则设置拥有独占锁的线程为null,如果不为0的话,则代表当前锁还没有被完全释放。 protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; //如果state值为0,才算是释放了,设置当前拥有锁为null,否则只能把state减1 if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } 如果释放成功则判断是否要唤醒队列中的线程 private void unparkSuccessor(Node node) { /* *如果状态为负(即可能需要信号),请尝试 *清除预期的信号。 */ int ws = node.waitStatus; if (ws 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) //唤醒该线程 LockSupport.unpark(s.thread); } 四、从ReentranLock看公平锁和非公平锁的实现区别

先看ReentranLock的类图
在这里插入图片描述
Sync是ReentranLock的抽象内部类,FairSync和NonfairSync则是Sync的实现类,他们同样也是ReentranLock的内部类。

4.1 公平锁

公平锁主要体现在加锁过程

final void lock() { acquire(1); }

具体获取锁的时候先从队列中获取。

protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //state为0代表没有加锁 if (c == 0) { //判断Queue中是否有对象等待获取锁 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; } 4.2 非公平锁

而非公平锁的获取锁流程如下:

final void lock() { //尝试CAS获得锁,如果获取锁成功,则将当前线程设置为独占线程。 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }

先尝试自己获取锁,如果获取锁失败,最终调用nonfairTryAcquire获取锁

final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //0是代表没有锁的状态 if (c == 0) { //CAS比较获取锁 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //如果有锁,需要判断是不是当前持有锁的线程 else if (current == getExclusiveOwnerThread()) { //在当前state上加1 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }

如果这里获取锁失败,那么才会把节点追加到链表尾部。在链表尾部中,这个节点仍然会无限for循环获得锁,获得锁的代码走的仍然是上面nonfairTryAcqires。

4.3 小结

公平锁和非公平锁在获取锁的差别主要是,公平锁每次获取锁,都会先从队列头部获取,而非公平锁每次都是抢占式的,自己先抢占获取锁。

五、实战

简单使用ReentranLock写一段代码

public class ReenterLock implements Runnable { //使用ReentranLock加锁 private static ReentrantLock lock = new ReentrantLock(); public static int i = 0; @Override public void run() { for (int j = 0; j < 10; j++) { lock.lock(); //支持重入锁 //断点位置 lock.lock(); try { i++; } finally { //执行两次解锁 lock.unlock(); lock.unlock(); } } } public static void main(String[] args) throws InterruptedException { ReenterLock tl = new ReenterLock(); Thread t1 = new Thread(tl); Thread t2 = new Thread(tl); System.out.println(t1); System.out.println(t2); t1.start();t2.start(); t1.join();t2.join(); //输出结果 System.out.println(i); } }

如上面的源代码,我们将断点打在断点位置
在这里插入图片描述
0:可见t1是Thread-0,t2是Thread-1
1:当前持有锁的是Thread-0
2:head节点的下一个节点指向Thread-1
3:tail节点是Thread-1
4:因为是可重入锁,当前已经加了2次锁了,所以state的值为2


作者:四眼仔_



juc

需要 登录 后方可回复, 如果你还没有账号请 注册新账号