AQS源码分析看这一篇就够了

最后

由于篇幅有限,这里就不一一罗列了,20道常见面试题(含答案)+21条MySQL性能调优经验小编已整理成Word文档或PDF文档

MySQL全家桶笔记

还有更多面试复习笔记分享如下

Java架构专题面试复习

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

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

在这里我们有一个能被多个线程共享操作的资源,在这个场景中应该能看出我们的数据是不安全的,因为我们并不能保证我们的操作是原子操作对吧。基于这个场景我们通过代码来看看效果

package com.example.demo;

public class AtomicDemo {

// 共享变量

private static int count = 0;

// 操作共享变量的方法

public static void incr(){

// 为了演示效果 休眠一下子

try {

Thread.sleep(1);

count ++;

} catch (InterruptedException e) {

e.printStackTrace();

}

}

public static void main(String[] args) throws InterruptedException {

for (int i = 0; i < 1000 ; i++) {

new Thread(()->AtomicDemo.incr()).start();

}

Thread.sleep(4000);

System.out.println(“result:” + count);

}

}

通过执行发现,执行的结果是一个不确定的值,但总是会小于等于1000,至于原因,是因为incr() 方法不是一个原子操作。为什么不是原子操作这个咱们今天就不深究此处了.

迎合今天的主题,我们通过Lock来解决

package com.example.demo;

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

public class AtomicDemo {

// 共享变量

private static int count = 0;

private static Lock lock = new ReentrantLock();

// 操作共享变量的方法

public static void incr(){

// 为了演示效果 休眠一下子

try {

lock.lock();

Thread.sleep(1);

count ++;

} catch (InterruptedException e) {

e.printStackTrace();

}finally {

lock.unlock();

}

}

public static void main(String[] args) throws InterruptedException {

for (int i = 0; i < 1000 ; i++) {

new Thread(()->AtomicDemo.incr()).start();

}

Thread.sleep(4000);

System.out.println(“result:” + count);

}

}

然后我们运行发现结果都是 1000了,这也就是1000个线程都去操作这个 count 变量,结果符合我们的预期了。那lock到底是怎么实现的呢?

需求分析

===================================================================

我们先来分析分析

在这里插入图片描述

这样的图片看着比较复杂,咱们简化下。

在这里插入图片描述

我们自己假设下,如果要你去设计这样的方法,你应该要怎么设计,他们需要实现哪些功能,

首先是lock方法,它是不是要满足这几个功能。

在这里插入图片描述

需求清楚了,那我们怎么设计呢?

第一个互斥怎么做,也就是多个线程只有一个线程能抢占到资源,这个时候我们可以这样设置

// 给一个共享资源

Int state = 0 ; // 0表示资源没有被占用,可以抢占

if(state == 0 ){

// 表示可以获取锁

}else{

// 表示锁被抢占 需要阻塞等待

}

在这里插入图片描述

然后就是没有抢占到锁的线程的存储,我们可以通过一个队列,利用FIFO来实现存储。

最后就是线程的阻塞和唤醒。大家说说有哪些阻塞线程的方式呀?

  1. wait/notify: 不合适,不能唤醒指定的线程

  2. Sleep:休眠,类似于定时器

  3. Condition:可以唤醒特定线程

  4. LockSupport:

LockSupport.park():阻塞当前线程

LockSupport.unpark(Thread t):唤醒特定线程

结合今天的主题,我们选择LockSupport来实现阻塞和唤醒。

在这里插入图片描述

好了,到这儿我们已经猜想到了Lock中的实现逻辑,但是在探究源码之前我们还有个概念需要先和大家讲下,因为这个是我们源码中会接触到的一个,先讲了,看的时候就比较轻松了对吧。

什么是重入锁?

======================================================================

我们先来看看重入锁的场景代码

package com.example.demo;

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

public class AtomicDemo {

// 共享变量

private static int count = 0;

private static Lock lock = new ReentrantLock();

// 操作共享变量的方法

public static void incr(){

// 为了演示效果 休眠一下子

try {

lock.lock();

Thread.sleep(1);

count ++;

// 调用了另外一个方法。

decr();

} catch (InterruptedException e) {

e.printStackTrace();

}finally {

lock.unlock();

}

}

public static void decr(){

try {

// 重入锁

lock.lock();

count–;

}catch(Exception e){

}finally {

lock.unlock();

}

}

public static void main(String[] args) throws InterruptedException {

for (int i = 0; i < 1000 ; i++) {

new Thread(()->AtomicDemo.incr()).start();

}

Thread.sleep(4000);

System.out.println(“result:” + count);

}

}

首先大家考虑这段代码会死锁吗? 大家给我个回复,我看看大家的理解的怎么样

好了,有说会死锁的,有说不会,其实这儿是不会死锁的,而且结果就是0.为什么呢?

这个其实是锁的一个嵌套,因为这两把锁都是同一个 线程对象,我们讲共享变量的设计是

当state=0;线程可以抢占到资源 state =1; 如果进去嵌套访问 共享资源,这时 state = 2 如果有多个嵌套 state会一直累加,释放资源的时候, state–,直到所有重入的锁都释放掉 state=0,那么其他线程才能继续抢占资源,说白了重入锁的设计目的就是为了防止 死锁

AQS类图

====================================================================

在这里插入图片描述

通过类图我们可以发现右车的业务应用其实内在都有相识的设计,这里我们只需要搞清楚其中的一个,其他的你自己应该就可以看懂~,好了我们就具体结合前面的案例代码,以ReentrantLock为例来介绍AQS的代码实现。

源码分析

===================================================================

在看源码之前先回顾下这个图,带着问题去看,会更轻松

在这里插入图片描述

Lock.lock()


final void lock() {

if (compareAndSetState(0, 1))

setExclusiveOwnerThread(Thread.currentThread());

else

acquire(1);

}

这个方法逻辑比较简单,if条件成立说明 抢占锁成功并设置 当前线程为独占锁

else 表示抢占失败,acquire(1) 方法我们后面具体介绍

compareAndSetState(0, 1):用到了CAS 是一个原子操作方法,底层是UnSafe.作用就是设置 共享操作的 state 由0到1. 如果state的值是0就修改为1

setExclusiveOwnerThread:代码很简单,进去看一眼即可

acquire方法

public final void acquire(int arg) {

if (!tryAcquire(arg) &&

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

selfInterrupt();

}

  1. tryAcquire()尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待);

  2. addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;

  3. acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

当然这里代码的作用我是提前研究过的,对于大家肯定不是很清楚,我们继续里面去看,最后大家可以回到这儿再论证。

tryAcquire(int)

再次尝试抢占锁

protected final boolean tryAcquire(int acquires) {

return nonfairTryAcquire(acquires);

}

final boolean nonfairTryAcquire(int acquires) {

final Thread current = Thread.currentThread();

int c = getState();

//再次尝试抢占锁

if (c == 0) {

if (compareAndSetState(0, acquires)) {

setExclusiveOwnerThread(current);

return true;

}

}

// 重入锁的情况

else if (current == getExclusiveOwnerThread()) {

int nextc = c + acquires;

if (nextc < 0) // overflow

throw new Error(“Maximum lock count exceeded”);

setState(nextc);

return true;

}

// false 表示抢占失败

return false;

}

addWaiter

将阻塞的线程添加到双向链表的结尾

private Node addWaiter(Node mode) {

//以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)

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

//尝试快速方式直接放到队尾。

Node pred = tail;

if (pred != null) {

node.prev = pred;

if (compareAndSetTail(pred, node)) {

pred.next = node;

return node;

}

}

//上一步失败则通过enq入队。

enq(node);

return node;

}

enq(Node)

private Node enq(final Node node) {

//CAS"自旋",直到成功加入队尾

for (;😉 {

Node t = tail;

if (t == null) { // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。

if (compareAndSetHead(new Node()))

tail = head;

} else {//正常流程,放入队尾

node.prev = t;

if (compareAndSetTail(t, node)) {

t.next = node;

return t;

}

}

}

}

第一个if语句

在这里插入图片描述

else语句

在这里插入图片描述

线程3进来会执行如下代码

在这里插入图片描述

那么效果图

在这里插入图片描述

acquireQueued(Node, int)

OK,通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了。聪明的你立刻应该能想到该线程下一部该干什么了吧:进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。没错,就是这样!是不是跟医院排队拿号有点相似~~acquireQueued()就是干这件事:在等待队列中排队拿号(中间没其它事干可以休息),直到拿到号后再返回。这个函数非常关键,还是上源码吧:

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

boolean failed = true;//标记是否成功拿到资源

try {

boolean interrupted = false;//标记等待过程中是否被中断过

//又是一个“自旋”!

for (;😉 {

final Node p = node.predecessor();//拿到前驱

//如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。

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

setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。

p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!

failed = false; // 成功获取资源

return interrupted;//返回等待过程中是否被中断过

}

//如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。

if (shouldParkAfterFailedAcquire(p, node) &&

parkAndCheckInterrupt())

interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true

}

} finally {

if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。

cancelAcquire(node);

}

}

最后

关于面试刷题也是有方法可言的,建议最好是按照专题来进行,然后由基础到高级,由浅入深来,效果会更好。当然,这些内容我也全部整理在一份pdf文档内,分成了以下几大专题:

  • Java基础部分

  • 算法与编程

  • 数据库部分

  • 流行的框架与新技术(Spring+SpringCloud+SpringCloudAlibaba)

这份面试文档当然不止这些内容,实际上像JVM、设计模式、ZK、MQ、数据结构等其他部分的面试内容均有涉及,因为文章篇幅,就不全部在这里阐述了。

作为一名程序员,阶段性的学习是必不可少的,而且需要保持一定的持续性,这次在这个阶段内,我对一些重点的知识点进行了系统的复习,一方面巩固了自己的基础,另一方面也提升了自己的知识广度和深度。

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

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

下被中断了),那么取消结点在队列中的等待。

cancelAcquire(node);

}

}

最后

关于面试刷题也是有方法可言的,建议最好是按照专题来进行,然后由基础到高级,由浅入深来,效果会更好。当然,这些内容我也全部整理在一份pdf文档内,分成了以下几大专题:

  • Java基础部分

[外链图片转存中…(img-c5Xlgo78-1715811082722)]

  • 算法与编程

[外链图片转存中…(img-8oQC6WIg-1715811082723)]

  • 数据库部分

[外链图片转存中…(img-VveEXN59-1715811082723)]

  • 流行的框架与新技术(Spring+SpringCloud+SpringCloudAlibaba)

[外链图片转存中…(img-nGmq32he-1715811082723)]

这份面试文档当然不止这些内容,实际上像JVM、设计模式、ZK、MQ、数据结构等其他部分的面试内容均有涉及,因为文章篇幅,就不全部在这里阐述了。

作为一名程序员,阶段性的学习是必不可少的,而且需要保持一定的持续性,这次在这个阶段内,我对一些重点的知识点进行了系统的复习,一方面巩固了自己的基础,另一方面也提升了自己的知识广度和深度。

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

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

  • 8
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的AQS(AbstractQueuedSynchronizer)是实现锁和同步器的一种重要工具。在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、付费专栏及课程。

余额充值