AQS面试题总结

news2024/11/26 23:30:33

一:线程等待唤醒的实现方法

方式一:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程

必须都在synchronized同步代码块内使用,调用wait,notify是锁定的对象;
notify必须在wait后执行才能唤醒;

public class LockSupportDemo1 {
  public static void main(String[] args) {
    Object objectLock = new Object();
    /**
     * t1	 -----------come in
     * t2	 -----------发出通知
     * t1	 -------被唤醒
     */
    new Thread(() -> {
        synchronized (objectLock) {
            System.out.println(Thread.currentThread().getName() + "\t -----------come in");
            try {
                objectLock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t -------被唤醒");
        }
    }, "t1").start();

    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    new Thread(() -> {
        synchronized (objectLock) {
            objectLock.notify();
            System.out.println(Thread.currentThread().getName() + "\t -----------发出通知");
        }

    }, "t2").start();
 }
}

方式二:使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程

必须在lock同步代码块内使用;
signal必须在await后执行才能唤醒;

public class LockSupportDemo2 {
  public static void main(String[] args) {
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    new Thread(() -> {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t -----------come in");
            condition.await();
            System.out.println(Thread.currentThread().getName() + "\t -----------被唤醒");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }, "t1").start();

    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    new Thread(() -> {
        lock.lock();
        try {
            condition.signal();
            System.out.println(Thread.currentThread().getName() + "\t -----------发出通知");
        } finally {
            lock.unlock();
        }
    }, "t2").start();
 }
}

方式三:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

不需要锁块;
unpark()可以在park()前唤醒;

public class LockSupportDemo {
  public static void main(String[] args) {
    /**
     * t1	 -----------come in
     * t2	 ----------发出通知
     * t1	 ----------被唤醒
     */
    Thread t1 = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "\t -----------come in");
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LockSupport.park();
        System.out.println(Thread.currentThread().getName() + "\t ----------被唤醒");
    }, "t1");
    t1.start();

    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    new Thread(() -> {
        LockSupport.unpark(t1);//指定需要唤醒的线程,可以先给t1发放许可证,t1再被锁定,此时t1可以立马被唤醒
        System.out.println(Thread.currentThread().getName() + "\t ----------发出通知");
    }, "t2").start();
 }
}

二: 介绍一下LockSupport

LockSupport是什么: LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(Permit),许可证只有两个值,1和0;默认是0,许可证不能超过1;
park()方法:调用park方法,当前线程会阻塞,直到别的线程给当前线程发放peimit,park方法才会被唤醒。
unpack(thread)方法: 调用unpack方法,就会将thread线程的许可证peimit发放,唤醒处于阻塞状态的指定线程。

面试题:
1:LockSupport为什么可以先唤醒线程后阻塞线程但不会阻塞?
答:因为unpark()方法获得了一个许可证,许可证值为1,再调用park()方法,就可消费这个许可证,所以不会阻塞;

2:为什么唤醒两次后阻塞两次,最终还是会阻塞?
答:如果线程A调用两遍park(),线程B调用两边unpark(),那么只会解锁一个park(),因为许可证最多只能为1,不能累加;

三:AQS是什么

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列完成资源获取排队工作,将每条要去抢占资源的线程封装成一个NODE节点来实现锁的分配,通过CAS完成对State值的修改,
AQS的本质是一个双向队列加一个状态为state

五:公平锁与非公平锁的区别

公平锁: 多个线程按照线程调用lock()的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

非公平锁: 多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

六: 非公平锁加锁的源码分析

tryAcquire(arg):尝试获取锁
addWaiter(Node.EXCLUSIVE):添加到同步队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)):队列里自旋等待获取等

第一步、tryAquire

先获取当前AQS 的state的值,判断是否为0,如果为0表示没有人抢占,此刻他抢占,返回true,抢占锁后就完事了;

   // 这里调用进入非公平锁的tryAcquire
	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;
                }
            }
        //如果已获取锁的线程再调用lock()则state值+1,这里就是可重入的原理
            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

创建队列的节点,首先就先new出来一个节点,由于刚开始当前队列没有节点,因此进入enq()方法;

enq方法插入节点是死循环:
第一次循环,由于tail为空,他先创建一个空的node节点,作为头节点,此时waitStatus=0,然后将head指向该头节点,并将tail指针也指向head;

第二次循环,他将待插入node节点(线程B)的前置指针指向tail指向的节点(头节点),然后CAS将tail指向当前待插入节点(线程B),再让原来的tail指向的节点(头节点)的next域指向当前节点,这样就完成了节点(线程B)插入队尾,完成链式结构,跳出循环;
在这里插入图片描述

private Node addWaiter(Node mode) {
   	// 创建一个节点 mode
       Node node = new Node(Thread.currentThread(), mode);
       // Try the fast path of enq; backup to full enq on failure
       Node pred = tail;
    // 刚开始tail是null,如果tail有值了就将node插入队尾;
       if (pred != null) {
           node.prev = pred;
           if (compareAndSetTail(pred, node)) {
               pred.next = node;
               return node;
           }
       }
    // 若队列为空,则插入节点
       enq(node);
       return node;
   }


	private Node enq(final Node node) {
        for (;;) { // 死循环
            Node t = tail;
            if (t == null) { // 初始下tail为null,因此创建一个头节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;// 第二次循环,队列不为空,就将该节点插入队尾
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

第三步:aquireQueued

这个方法依旧是死循环。
第一次循环:首先predecessor()取出的就是前置节点,p就是链表中的头节点,然后进入判断,当前确实是头节点,然后再次尝试tryAcquire(),由于线程A并没有释放锁,因此,只能进入shouldParkAfterFailedAcquire()方法;

第二次循环,再次进入shouldParkAfterFailedAcquire(),这一次由于ws=-1,因此返回true,并进入parkAndCheckInterrupt()方法;这里会调用LockSupport.park()将线程挂起,此刻线程B就阻塞再这里了。
在这里插入图片描述

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();//获取节点的前置节点,线程B获取到的是头节点
            if (p == head && tryAcquire(arg)) {//由于线程A占用,尝试获取失败
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())// 线程B会进入这里
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}


private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;// 头节点的waitStatus=0
    if (ws == Node.SIGNAL)// -1
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);// 将头节点的waitStatus设置成-1
    }
    return false;
}

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

七:非公平锁解锁的具体流程

线程A就先去获取AQS的state,并对应减去1个,并设置当前占有线程为null,然后找到头节点去调用unparkSuccessor(head),他将头节点的状态从-1设置为0,然后唤醒线程B;

在这里插入图片描述
在这里插入图片描述

// 执行ReentrantLock.unlock()
public void unlock() {
    sync.release(1);
}

// AQS.release()
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

// 执行ReentrantLock.tryRelease()
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;
    }

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;// 头节点是-1
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);// 头节点设置为0
    Node s = node.next;// 线程B
    if (s == null || s.waitStatus > 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);//唤醒线程B
}

线程B还锁在parkAndCheckInterrupt()方法中,解锁后开始第三次循环,第三次循环发现前置节点是头,且可以占用锁,因此线程B获取到锁并进入第一个if;然后重新设置头节点,将头指向线程B,将原头节点剔除队列,然后将线程B设置成头节点。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();//获取节点的前置节点,线程B获取到的是头节点
            if (p == head && tryAcquire(arg)) {//目前锁无占用,进入此处
                setHead(node); // 重新设置头节点
                p.next = null; // help GC
                failed = false;
                return interrupted; // 被改为true
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())// 线程B从这里唤醒
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

// 修改头节点          
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1160779.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

振弦式传感器读数模块VM5系列介绍

VM5系列是专门针对单线圈式振弦传感器研发&#xff0c;可完成传感器的线圈激励、频率读数、温度测量等工作&#xff0c;具有标准的 UART&#xff08;TTL/RS232/RS485&#xff09;和 IIC 数字接口、模拟量输出接口&#xff08;电压或电流&#xff09;&#xff0c;通过数字接口数…

【论文阅读笔记】GLM-130B: AN OPEN BILINGUAL PRE-TRAINEDMODEL

Glm-130b:开放式双语预训练模型 摘要 我们介绍了GLM-130B&#xff0c;一个具有1300亿个参数的双语(英语和汉语)预训练语言模型。这是一个至少与GPT-3(达芬奇)一样好的100b规模模型的开源尝试&#xff0c;并揭示了如何成功地对这种规模的模型进行预训练。在这一过程中&#xff0…

arcgis图上添加发光效果!

看完本文, 你可以不借助外部图片素材, 让你的图纸符号表达出你想要的光! 我们以之前的某个项目图纸为例,来介绍下让符号发光的技术! 第一步—底图整理 准备好栅格影像底图、行政边界的矢量数据,确保“数据合适、位置正确、边界吻合”。 确定好图纸的大小、出图比例、投…

食品企业数字孪生可视化管理平台,实现智慧轻工业高质量发展

如今&#xff0c;数字技术正在打破传统食品产业的边界&#xff0c;随着食品加工产业链不断进化为智慧体&#xff0c;数字孪生技术已经成了食品行业数字进阶的重要抓手。食品加工数字孪生工厂&#xff0c;通过应用数字孪生技术&#xff0c;将食品加工工厂的自动化生产线全过程进…

浏览器哪家强——PC端篇

今天的分享将围绕一个大家再熟悉不过的名称展开——浏览器。 根据百科给出的解释&#xff1a;浏览器是用来检索、展示以及传递Web信息资源的应用程序。通俗的说&#xff0c;浏览器就是一种阅读工具&#xff0c;类似记事本、word、wps&#xff0c;只不过后者阅读的是文本文档&am…

Linux0.11内核源码解析-malloc

malloc介绍 Linux内核版本0.11中的malloc.c文件实现了内存分配的功能。在这个版本的Linux内核中&#xff0c;malloc.c文件包含了内核级别的内存分配函数&#xff0c;用于分配和释放内核中的内存。这些函数可以帮助内核管理可用的内存&#xff0c;并允许内核动态地分配和释放内…

ajax-axios发送 get请求 或者 发送post请求带有请求体参数

/* axios v0.21.1 | (c) 2020 by Matt Zabriskie */ !function(e,t){"object"typeof exports&&"object"typeof module?module.exportst():"function"typeof define&&define.amd?define([],t):"object"typeof export…

记一次大数据事故@用了很久的虚拟机环境突然不能联网了

记一次大数据事故用了很久的虚拟机环境突然不能联网了 背景 今天打开自己电脑上的虚拟机环境打算练习一下flink&#xff0c;结果发现vmware里虚拟机能正常开机&#xff0c;也能正常进图os&#xff0c;但是就是不能ping通主机&#xff0c;主机也不能ping通虚拟机 探查 1、…

绝缘检测原理和绝缘电阻计算方法

文章目录 简介绝缘检测功能绝缘检测原理绝缘电阻检测的常用方法不平衡电桥法 绝缘电阻绝缘电阻的计算 绝缘检测开启或关闭为什么根据 V1 &#xff1c; V2 或 V1 ≥ V2 判断是上桥臂并入电阻还是下桥臂并入电阻 简介 绝缘检测是判断动力&#xff08;正、负&#xff09;总线与外…

Maven本地配置获取nexus私服的依赖

场景 Nexus-在项目中使用Maven私服&#xff0c;Deploy到私服、上传第三方jar包、在项目中使用私服jar包&#xff1a; Nexus-在项目中使用Maven私服&#xff0c;Deploy到私服、上传第三方jar包、在项目中使用私服jar包_nexus maven-releases 允许deploy-CSDN博客 在上面讲的是…

【6】c++11新特性(稳定性和兼容性)—>Lambda表达式

基本用法 lambda表达式是c最重要也是最常用的特性之一&#xff0c;这是现代编程语言的一个特点&#xff0c;lambda表达式有如下的一些优点&#xff1a; &#xff08;1&#xff09;声明式的编成风格&#xff1a;就地匿名定义目标函数活着函数对象&#xff0c;不需要额外写一个命…

Ubuntu20.04安装CUDA、cuDNN、tensorflow2可行流程(症状:tensorflow2在RTX3090上运行卡住)

最近发现我之前在2080ti上运行好好的代码&#xff0c;结果在3090上运行会卡住很久&#xff0c;而且模型预测结果完全乱掉&#xff0c;于是被迫研究了一天怎么在Ubuntu20.04安装CUDA、cuDNN、tensorflow2。 1.安装CUDA&#xff08;包括CUDA驱动和CUDA toolkit&#xff0c;注意此…

【MySQL】MySQL的安装与配置环境变量(使其在控制台上使用)

作者主页&#xff1a;paper jie_博客 本文作者&#xff1a;大家好&#xff0c;我是paper jie&#xff0c;感谢你阅读本文&#xff0c;欢迎一建三连哦。 本文录入于《MySQL》专栏&#xff0c;本专栏是针对于大学生&#xff0c;编程小白精心打造的。笔者用重金(时间和精力)打造&a…

删除文件要谨慎!如何在Linux中删除目录或文件

删除目录和文件是任何操作系统中最基本但最重要的功能之一。在Linux中,如果运行的是窗口环境,则可以使用文件管理器应用程序查找和删除文件。也许你是通过SSH远程登录的,或者你的Linux计算机没有安装GUI,或者你想对你要删除的内容有更多的控制权。与Linux中的任何东西一样,…

【微服务 Spring Cloud Alibaba】- Nacos 服务注册中心

目录 1. 什么是注册中心&#xff1f; 1.2 注册中心的作用 2. SpringBoot 整合 Nacos 实现服务注册中心 2.1 将服务注册到 Nacos 2.2 实现消费者 3. 服务列表各个参数的含义、作用以及应用场景 1. 什么是注册中心&#xff1f; 注册中心是微服务架构中的一个重要组件&…

NoSQL数据库以及架构介绍

文章目录 一. 什么是NoSQL&#xff1f;二. NoSQL分类三. NoSQL与关系数据库有什么区别四. NoSQL主要优势和缺点五. NoSQL体系框架 其它相关推荐&#xff1a; 系统架构之微服务架构 系统架构设计之微内核架构 鸿蒙操作系统架构 架构设计之大数据架构&#xff08;Lambda架构、Kap…

Selenium学习(Java + Edge)

Selenium /səˈliːniəm/ 1. 简介 ​ Selenium是一个用于Web应用程序自动化测试工具。Selenium测试直接运行在浏览器中&#xff0c;就像真正的用户在操作一样。支持的浏览器包括IE、Mozilla Firefox、Safari、Google Chrome、Opera、Edge等。 ​ 适用于自动化测试&#x…

Linux期末复习——文件I/O编程

Linux系统调用以及用户编程接口 三者关系 系统调用、API以及系统命令之间关系&#xff1a; 什么是文件描述符&#xff1f; 是一个非负整数&#xff0c;索引值 打开或者创建一个文件的时候&#xff0c;内核会向进程返回一个文件描述符 读写文件时&#xff0c;会向函数传递一个文…

ModuleNotFoundError: No module named ‘paddle.fluid.incubate.fleet‘

在使用rocketqa的时候可能会遇到下面的问题&#xff1a; 问题&#xff1a; 解决方法&#xff1a; 这完全是paddlepaddle的问题。 在rocketqa/utils/optimization.py出现下面的语句&#xff0c;这个时候直接把出错的注释掉就可以&#xff0c;因为它完全没有用到。&#xff08;…

Win10奇怪的部分文字乱码问题

1.打开Windows设置的时间和语言 2.打开区域下方的的其他日期、时间和区域设置 3.点击更改日期、时间或数字格式。切换到管理 4.点击更改系统区域设置&#xff0c;取消Beta版&#xff1a;使用Unicode UTF-8提供全球语言支持。 按提示重启电脑即可。