2024年Java最新AQS(AbstractQueuedSynchronizer)——源码分析,牛逼

最后

光给面试题不给答案不是我的风格。这里面的面试题也只是凤毛麟角,还有答案的话会极大的增加文章的篇幅,减少文章的可读性

Java面试宝典2021版

最常见Java面试题解析(2021最新版)

2021企业Java面试题精选

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • @throws InterruptedException

*/

public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)

throws InterruptedException {

if (Thread.interrupted())

throw new InterruptedException();

return tryAcquireShared(arg) >= 0 ||

doAcquireSharedNanos(arg, nanosTimeout);

}

/**

  • 独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒

  • @param arg

  • @return

*/

public final boolean release(int arg) {

if (tryRelease(arg)) {

java.util.concurrent.locks.AbstractQueuedSynchronizer.Node h = head;

if (h != null && h.waitStatus != 0)

unparkSuccessor(h);

return true;

}

return false;

}

/**

  • 共享式的释放同步状态

  • @param arg

  • @return

*/

public final boolean releaseShared(int arg) {

if (tryReleaseShared(arg)) {

doReleaseShared();

return true;

}

return false;

}

/**

  • 获取等待在同步队列上的线程集合

  • @return

*/

public final Collection getQueuedThreads() {

ArrayList list = new ArrayList();

for (java.util.concurrent.locks.AbstractQueuedSynchronizer.Node p = tail; p != null; p = p.prev) {

Thread t = p.thread;

if (t != null)

list.add(t);

}

return list;

}

总结

| 方法名称 | 描述 |

| — | — |

| void acquire(int arg) | 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回;否则,将会进入同步等待队列等待,该方法将会调用重写的tryAcquire(int arg)方法 |

| void acquireInterruptibly(int arg) | 与acquire方法相同,但是该方法可以响应中断,当前线程未获取到同步状态进入同步队列中,如果当前线程被中断,则该方法抛出InterruptedException异常 |

| tryAcquireNanos(int arg, long nanosTimeout) | 在acquireInterruptibly方法的基础上增加了超时限制,如果当前线程在超时时间内未获取到同步状态,则返回false,获取到则返回true |

| acquireShared(int arg) | 共享式额获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式获取的主要区别是在同一时刻可以由多个线程同时获取到同步状态 |

| void acquireSharedInterruptibly(int arg) | 与acquireShared方法相同,该方法响应中断 |

| tryAcquireSharedNanos(int arg, long nanosTimeout) | 在acquireSharedInterruptibly的基础上增加了超时限制 |

| boolean release(int arg) | 独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒 |

| boolean releaseShared(int arg) | 共享式的释放同步状态 |

| Collection getQueuedThreads() | 获取等待在同步队列上的线程集合 |

2.2 自定义独占锁加强AbstractQueuedSynchronizer的工作原理的理解

独占锁指的是,同一时刻只能有一个线程获取到锁,其他获取锁的线程只能处于等待队列中等待,只有获取到锁的线程释放了锁,后继的线程才能获取到锁。(不太了解的可以写一遍,基本上就懂了)

package com.lizba.p5;

import java.util.concurrent.TimeUnit;

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

import java.util.concurrent.locks.Condition;

import java.util.concurrent.locks.Lock;

/**

  •  自定义独占锁
    
  • @Author: Liziba

  • @Date: 2021/6/19 22:40

*/

public class Mutex implements Lock {

private static class Sync extends AbstractQueuedSynchronizer {

/**

  • 尝试获取锁

  • @param arg

  • @return

*/

@Override

protected boolean tryAcquire(int arg) {

// 当前状态如果为0则获取到锁

if (compareAndSetState(0, 1)) {

// 设置锁的占用线程为当线程

setExclusiveOwnerThread(Thread.currentThread());

return true;

}

return false;

}

/**

  • 尝试释放锁

  • @param arg

  • @return

*/

@Override

protected boolean tryRelease(int arg) {

// 如果当前同步状态为0,调用该方法则抛出异常

if (getState() == 0) {

throw new IllegalMonitorStateException();

}

// 清空占用线程

setExclusiveOwnerThread(null);

// 设置共享状态为0

setState(0);

return true;

}

/**

  • 判断当前线程是否处于占用状态

  • @return

*/

@Override

protected boolean isHeldExclusively() {

return getState() == 1;

}

/**

  • 返回一个Condition,每个Condition包含一个condition队列

  • @return

*/

Condition condition() {

return new ConditionObject();

}

}

// 将需要的操作代理至Sync上

private Sync sync = new Sync();

@Override

public void lock() {

sync.acquire(1);

}

@Override

public boolean tryLock() {

return sync.tryAcquire(1);

}

@Override

public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {

return sync.tryAcquireNanos(1, unit.toNanos(time));

}

@Override

public void lockInterruptibly() throws InterruptedException {

sync.acquireInterruptibly(1);

}

@Override

public void unlock() {

sync.release(1);

}

@Override

public Condition newCondition() {

return sync.condition();

}

public boolean hasQueuedThreads() {

return sync.hasQueuedThreads();

}

}

总结自定义同步组件Mutex互斥锁:

通过自定义同步组件Mutex我们可以看出。Mutex定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。用户在使用Mutex的时候并不会直接和内部同步器打交道,而是调用Mutex提供的方法,在Mutex的实现中以获取锁的lock()方法为例,只需要在方法实现中调用同步器的模板方法acquire(int args)即可。这种实现方法大大降低了实现一个可靠自定义同步组件的门槛。(不多说_Doug Lea牛逼_)

3、AbstractQueuedSynchronizer实现分析

分析的主要内容包括如下几个方面

  1. 同步队列

  2. 独占式同步状态获取与释放

  3. 共享式同步状态获取与释放

  4. 超时获取同步状态

3.1 同步队列

同步队列实现依赖的是内部的一个(FIFO)的同步队列来完成同步状态管理的,而这个队列的重中之重就是AbstractQueuedSynchronizer的内部类Node,这个Node节点是用来保存同步失败的线程引用、等待状态以及前驱prev和后继next节点。

节点源码重点部分解释:

static final class Node {

/**

  • 等待状态

  • 0 初始状态

  • -3 = PROPAGATE 表示下一次共享式同步状态获取将会无条件地传播下去

  • -2 = CONDITION 节点线程等待在Condition上,当其他线程对Condition调用了signal()后,节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中

  • -1 = SIGNAL 当前节点的线程释放同步状态或者被取消,则通知后继节点,使其节点线程得以运行

  • 1 = CANCELLED 同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消,进入该状态后将不会在改变状态

*/

volatile int waitStatus;

/**

  • 前驱节点

*/

volatile Node prev;

/**

  • 后继节点

*/

volatile Node next;

/**

  • 获取同步状态的线程

*/

volatile Thread thread;

/**

  • 等待在Condition上的后继节点。当前节点是共享模式时,含义特殊,它也代表节点类型(独占/共享)

*/

Node nextWaiter;

}

在AbstractQueuedSynchronizer的内部类代表同步队列中的节点,而AbstractQueuedSynchronizer会持有首节点(head)和尾节点(tail),获取同步状态失败的节点加入队列的尾部。

/**

  • AbstractQueuedSynchronizer中持有的同步队列的头节点

*/

private transient volatile Node head;

/**

  • AbstractQueuedSynchronizer中持有的同步队列的尾节点

*/

private transient volatile Node tail;

通过一组图来查看AQS的结构

AQS同步队列的基本结构

  1. 同步器持有首节点和尾节点,初始都为null

  2. 获取同步状态失败的线程节点加入队列尾部

在这里插入图片描述

AQS同步队列加入节点

  1. 通过compareAndSetTail(Node expect, Node update)提供的CAS操作来正确的设置尾节点

  2. 加入尾节点成功后,将原先尾节点的后继节点指向新的尾节点,新的尾节点的前驱节点设置为原尾节点

在这里插入图片描述

AQS同步队列设置首节点

  1. 同步队列节点顺序出入队遵循FIFO

  2. 原先首节点释放同步状态,唤醒后继节点,后继节点获取同步状态成功后设置自己为首节点

在这里插入图片描述

3.2 独占式同步状态获取与释放

独占式同步状态获取通过调用同步器的acquire(int arg)获取同步状态,注意该方法不响应中断。

获取解析

acquire(int arg)源码解析

/**

  • 方法主要完成以下功能

  • 1、线程安全的同步状态获取

  • 2、节点构造加入同步队列

  • 3、同步队列中自旋

*/

public final void acquire(int arg) {

if (!tryAcquire(arg) &&

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

selfInterrupt();

}

acquire(int arg)之addWaiter(Node mode)

/**

  • 尝试将获取同步状态失败的节点通过CAS的方式加入同步队列尾部

*/

private Node addWaiter(Node mode) {

// 创建一个新的节点,设置节点信息,和节点线程为当前线程

Node node = new Node(Thread.currentThread(), mode);

// 获取尾节点,当尾节点不为空的时候,尝试设置尾节点

Node pred = tail;

if (pred != null) {

// 设置当前插入节点的前驱节点为当前同步队列中的尾节点

node.prev = pred;

// 通过CAS快速设置尾节点为当前插入的节点

if (compareAndSetTail(pred, node)) {

// 如设置尾节点成功,将pred节点(同步队列上一个的尾节点,此时新的尾节点为插入节点)的后继节点指向新插入的节点(新的尾节点)

pred.next = node;

// 返回节点对象

return node;

}

}

enq(node);

return node;

}

addWaiter(Node mode)之 enq(node)

/**

  • 通过“死循环”的方式来正确的添加节点

*/

private Node enq(final Node node) {

// 不断循环,直至CAS插入节点成功

for (;😉 {

Node t = tail;

if (t == null) {

// 当尾节点为null,此时需要初始化头节点和尾节点

if (compareAndSetHead(new Node()))

tail = head;

} else {

// 插入节点前驱节点指向原先尾节点

node.prev = t;

// CAS插入至同步队列的尾节点

if (compareAndSetTail(t, node)) {

t.next = node;

return t;

}

}

}

}

acquire(int arg)之 acquireQueued(final Node node, int arg)

/**

  • 死循环获取同步状态,并且当前仅当前驱节点是头节点是才能够尝试获取同步状态

*/

final boolean acquireQueued(final Node node, int arg) {

boolean failed = true;

try {

boolean interrupted = false;

// 不断循环

for (;😉 {

// 获取当前节点的前驱节点,如果前驱节点为null将会抛出空指针异常

final Node p = node.predecessor();

// 如果当前节点的前驱节点是头节点,尝试获取同步状态

if (p == head && tryAcquire(arg)) {

// 设置当前节点为头节点,并且将节点线程和节点的前驱节点置为null,help GC

setHead(node);

p.next = null; // help GC

failed = false;

return interrupted;

}

// 如果不符合条件,则判断当前节点前驱节点的waitStatus状态来决定是否需要挂起LockSupport.park(this);

if (shouldParkAfterFailedAcquire(p, node) &&

parkAndCheckInterrupt())

interrupted = true;

}

} finally {

// 失败则取消

if (failed)

cancelAcquire(node);

}

}

为何当且仅当当前节点的前驱节点是头节点才能尝试获取同步状态?

我们先看一张节点自旋获取同步状态的图,再总结原因

在这里插入图片描述

  1. 头节点是成功获取同步状态的节点,头节点线程释放同步状态之后会唤醒后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点

  2. 保持并维护同步队列FIFO的出队入队原则

acquire(int arg) 执行时序图

在这里插入图片描述

3.2 release

当同步状态获取成功之后,当前线程从acquire(int arg)方法返回,执行响应逻辑执行,需要释放同步状态,是的后续节点能够继续获取同步状态。释放同步状态调用的是release(int arg) 方法,该方法释放同步状态之后,会唤醒其他后继节点。

源码解析:

/**

  • 释放同步状态,并且唤醒后继节点

*/

public final boolean release(int arg) {

// 尝试释放同步状态

if (tryRelease(arg)) {

Node h = head;

// 如果头节点不为空,并且头节点的等待状态waitStatus不为初始状态0,此时判断为仍存在后继节点

if (h != null && h.waitStatus != 0)

// 使用LockSupport来唤醒处于等待状态的线程

unparkSuccessor(h);

return true;

}

return false;

}

3.3 独占式同步状态获取与释放总结
  1. 在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并且在队列中进行自旋

  2. 移除队列或停止自旋的条件是当前节点的前驱节点是头节点并且成功的获取到了同步状态

  3. 释放同步状态时调用release(int arg) 来释放同步状态,如果存在后继节点则唤醒后继节点

3.3 共享式同步状态获取与释放

区别

共享式同步状态获取与独占式同步状态获取的主要区别在于同一时刻释放能被多个线程同时获取到同步状态。例如,文件的读写,一个程序对文件进行读操作,那么此时运行多个读操作同步进行,但是写操作将会被阻塞。而独占式访问,例如写操作,此时会阻塞其他所有的读写操作。

共享式和独占式访问资源对比

在这里插入图片描述

源码解析:

/**

  • 尝试获取共享同步状态,tryAcquireShared(arg) >= 0表示获取成功

*/

public final void acquireShared(int arg) {

if (tryAcquireShared(arg) < 0)

doAcquireShared(arg);

}

acquireShared(int arg)之doAcquireShared(int arg)

/**

*/

private void doAcquireShared(int arg) {

final Node node = addWaiter(Node.SHARED);

boolean failed = true;

try {

boolean interrupted = false;

for (;😉 {

// 判断当前节点的前驱节点是否为头节点

final Node p = node.predecessor();

if (p == head) {

// tryAcquireShared(arg) >= 0表示获取同步状态成功

int r = tryAcquireShared(arg);

if (r >= 0) {

setHeadAndPropagate(node, r);

p.next = null; // help GC

if (interrupted)

selfInterrupt();

failed = false;

return;

}

}

if (shouldParkAfterFailedAcquire(p, node) &&

parkAndCheckInterrupt())

interrupted = true;

}

} finally {

if (failed)

cancelAcquire(node);

}

}

共享式同步状态释放

/**

  • 释放同步状态,并且唤醒后继节点

*/

public final boolean releaseShared(int arg) {

if (tryReleaseShared(arg)) {

doReleaseShared();

return true;

}

return false;

}

共享式释放同步状态与独占式有区别,共享式因为同步状态可以被多个线程获取,自然释放的时候也可以来自多个线程,因此要tryReleaseShared(int arg)要保证同步状态的正确释放,具体实现可以在支持多线程同时访问的并发组件Semaphore中阅读。

最后

权威指南-第一本Docker书

引领完成Docker的安装、部署、管理和扩展,让其经历从测试到生产的整个开发生命周期,深入了解Docker适用于什么场景。并且这本Docker的学习权威指南介绍了其组件的基础知识,然后用Docker构建容器和服务来完成各种任务:利用Docker为新项目建立测试环境,演示如何使用持续集成的工作流集成Docker,如何构建应用程序服务和平台,如何使用Docker的API,如何扩展Docker。

总共包含了:简介、安装Docker、Docker入门、使用Docker镜像和仓库、在测试中使用Docker、使用Docker构建服务、使用Fig编配Docke、使用Docker API、获得帮助和对Docker进行改进等9个章节的知识。

image

image

image

image

关于阿里内部都在强烈推荐使用的“K8S+Docker学习指南”—《深入浅出Kubernetes:理论+实战》、《权威指南-第一本Docker书》,看完之后两个字形容,爱了爱了!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

*/

public final boolean releaseShared(int arg) {

if (tryReleaseShared(arg)) {

doReleaseShared();

return true;

}

return false;

}

共享式释放同步状态与独占式有区别,共享式因为同步状态可以被多个线程获取,自然释放的时候也可以来自多个线程,因此要tryReleaseShared(int arg)要保证同步状态的正确释放,具体实现可以在支持多线程同时访问的并发组件Semaphore中阅读。

最后

权威指南-第一本Docker书

引领完成Docker的安装、部署、管理和扩展,让其经历从测试到生产的整个开发生命周期,深入了解Docker适用于什么场景。并且这本Docker的学习权威指南介绍了其组件的基础知识,然后用Docker构建容器和服务来完成各种任务:利用Docker为新项目建立测试环境,演示如何使用持续集成的工作流集成Docker,如何构建应用程序服务和平台,如何使用Docker的API,如何扩展Docker。

总共包含了:简介、安装Docker、Docker入门、使用Docker镜像和仓库、在测试中使用Docker、使用Docker构建服务、使用Fig编配Docke、使用Docker API、获得帮助和对Docker进行改进等9个章节的知识。

[外链图片转存中…(img-upEQ6ZrS-1714905678858)]

[外链图片转存中…(img-B4jJbrAy-1714905678858)]

[外链图片转存中…(img-ZWeJaaBu-1714905678858)]

[外链图片转存中…(img-XqIw1ZsJ-1714905678859)]

关于阿里内部都在强烈推荐使用的“K8S+Docker学习指南”—《深入浅出Kubernetes:理论+实战》、《权威指南-第一本Docker书》,看完之后两个字形容,爱了爱了!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 30
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的AQSAbstractQueuedSynchronizer)是实现锁和同步器的一种重要工具。在AQS中,一个节点表示一个线程,依次排列在一个双向队列中,同时使用CAS原子操作来保证线程安全。当多个线程对于同一资源竞争时,一个节点会被放置在队列的尾部,其他线程则在其之前等待,直到该资源可以被锁定。 当一个线程调用lock()方法进行锁定时,它会首先调用tryAcquire()方法尝试获取锁。如果当前资源尚未被锁定,则该线程成功获取锁,tryAcquire()返回true。如果当前资源已被锁定,则线程无法获取锁,tryAcquire()返回false。此时该线程就会被加入到等待队列中,同时被加入到前一个节点的后置节点中,即成为它的后继。然后该线程会在park()方法处等待,直到前一个节点释放了锁,再重新尝试获取锁。 在AQS中,当一个节点即将释放锁时,它会调用tryRelease()方法来释放锁,并唤醒后置节点以重试获取锁。如果当前节点没有后置节点,则不会发生任何操作。当一个线程在队列头部成功获取锁和资源时,该线程需要使用release()方法释放锁和资源,并唤醒等待队列中的后置节点。 总之,AQS中的锁机制是通过双向等待队列实现的,其中节点表示线程,使用CAS原子操作保证线程安全,并在tryAcquire()和tryRelease()方法中进行锁定和释放。该机制保证了多线程环境下资源的正确访问和线程的安全执行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值