阅读完synchronized和ReentrantLock的源码后,我竟发现其完全相似

news2024/10/6 18:22:43
  • 👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主
  • 📕系列专栏:Java设计模式、数据结构和算法、Kafka从入门到成神、Kafka从成神到升仙、Spring从成神到升仙系列
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人
  • 📝联系方式:hls1793929520,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀

在这里插入图片描述

文章目录

  • ConditionObject
    • 一、引言
    • 二、使用
    • 三、源码
      • 1、newCondition
      • 2、await-挂起前的操作
        • 2.1 addConditionWaiter
        • 2.2 fullyRelease
        • 2.3 isOnSyncQueue
        • 2.4 问题考查
      • 3、signal
        • 3.1 isHeldExclusively
        • 3.2 doSignal
      • 4、await-唤醒后的操作
        • 4.1 checkInterruptWhileWaiting
    • 四、流程图
    • 五、写在最后

ConditionObject

一、引言

并发编程在互联网技术使用如此广泛,几乎所有的后端技术面试官都要在并发编程的使用和原理方面对小伙伴们进行 360° 的刁难。

作为一个在互联网公司面一次拿一次 Offer 的面霸,打败了无数竞争对手,每次都只能看到无数落寞的身影失望的离开,略感愧疚(请允许我使用一下夸张的修辞手法)。

于是在一个寂寞难耐的夜晚,暖男我痛定思痛,决定开始写 《吊打面试官》 系列,希望能帮助各位读者以后面试势如破竹,对面试官进行 360° 的反击,吊打问你的面试官,让一同面试的同僚瞠目结舌,疯狂收割大厂 Offer

虽然现在是互联网寒冬,但乾坤未定,你我皆是黑马

二、使用

我们上篇文章分析了 ReentrantLocklockunLock 方法,具体可见:ReentrantLock

我们知道,对于 synchronized 来说,拥有 waitnotify 方法,可暂停和唤醒线程,具体可见:synchronized

作为 synchronized 的竞争对手,AQS 必然也提供了此功能,我们一起来看看 AQS 中的使用

这里吐槽一句:这个唤醒的流程,AQSsynchronized 有点神似

public class ConditionObjectTest {
    public static void main(String[] args) throws Exception{
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        new Thread(() -> {
            lock.lock();
            System.out.println("子线程获取锁资源并await挂起线程");
            try {
                Thread.sleep(5000);
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("子线程挂起后被唤醒!持有锁资源");

        }).start();

        Thread.sleep(100);
        // =================main======================
        lock.lock();
        System.out.println("主线程等待5s拿到锁资源,子线程执行了await方法");
        condition.signal();
        System.out.println("主线程唤醒了await挂起的子线程");
        lock.unlock();

    }
}

我们运行上述代码,可以发现执行步骤如下:

子线程获取锁资源并await挂起线程
主线程等待5s拿到锁资源,子线程执行了await方法
主线程唤醒了await挂起的子线程
子线程挂起后被唤醒!持有锁资源

我们简单的说一下过程,具体的我们后面源码会讲到:

  • 首先,我们的子线程执行 lock.lock() 方法获取锁资源,将 AQS 中的 state0 修改为 1
  • 我们的主线程执行 lock.lock() 方法,察觉当前的 state1,封装成 Node 节点放至 AQS 队列中,随后 park 挂起;
  • 当子线程执行 condition.await() 方法时,将该线程封装成 Node 扔到 Condition队列 中并放弃锁资源。我们的主线程被唤醒且将 state0 修改为 1,拿到锁资源;
  • 主线程执行 condition.signal() 将我们的子线程让 ConditionObject 里面扔到 AQS 里面,等待被被唤醒;
  • 主线程执行 lock.unlock() 方法让出锁资源,唤醒子线程执行后续的业务逻辑;

三、源码

1、newCondition

首先肯定是我们 Condition 的构造方法了,我们主要是通过 lock.newCondition() 来获取,该方法是不区分公平锁、非公平锁的

public Condition newCondition() {
    return sync.newCondition();
}

final ConditionObject newCondition() {
    return new ConditionObject();
}

这里我们可以看到,朴实无华的 new 了一个 ConditionObject 返回,我们看下 ConditionObject 里面的参数

public class ConditionObject implements Condition {
    // 头节点
    private transient Node firstWaiter;
    // 尾结点
    private transient Node lastWaiter;
}

我们看到这里,可能感觉和我们上一篇 AQS 队列中的双向链表差不多,但要记住:这里是一个单向链表,他的指针是 Node nextWaiter 并非 prevnext

2、await-挂起前的操作

我们在讲 await 方法时,会分两部分讲:

  • 第一部分:我们执行 await 方法直到 park

  • 第二部分:unpark 后续的操作

public final void await() throws InterruptedException {
    // 判断当前线程是不是处于中断,如果是中断,则抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 干掉所有标识非CONDITION(-2)的节点并将该线程封装成Node节点放到Condition队列中
    Node node = addConditionWaiter();
    // 释放当前的锁资源并唤醒AQS队列中的第一个节点(虚拟头节点的下一个)
    long savedState = fullyRelease(node);
    int interruptMode = 0;
    
    // isOnSyncQueue:检测当前的节点是不是在AQS队列中、true(在AQS队列中)/false(不在AQS队列中)
    while (!isOnSyncQueue(node)) {
        // 节点不在AQS直接挂起当前线程
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

2.1 addConditionWaiter

  • 删除 Condition队列 所有标识非 CONDITION(-2) 的节点

  • 将该线程封装成 Node 节点放到 Condition 队列中

private Node addConditionWaiter() {
    // 引用指向尾节点
    Node t = lastWaiter;
    // 如果当前的尾节点不等于null && 尾节点的标识不等于CONDITION(-2)
    // 证明我们当前的尾节点是有问题的
    // 因为你只要在Condition队列中,只有CONDITION(-2)是有效的
    if (t != null && t.waitStatus != Node.CONDITION()) {
        // 删除非CONDITION(-2)的节点
        unlinkCancelledWaiters();
        // 最后重新赋值一下
        t = lastWaiter;
    }
    // 将当前线程封装成Node节点,标识为CONDITION(-2)
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // 如果为null,说明Condition队列为空,头尾指针都指向当前节点即可
    if (t == null)
        firstWaiter = node;
    else
        // 将最后的指向当前节点
        t.nextWaiter = node;
	// 尾指针指向当前节点
    lastWaiter = node;
    // 完成插入并返回
    return node;
}

// 遍历当前的Condition队列,删除掉那些标识不为CONDITION(-2)的节点
// 这段代码逻辑有点绕,不熟悉链表的同学建议可以直接不看了,记住其功能就可以了
// 关键是用三个引用来删除链表,有兴趣的同学可以自己画一下流程
private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        if (t.waitStatus != Node.CONDITION) {
            t.nextWaiter = null;
            if (trail == null)
                firstWaiter = next;
            else
                trail.nextWaiter = next;
            if (next == null)
                lastWaiter = trail;
        }
        else
            trail = t;
        t = next;
    }
}

2.2 fullyRelease

  • 判断当前的线程是否是持有锁的线程,如果不是则抛出异常
  • 如果当前线程是持有锁的线程,则一次性释放掉所有的锁资源(可重入一次性释放)并将持有锁线程置为 null
  • 如果上述操作出现异常,则将当前节点置为报废节点(CANCELLED),后续进行清除
final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        // 拿到当前的state
        int savedState = getState();
        // 释放当前的锁资源并唤醒AQS队列中的第一个节点
        if (release(savedState)) {
            // 没有失败,直接返回即可
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        // 失败了
        if (failed)
            // 这个节点报废了,置为1,后续直接清除掉
            node.waitStatus = Node.CANCELLED;
    }
}

// 这个方法我们上篇文章中讲过
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        // 如果能够放弃锁
        Node h = head;
        // 直接唤醒AQS队列里面第一个节点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

// Step1:检测当前线程是否是占用锁的线程,不是则抛出异常
// Step2:如果是占用锁的线程,将state置为0并将占用锁的线程置为null
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;
}

2.3 isOnSyncQueue

  • 检测当前的节点是不是在AQS队列中
final boolean isOnSyncQueue(Node node) {
    // 如果这个节点是CONDITION或者前继节点为null,那肯定是Condition队列
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    // 如果他的next指针不为空,证明这哥们一定在AQS中(因为Condition队列是用nextWaiter连接的)
    if (node.next != null) 
        return true;
    // 暴力查询
    return findNodeFromTail(node);
}

// 朴实无华在AQS中遍历寻找
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

2.4 问题考查

我们在上面可以看到这一段代码:

// isOnSyncQueue:检测当前的节点是不是在AQS队列中
// true:在AQS队列中
// false:不在AQS队列中
while (!isOnSyncQueue(node)) {
    // 节点不在AQS直接挂起当前线程
    LockSupport.park(this);
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0){
         break;
    }
}

这时候我们可能会有一个疑问,我们上面明明已经把当前线程给封装成 Node 放到 Condition队列 里面了,这里为什么还要判断其有没有在 AQS 队列中呢?

这里考虑到另外一个原因,因为我们在 封装成 Node 放到 Condition队列 里面 到 LockSupport.park(this) 这个外围的判断,这段时间有可能我们当前的线程被别的线程执行 signal 方法直接唤醒了,这样我们当前节点已经不会在 Condition队列 中了。

那么我们这里挂起之后该线程已经停止了,我们去分析 signal 唤醒方法

3、signal

  • 判断其是不是持有锁的线程,如果不是抛出异常
  • 将节点从 Condition队列 中删除掉并且放入到 AQS 队列中,等待唤醒
public final void signal() {
    // 当前线程是不是持有锁的线程,不是则抛出异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 设置一个引用
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

3.1 isHeldExclusively

  • 当前线程是不是持有锁的线程
protected final boolean isHeldExclusively() {
    return getExclusiveOwnerThread() == Thread.currentThread();
}

3.2 doSignal

  • 把头节点直接删除掉并且将其状态修改为0,放入到 AQS 队列中
  • 如果当前头节点修改标识失败的话,则去修改 Condition队列 中的下一个节点
  • 如果放入到 AQS 队列中的该节点的前继节点无效,则需要立即唤醒该节点,去清除无效的节点
private void doSignal(Node first) {
    do {
        // 如果这个条件可以成立的话,说明当前的Condition队列只有一个数据
        // 直接置空,唤醒即可
        if ( (firstWaiter = first.nextWaiter) == null){
            lastWaiter = null;
        }
        // 如果有多个的话,把第一个头节点给删除掉
        first.nextWaiter = null;
        // 这里如果返回true的话,则退出循环
        // 如果当前节点修改标识失败之后,需要执行后面的`first = firstWaiter`,相当于唤醒后面的节点
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

// 入参:node(Condition队列中的第一个节点)
final boolean transferForSignal(Node node()) {
    // 尝试将当前的标识从CONDITION(-2)修改为0,为放入AQS队列做准备
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    // 将该节点放入到AQS队列中,这里的p是前继节点
    Node p = enq(node);
    // 拿到当前的标识
    int ws = p.waitStatus;
    
    // 这一段if语句主要是做了兼容处理
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        // 如果这里可以进来,那么只有两个情况
        // ws > 0:证明当前是无效的节点,那么我排在后面的节点可能永远都不会唤醒,那么我不行呀,我得立即唤醒该节点
        // 		   唤醒之后,执行我们的acquireQueued.shouldParkAfterFailedAcquire方法,清除所有的无效节点并挂起
        // CAS失败:如果前面节点正常,但是我们CAS将其修改为SIGNAL失败了,说明前继节点有问题,和上面类似,需要重新唤醒该节点
        LockSupport.unpark(node.thread);
    return true;
}

4、await-唤醒后的操作

  • 唤醒之后会判断唤醒的方式,这里不需要纠结
  • 确保该节点在 AQS 队列中,取出 AQS 队列中的第一个节点获取锁资源,如果不是第一个节点则挂起
public final void await() throws InterruptedException {
    // 判断当前线程是不是处于中断,如果是中断,则抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 干掉所有标识非CONDITION(-2)的节点并将该线程封装成Node节点放到Condition队列中
    Node node = addConditionWaiter();
    // 释放当前的锁资源并唤醒AQS队列中的第一个节点(虚拟头节点的下一个)
    long savedState = fullyRelease(node);
    int interruptMode = 0;
    
    // isOnSyncQueue:检测当前的节点是不是在AQS队列中、true(在AQS队列中)/false(不在AQS队列中)
    while (!isOnSyncQueue(node)) {
        // 节点不在AQS直接挂起当前线程
        LockSupport.park(this);
        
        // 如果线程执行到这,说明现在被唤醒了。
        // 线程可以被signal唤醒。(如果是signal唤醒,可以确认线程已经在AQS队列中)
        // 线程可以被interrupt唤醒,线程被唤醒后,没有在AQS队列中。
        // 如果线程先被signal唤醒,然后线程中断了。。。。(做一些额外处理)
        // checkInterruptWhileWaiting可以确认当前中如何唤醒的。
        // 返回的值,有三种
        // 0:正常signal唤醒,没别的事(不知道Node是否在AQS队列)
        // THROW_IE(-1):中断唤醒,并且可以确保在AQS队列
        // REINTERRUPT(1):signal唤醒,但是线程被中断了,并且可以确保在AQS队列
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    
    // 你就当上面的最终结果,就是唤醒后退出循环执行后续的唤醒操作即可
    // 如果确保在AQS中的话,将AQS中的第一个节点获取锁资源,如果不是第一个节点的话,则会陷入挂起状态
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    // 如果当前节点还有nextWaiter的话,需要删除
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

4.1 checkInterruptWhileWaiting

private int checkInterruptWhileWaiting(Node node) {
    // Thread.interrupted():这个方法很经典,上篇我们讲过,获取该线程的中断状态并清除
    return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;
}

// 
final boolean transferAfterCancelledWait(Node node) {
    // CAS将当前的状态修改为0
    // 如果可以修改成功,说明这个节点是被中断唤醒的,不是正常唤醒的
    // 既然不是正常唤醒的,那么就得放到AQS队列中
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        enq(node);
        return true;
    }
    // 如果这个节点不在AQS队列中,则执行Thread.yield()
    // 这里也是一个小细节,我们前面提到会将这个节点放入到AQS队列中,但是有可能这个哥们还没在AQS队列中
    // 可能由于CPU的一些原因,总之做了一个保障
    // 如果没在里面,则让线程停一停,等一等
    while (!isOnSyncQueue(node))
        Thread.yield();
    // signal唤醒的,最终返回false
    return false;
}

四、流程图

五、写在最后

鲁迅先生曾说:独行难,众行易,和志同道合的人一起进步。彼此毫无保留的分享经验,才是对抗互联网寒冬的最佳选择。

其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。

如果你也对 后端架构和中间件源码 有兴趣,欢迎添加博主微信:hls1793929520,一起学习,一起成长

我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,喜欢后端架构和中间件源码。

我们下期再见。

我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。

往期文章推荐:

  • 《吊打面试官系列》从根上剖析ReentrantLock的来龙去脉
  • 《吊打面试官系列》从源码全面解析 ThreadLocal 关键字的来龙去脉
  • 《吊打面试官系列》从源码全面解析 synchronized 关键字的来龙去脉
  • 《吊打面试官系列》阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试

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

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

相关文章

【数据结构】七大排序之快速排序详解(挖坑法快排,非递归快排,二路快排,三路快排)

目录 1.快速排序核心思路 2.挖坑法快速排序(递归) 2.1步骤 2.2代码(详细注释) 3.非递归快排(用栈实现快速排序) 3.1思路 3.2代码 4.二路快排 4.1思路 4.2代码 5.三路快排 5.1思路 5.2代码 1.快速…

大白话chatGPT及其原理之快速理解篇

大白话chatGPT及其原理之快速理解篇 从GPT名字理解chatGPTchatGPT三步曲 声明:本文为原创,未经同意请勿转载,感谢配合😄 chatGPT今年年初的时候是非常火爆的,现在也有很多相关的应用和插件。当然现在也有很多新的技术出…

老宋 带你五分钟搞懂vue

Vue 1.1 什么是框架 任何编程语言在最初的时候都是没有框架的,后来随着在实际开发过程中不断总结『经验』,积累『最佳实践』,慢慢的人们发现很多『特定场景』下的『特定问题』总是可以『套用固定解决方案』。于是有人把成熟的『固定解决方案…

袋鼠云春季生长大会圆满落幕,带来数实融合下的新产品、新方案、新实践

4月20日,以“数实融合,韧性生长”为主题的袋鼠云春季生长大会圆满落幕。 在春季生长大会中,袋鼠云带来了数实融合趋势下的最新行业沉淀、最佳实践经验和行业前瞻性的产品发布。从大数据基础软件“数栈”、到低代码数字孪生世界“易知微”&…

离散数学-考纲版-01-命题逻辑

文章目录 1. 命题逻辑的等值演算与推理演算参考1.1 命题1.2 常用联结词1.3 命题公式命题公式的分类-重言式-矛盾式-可满足式等价关系式-逻辑等价 logically equivalent 1.4 命题的等值演算与推理基本等价式逻辑蕴涵重言式 logically implication重言蕴涵推到归结法 1.5 命题公式…

log4j2日志简单使用

log4j2日志使用 1、log4j2介绍 Apache Log4j2是对Log4j的升级版, log4j2借鉴了logback的一些优秀的设计,并且修复了一些问题,因此带来了一些重大的提升,主要有: 1、异常处理:在logback中,Appe…

Makefile通用模板

工程目录 假如我们有以下目录结构&#xff1a; . ├── inc │ ├── add.h │ └── sub.h ├── main.c └── src├── add.c└── sub.c文件中的内容如下&#xff1a; //main.c #include <stdio.h> #include "add.h" #include "sub.h&q…

Mysql 学习(六)Mysql的数据目录

数据库中数据的存放 Mysql中 InnoDB 和 MyISAM 这样的存储引擎都是把数据存储到磁盘上的&#xff0c;而我们把这种存放到磁盘上的东西叫做文件系统&#xff0c;当我们想读取对应数据的时候&#xff0c;就会把数据从文件系统上加载&#xff0c;并且处理返回给我们&#xff0c;当…

每日学术速递4.19

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.CV 1.Visual Instruction Tuning 标题&#xff1a;可视化指令调优 作者&#xff1a;Haotian Liu, Chunyuan Li, Qingyang Wu, Yong Jae Lee 文章链接&#xff1a;https://arxiv.org/ab…

Midjourney:一步一步教你如何使用 AI 绘画 MJ

一步一步如何使用 Midjourney 教程&#xff1a;教学怎么用 MJ&#xff1f; 一、Midjourney&#xff08;MJ&#xff09;是什么&#xff1f; Midjourney是一款使用文字描述来生成高质量图像的AI绘画工具。这篇文章主要介绍了Midjourney及其用途&#xff0c;并针对Midjourney的使…

python 定时任务执行命令行

1.使用场景&#xff1a; 定时执行jmeter脚本&#xff0c;通过python定时器隔一段时间执行命令行命令。 2.库&#xff1a; os、datetime、threading &#xff08;1&#xff09;利用threading.Timer()定时器实现定时任务 Timer方法说明Timer(interval, function, argsNone, k…

如何利用python实现TURF分析?

1.TRUF分析简介 TURF分析(Total Unduplicated Reach and Frequency)是累计净到达率和频次分析的简称。最初被应用于媒介研究领域。典型应用场景是&#xff0c;在既定条件下&#xff0c;例如预算等资源限制或就当前实施的媒体组合投放计划&#xff0c;哪些渠道组合能让广告投放…

【三十天精通Vue 3】第十二天 Vue 3 过滤器详解

✅创作者&#xff1a;陈书予 &#x1f389;个人主页&#xff1a;陈书予的个人主页 &#x1f341;陈书予的个人社区&#xff0c;欢迎你的加入: 陈书予的社区 &#x1f31f;专栏地址: 三十天精通 Vue 3 文章目录 引言一、Vue 3 过滤器概述1.1 过滤器的简介1.2 过滤器的作用1.3 过…

WEB通用漏洞水平垂直越权详解业务逻辑访问控制脆弱验证

目录 一、知识点概述 <分类> <原理简述> 二、水平越权示例——检测数据比对弱 <越权演示> <如何防护> 三、垂直越权示例——权限操作无验证 <越权演示> <漏洞成因> 四、访问控制示例——代码未引用验证 <越权演示> 五、脆…

如何才能写出一个符合预期的正则?

如何才能写出一个符合预期的正则&#xff1f; 正则表达式入门示例讲解1、java里正则表达式replaceAll连续的字符正则测试题主问题讲解 2、开发者遇到金额的校验正则描述正则测试 3、java正则表达式匹配字符串正则描述正则测试 4、关于#正则表达式#的问题&#xff0c;如何解决&a…

0基础自学软件测试 用这个方法 99%的人都成功了

对于大多数0基础的小白而言&#xff0c;刚开始学软件测试&#xff0c;肯定会遇到各种各样的难题&#xff0c;有时候问题多了&#xff0c;扛不住了&#xff0c;导致最后无法坚持&#xff0c;或者学的很杂&#xff0c;学而不精。 那么有哪些比较有效的方法和技巧&#xff0c;可以…

系统分析师之数据库系统(七)

目录 一、数据库概念 1.1 数据库管理系统DBMS 1.2 数据库系统DBS 二、数据库设计 2.1 数据库设计过程 2.2 E-R模型 2.3 关系代数 2.4 规范化理论 2.4.1 价值与用途 2.4.2 函数依赖 2.4.3 键 2.4.4 范式 2.4.5 无损分解 三、并发控制 3.1 基本概念 3.2 问题示例…

SCA技术进阶系列(二):代码同源检测技术在供应链安全治理中的应用

一、直击痛点&#xff1a;为什么需要同源检测 随着“数字中国”建设的不断提速&#xff0c;企业在数字化转型的创新实践中不断加大对开源技术的应用&#xff0c;引入开源组件完成应用需求开发已经成为了大多数研发工程师开发软件代码的主要手段。随之而来的一个痛点问题是&…

开启数字化之旅:VR全景视频带你进入真实而神奇的世界

引言&#xff1a;随着科技的不断发展&#xff0c;虚拟现实技术正在成为越来越多人所追捧和体验的技术。而VR全景视频作为虚拟现实技术的一种重要应用&#xff0c;也得到了越来越多人的关注。那么&#xff0c;VR全景视频到底是什么&#xff1f;它的优势和特点是什么&#xff1f;…

OpenGL入门教程之 变化颜色的三角形

一、 知识点 &#xff08;1&#xff09;着色器 着色器(Shader)是运行在GPU上的小程序。这些小程序为图形渲染管线的某个特定部分而运行。从基本意义上来说&#xff0c;着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序&#xff0c;因为它们之间不能相互通…