JUC并发编程与源码分析笔记13-AbstractQueuedSynchronizer之AQS

news2024/9/29 3:25:37

前置知识

  • 公平锁和非公平锁
  • 可重入锁
  • 自旋思想
  • LockSupport
  • 数据结构之双向链表
  • 设计模式之模板设计模式

AQS入门级别理论知识

是什么

AbstractQueuedSynchronizer:抽象的队列同步器。
用来实现锁或其他同步器组件的公共基础部分的抽象实现,是重量级基础框架及整个JUC体系的基石,主要用于解决锁分给谁的问题。
为了实现阻塞锁和相关的同步器提供的一个框架,依赖于一个先进先出的等待队列。依靠单个原子int值表示状态,通过占用和释放方法改变状态值。
在这里插入图片描述

AQS为什么是JUC内容中最重要的基石

和AQS有关的

在这里插入图片描述
ReentrantLock
在这里插入图片描述
CountDownLatch
在这里插入图片描述
ReentrantReadWriteLock
在这里插入图片描述
Semaphore
在这里插入图片描述

进一步理解锁和同步器的关系

锁:面向锁的使用者,定义了程序员和锁交互的使用层API,隐藏了实现细节,只需要调用即可。
同步器:面向锁的实现者,DougLee提出了统一规范并简化锁的实现,将其抽象出来屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等,是一切锁和同步组件实现的公共基础部分。

能干嘛

抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
加锁会导致阻塞,进而需要排队,就需要有队列。
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS同步队列的抽象表现。它将要请求共享资源的线程及自身的等待状态封装成队列的结点对象(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。
AQS使用一个volatile的int类型的成员变量state表示同步状态,通过内置FIFO队列完成资源获取的排队工作,将每条要去抢占资源的线程封装成一个Node结点来实现锁的分配,通过CAS完成对State值的修改。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
    static final class Node {
        volatile int waitStatus;
        volatile java.util.concurrent.locks.AbstractQueuedSynchronizer.Node prev;
        volatile java.util.concurrent.locks.AbstractQueuedSynchronizer.Node next;
    }
    private transient volatile java.util.concurrent.locks.AbstractQueuedSynchronizer.Node head;
    
    private transient volatile java.util.concurrent.locks.AbstractQueuedSynchronizer.Node tail;

    private volatile int state;

    protected final int getState() {
        return state;
    }

    protected final void setState(int newState) {
        state = newState;
    }
}

小总结

在这里插入图片描述

AQS源码分析前置知识储备

在这里插入图片描述

AQS自身

AQS的int变量

AQS的同步状态state成员变量

/**
 * The synchronization state.
 * state=0:可以操作
 * state>0:有线程占用
 */
private volatile int state;

AQS的CLH队列

CLH队列,是一个双向队列。尾部入队,头部出队。

小总结

阻塞→排队→队列=state变量+CLH双端队列。

内部类Node(Node类是AQS的内部类)

Node的int变量

/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED =  1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL    = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
 * waitStatus value to indicate the next acquireShared should
 * unconditionally propagate
 */
static final int PROPAGATE = -3;
/**
 * Status field, taking on only the values:
 *   SIGNAL:     The successor of this node is (or will soon be)
 *               blocked (via park), so the current node must
 *               unpark its successor when it releases or
 *               cancels. To avoid races, acquire methods must
 *               first indicate they need a signal,
 *               then retry the atomic acquire, and then,
 *               on failure, block.
 *   CANCELLED:  This node is cancelled due to timeout or interrupt.
 *               Nodes never leave this state. In particular,
 *               a thread with cancelled node never again blocks.
 *   CONDITION:  This node is currently on a condition queue.
 *               It will not be used as a sync queue node
 *               until transferred, at which time the status
 *               will be set to 0. (Use of this value here has
 *               nothing to do with the other uses of the
 *               field, but simplifies mechanics.)
 *   PROPAGATE:  A releaseShared should be propagated to other
 *               nodes. This is set (for head node only) in
 *               doReleaseShared to ensure propagation
 *               continues, even if other operations have
 *               since intervened.
 *   0:          None of the above
 *
 * The values are arranged numerically to simplify use.
 * Non-negative values mean that a node doesn't need to
 * signal. So, most code doesn't need to check for particular
 * values, just for sign.
 *
 * The field is initialized to 0 for normal sync nodes, and
 * CONDITION for condition nodes.  It is modified using CAS
 * (or when possible, unconditional volatile writes).
 */
volatile int waitStatus;

表示等待区其他线程的等待昨天,队列中每个排队的个体就是一个Node。

Node此类的讲解

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

AQS源码深度讲解和分析

Lock接口的实现类,基本都是通过聚合了一个队列同步器的子类完成线程访问控制的。

ReentrantLock的原理

Lock.java是一个接口,在创建Lock对象的时候,这里拿Lock的实现类ReentrantLock.java为例。
ReentrantLock创建对象的时候,可以传true/false,也可以不传(等价于false),从而创建出来公平锁和非公平锁。

从最简单的lock()方法开始看看公平和非公平

在执行lock()的时候,公平锁和非公平锁执行逻辑会不一样,顺着lock()方法点进去。
lock()sync.lock()acquire()
公平锁:

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

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        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;
}

非公平锁:

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

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;
    }
    return false;
}

观察公平锁和非公平锁的tryAcquire()方法,公平锁多了一个语句:!hasQueuedPredecessors(),用来判断当前队列是否有前驱。
公平锁:先来先到,线程在获取锁的时候,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列。
非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象,也就是队列的第一个排队线程苏醒后,不一定是排头的这个线程获取的锁,它还是需要参加竞争锁(如果线程竞争的情况),后来的线程就插队夺锁了。

以非公平锁ReentrantLock为例作为突破,了解lock()方法

以非公平锁作为案例突破,解读源码。

import java.util.concurrent.locks.ReentrantLock;

public class AQSDemo {
    public static void main(String[] args) {
        // A、B、C去银行办理业务,只有一个窗口
        ReentrantLock reentrantLock = new ReentrantLock();// 非公平锁
        // A先到,此时窗口空闲,A获得优先办理的机会,办理业务
        new Thread(() -> {
            try {
                reentrantLock.lock();
                System.out.println("A is running");
                try {
                    // A是一个耗时的任务,需要长期占用窗口
                    Thread.sleep(5 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                reentrantLock.unlock();
            }
        }, "A").start();
        // B到了,此时A正在办理业务,只能等待窗口被释放,于是进入AQS等待队列,尝试去抢占窗口
        new Thread(() -> {
            try {
                reentrantLock.lock();
                System.out.println("B is running");
            } finally {
                reentrantLock.unlock();
            }
        }, "B").start();
        // C到了,此时A正在办理业务,只能等待窗口被释放,于是进入AQS等待队列,尝试去抢占窗口,前面是B顾客
        new Thread(() -> {
            try {
                reentrantLock.lock();
                System.out.println("C is running");
            } finally {
                reentrantLock.unlock();
            }
        }, "C").start();
    }
}

lock()

lock()方法是Lock.java接口里的,查看实现类,找到ReentrantLock.javalock()方法,再看实现类,有FairSyncNonFairSync两个实现类。

static final class NonfairSync extends Sync {
	final void lock() {
	    if (compareAndSetState(0, 1))// 第一个线程抢占
	        setExclusiveOwnerThread(Thread.currentThread());// 设置排他锁拥有者为当前线程
	    else// 第二个线程及后续线程抢占
	        acquire(1);
	}
}

static final class FairSync extends Sync {
    final void lock() {
        acquire(1);
    }
}

acquire()

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

tryAcquire(arg)

在父类里只提供一个模板,具体实现交由子类来实现。
AbstractQueuedSynchronizer.java

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

FairSync.java

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    	// hasQueuedPredecessors()用来判断队列前面是否有元素
        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;
}

NonFairSync.java会调用nonfairTryAcquire()方法。

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {// 当前可以抢占
    	// 尝试CAS
        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;
    }
    return false;
}

对于tryAcquire()方法,如果抢锁成功了,继续执行下面的流程,如果没有成功,就会进入队列进行排队,也就来到了addWaiter()acquireQueued()方法。

addWaiter(Node.EXCLUSIVE)

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;// 初始的时候,tail和head都指向null
    if (pred != null) {// 这一段if和enq()里的else的含义是一样的:双向链表的尾插法
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 加入到排队的等待队列
    enq(node);// 第一次进这个方法的时候,会直接走enq()方法
    return node;
}
private Node enq(final Node node) {
	// 这里可以理解成一个双向链表的插入操作
    for (;;) {
        Node t = tail;
        // 第一次进来
        if (t == null) { // Must initialize
        	// 创建一个结点,并设置为头结点
            if (compareAndSetHead(new Node()))
                tail = head;// 让尾结点也指向这个结点,此时头结点=尾结点,这个结点叫虚拟结点/哨兵结点,用于占位,注意这里的node是new出来的,不是传进来的
        } else {
            node.prev = t;// 传进来的结点的prev指向t
            if (compareAndSetTail(t, node)) {// 尝试将node标记为尾结点
                t.next = node;// 建立next指向
                return t;
            }
        }
    }
}

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
        	// 获取前置结点,第一次进来的时候,前置结点是虚拟结点
            final Node p = node.predecessor();
            // 尝试抢占(队列头的结点和队列外的结点抢占)
            if (p == head && tryAcquire(arg)) {
            	// 更新头结点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 第一次执行shouldParkAfterFailedAcquire()结果为false,第二次执行shouldParkAfterFailedAcquire()结果为true,此时会继续走parkAndCheckInterrupt()
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
    	// 如果排队失败或者不想排队了,执行取消操作
        if (failed)
            cancelAcquire(node);
    }
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	// 获取前置结点的waitStatus
    int ws = pred.waitStatus;
    // 如果是SIGNAL,等待被占用资源释放直接返回true,准备调用parkAndCheckInterrupt()方法
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    // waitStatus > 0表示是CANCELED状态
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        // 循环判断当前结点的前置结点是否也为CANCELED,忽略该状态结点,重新连接队列
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        // 将前置结点的waitStatus标记为-1,这里意味着有一个结点已经阻塞了,前置结点有唤醒下一个结点的责任
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
private final boolean parkAndCheckInterrupt() {
	// 线程挂起,程序不会继续向下执行
    LockSupport.park(this);
    // 根据park方法API描述,程序在这三种情况下,会继续向下执行
    // 1.被unpark
    // 2.被中断(interrupt)
    // 3.其他不合逻辑的返回才会继续向下执行
    // 上述3种情况,会执行到这里,返回当前线程中断状态并清空中断状态
    // 由于被中断了,该方法返回true
    return Thread.interrupted();
}

unlock()

sync.release(1)tryRelease(arg)unparkSuccessor()
在分析unlock()的时候,还要回看前面的parkAndCheckInterrupt()方法。

public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;// 重新计算state的值,可重入锁,计算可重入次数
    // 当前线程和持有锁的线程不是一个,一般不会出现这个情况
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);// 把排他锁持有者设置为null
    }
    setState(c);// 更新state的值
    return free;
}
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    // 第一次进来,s为等待队列的第一个结点,且非null
    Node s = node.next;
    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)
    	// 执行unpack()操作,唤醒s线程
        LockSupport.unpark(s.thread);
}

看一下异常情况,也就是执行cancelAcquire()方法的情况。
这里要结合双向链表的删除,考虑删除最后一个结点的情况和删除中间结点的情况。

private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
        return;
    // 清空结点里的线程
    node.thread = null;
    // Skip cancelled predecessors
    // 找到前一个结点
    Node pred = node.prev;
    // CANCELED为1,大于0,那么继续向前找,直到碰到一个不是CANCELED的结点,一边找一边更新当前结点的前驱
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    // 找到后一个结点
    Node predNext = pred.next;
    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    // 标记当前结点是CANCELED
    node.waitStatus = Node.CANCELLED;
    // If we are the tail, remove ourselves.
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            unparkSuccessor(node);
        }
        node.next = node; // help GC
    }
}

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

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

相关文章

Hive知识回顾2

一、分桶表 1.1分桶表的概念 分桶表也叫做桶表&#xff0c;源自建表语法中bucket单词。是一种用于优化查询而设计的表类型。该功能可以让数据分解为若干个部分易于管理。 在分桶时&#xff0c;我们要指定根据哪个字段将数据分为几桶&#xff08;几个部分&#xff09;。默认规则…

人脸识别中的深度学习

深度学习在人脸识别中的应用 人脸识别的过程包括&#xff1a; 人脸检测人脸对齐特征提取&#xff08;在数学上&#xff0c;实质上是&#xff1a;空间变换&#xff09;特征度量 其中&#xff0c;特征提取与度量&#xff0c;是人脸识别问题中的关键问题&#xff0c;也是相关研究…

使用 ChatGPT 辅助学习——为自己找一个老师

我们每个人都有许多标签&#xff0c;例如高中生、成绩中等、文科&#xff0c;根据这些标签我和其他拥有相同标签的人分配了相同的教程、班级和老师&#xff0c;这可以带来效率上的提升&#xff0c;因为同一份教程、老师就可以服务几十上百人&#xff0c;而无须为每个人定制&…

软件测试工程师的核心竞争力究竟是什么?

对于测试员而言&#xff0c;了解自己岗位的核心竞争力是非常重要的。在职业初期&#xff0c;许多人认为掌握代码才是软件测试的核心竞争力&#xff0c;但是随着经验的增加&#xff0c;我们会发现真正的核心竞争力是由多个方面组成的。 首先&#xff0c;测试人员需要具备良好的测…

BERT 的面试题

BERT 的简介 1、BERT 是什么&#xff1f;它是用来做什么的&#xff1f; BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;是由Google开发的自然语言处理模型&#xff0c;是一种预训练模型&#xff0c;可以用于多种自然语言处理任务&…

【python自动化脚本—实现excel参数化循环调用判定结果】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、需求背景二、我要做什么三、已有接口四、脚本实现五、实现效果图 前言 为提升自己的python能力&#xff0c;记录在工作中实现的自动化脚本&#xff0c;温故…

51单片机(七)定时器

❤️ 专栏简介&#xff1a;本专栏记录了从零学习单片机的过程&#xff0c;其中包括51单片机和STM32单片机两部分&#xff1b;建议先学习51单片机&#xff0c;其是STM32等高级单片机的基础&#xff1b;这样再学习STM32时才能融会贯通。 ☀️ 专栏适用人群 &#xff1a;适用于想要…

写文章的软件-一款写作文章的软件

写文章的软件 写文章的软件是一种工具&#xff0c;可以帮助用户轻松快速地创作高质量的文章。该软件一般包括以下几个主要功能&#xff1a; 写作界面&#xff1a;提供简洁、美观的写作界面&#xff0c;让用户专注于文章创作&#xff0c;同时可以进行排版、字体、颜色等调整。 …

力扣707. 设计链表

707. 设计链表 - 力扣&#xff08;Leetcode&#xff09; class MyLinkedList { public:// 定义链表节点结构体struct LinkedNode {int val;LinkedNode* next;LinkedNode(int val):val(val), next(nullptr){}};// 初始化链表MyLinkedList() {_dummyHead new LinkedNode(0); // …

BasicVSR++代码解读(分模块介绍-前向传播函数)

一、前向传播函数def forward(self, lqs): 输入&#xff1a;低质量序列L,shape为**(n, t, c, h, w)。 输出&#xff1a;HR序列,shape为(n, t, c, 4h, 4w)**。 &#xff08;1&#xff09;读取输入数据的维度大小     这里的n,t,c,h,w分别表示&#xff1a;一个bitch的图片数…

数据结构之双向链表,实现双向链表的增删改查

目录 一、双向链表的定义 1.双向链表节点的定义 2.双向链表的初始化 二、双向链表的函数接口实现 1.双链表的尾插 2.双向链表的尾删 3.双向链表的头插 4.双向链表的头删 6.双向链表在pos前面插入 7.双向链表删除pos位置的节点 8.双向链表的销毁 总结 一、双向链表的定义 双向链…

PyQt5桌面应用开发(11):摸鱼也要讲基本法,两个字,16

本文目录 PyQt5桌面应用系列鼠标不要钱&#xff0c;手腕还不要钱吗&#xff1f;PyQt5源程序python文件资源定义界面定义文件 技术要素资源文件StyleSheetsQMainWindow设置窗体几何 结论 PyQt5桌面应用系列 PyQt5桌面应用开发&#xff08;1&#xff09;&#xff1a;需求分析 PyQ…

EC6110-Hi3798MV310-当贝纯净桌面-卡刷强刷固件包

EC6110-Hi3798MV310-当贝纯净桌面-卡刷强刷固件包-内有教程及短接点 特点&#xff1a; 1、适用于对应型号的电视盒子刷机&#xff1b; 2、开放原厂固件屏蔽的市场安装和u盘安装apk&#xff1b; 3、修改dns&#xff0c;三网通用&#xff1b; 4、大量精简内置的没用的软件&a…

【信息安全案例】——网络攻击分析(学习笔记)

&#x1f4d6; 前言&#xff1a;随着信息技术的发展&#xff0c;网络空间的斗争可谓是兵家必争之地&#xff0c;网络攻击的事件层出不穷。道高一尺魔高一丈&#xff0c;了解常见的网络攻击类型有利于我们日后工作生活的安全稳定。 目录 &#x1f552; 1. 网络攻击威胁&#x1f…

CM211-2-YS-Hi3798MV300/MV310-当贝纯净桌面线刷固件包(可救砖)

CM211-2-YS-Hi3798MV300&#xff0f;MV310-当贝纯净桌面线刷固件包&#xff08;可救砖&#xff09;-内有教程 特点&#xff1a; 1、适用于对应型号的电视盒子刷机&#xff1b; 2、开放原厂固件屏蔽的市场安装和u盘安装apk&#xff1b; 3、修改dns&#xff0c;三网通用&…

05- 算法解读 R-CNN (目标检测)

要点&#xff1a; R-CNN可以说是利用深度学习进行目标检测的开山之作。 一 R-CNN算法 R-CNN可以说是利用深度学习进行目标检测的开山之作。作者Ross Girshick多次 在PASCAL VOC的目标检测竞赛中折桂&#xff0c;曾在2010年带领团队获得终身成就奖。 RCNN算法流程可分为4个步…

ACM输入输出

写在前面 主要记录一下ACM输入输出的写法&#xff1b; 一、输入数值 1. 给定N的定长多行输入 题目&#xff1a;https://ac.nowcoder.com/acm/contest/5657/B&#xff1b; 代码&#xff1a; #include <iostream> #include <stdio.h> using namespace std;int m…

JavaWeb《后端内容:1. Tomcat - Servlet - Thymeleaf》

目录 1. 基础概念 1.1 BS架构和CS架构 1.2 Tomcat图解 2.TomCat 2.1 IDEA配置web项目和tomcat 2.2 idea启动TomCat因为端口号失败的问题 3.Servlet使用流程 3.1 Servlet简单图解 3.2 Servlet导入依赖 3.3 编写Servlet和add.html 3.4 试着使用Jdbc和Dao层连接水果库存…

如何是Jetpack Compose构建漂亮的应用程序

如何是Jetpack Compose构建漂亮的应用程序 Jetpack compose 是在 Android 上构建 UI 的未来。 如果您完全不熟悉 android 并且不知道 Jetpack Compose 是什么——它基本上是一种构建本机用户界面的新方法。 Jetpack compose官方站点 https://developer.android.com/jetpack/co…