AQS源码学习

一、park/unpark阻塞唤醒线程

LockSupport是JDK中用来实现线程阻塞和唤醒的工具。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。JDK并发包下的锁和其他同步工具的底层实现中大量使用了LockSupport进行线程的阻塞和唤醒,掌握它的用法和原理可以让我们更好的理解锁和其它同步工具的底层实现。

public class LockSupportTest {
    public static void main(String[] args) {
        Thread parkThread = new Thread(new ParkThread());
        parkThread.start();
        System.out.println("唤醒parkThread");
        LockSupport.unpark(parkThread);
    }

    static class ParkThread implements Runnable{
        public void run() {
            System.out.println("ParkThread开始执行");
            LockSupport.park();
            System.out.println("ParkThread执行完成");
        }
    }
}

一、LockSupport阻塞和唤醒线程原理

每个线程都有Parker实例

class Parker : public os::PlatformParker {
    private:
    volatile int _counter ;
    ...
    public:
    void park(bool isAbsolute, jlong time);
    void unpark();
    ...
}
class PlatformParker : public CHeapObj<mtInternal> {
    protected:
    pthread_mutex_t _mutex [1] ;
    pthread_cond_t _cond [1] ;
    ...
}

LockSupport就是通过控制变量_counter来对线程阻塞唤醒进行控制的。原理有点类似于信号量机制。
当调用park()方法时,会将_counter置为0,同时判断前值,大于1说明前面被unpark过,则直接退出,否则将使该线程阻塞。
当调用unpark()方法时,会将_counter置为1,同时判断前值,小于1会进行线程唤醒,否则直接退出。
形象的理解,线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用park方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;但是如果没有凭证,就必须阻塞等待凭证可用;而unpark则相反,它会增加一个凭证,但凭证最多只能有1个。
1、为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后调用park因为有凭证消费,故不会阻塞。
2、为什么唤醒两次后阻塞两次会阻塞线程?
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证。 

二、AQS原理

一、什么是AQS

java.util.concurrent包中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这些行为的抽象就是基于AbstractQueuedSynchronizer(简称AQS)实现的,AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器。
JDK中提供的大多数的同步器如Lock, Latch, Barrier等,都是基于AQS框架来实现的。

AQS具备的特性:
1、阻塞等待队列
2、共享/独占
3、公平/非公平
4、可重入
5、允许中断。

AQS内部维护属性volatile int state,state表示资源的可用状态。
State三种访问方式:
1、getState()
2、setState()
3、compareAndSetState()

AQS定义两种资源共享方式

1、Exclusive-独占,只有一个线程能执行,如ReentrantLock
2、Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch

AQS定义两种队列
同步等待队列: 主要用于维护获取锁失败时入队的线程。
条件等待队列: 调用await()的时候会释放锁,然后线程会加入到条件队列,调用signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁。

AQS 定义了5个队列中节点状态:
1. 值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
2. CANCELLED,值为1,表示当前的线程被取消;
3. SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;
4. CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
5. PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;

不同的自定义同步器竞争共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

二、同步等待队列

AQS当中的同步等待队列也称CLH队列,CLH队列是一种基于双向链表数据结构的队列,是FIFO先进先出线程等待队列。

AQS 依赖CLH同步队列来完成同步状态的管理:

当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程。当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。通过signal或signalAll将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)。

三、条件等待队列

AQS中条件队列是使用单向列表保存的,用nextWaiter来连接:
调用await方法阻塞线程;
当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条件队列)

四、Condition接口详解

1. 调用Condition#await方法会释放当前持有的锁,然后阻塞当前线程,同时向Condition队列尾部添加一个节点,所以调用Condition#await方法的时候必须持有锁。
2. 调用Condition#signal方法会将Condition队列的首节点移动到阻塞队列尾部,然后唤醒因调用Condition#await方法而阻塞的线程(唤醒之后这个线程就可以去竞争锁了),所以调用Condition#signal方法的时候必须持有锁,持有锁的线程唤醒被因调用Condition#await方法而阻塞的线程。

三、ReentrantLock源码流程

下面是三个线程启动争抢lock的一个demo

public class ReentrantLockDemo {
    private static int sum = 0;

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(()->{
                lock.lock();
                try {
                    for (int j = 0; j < 10000; j++) {
                        sum++;
                    }
                } finally {
                    lock.unlock();
                }
            });
            thread.start();
        }
        Thread.sleep(2000);
        System.out.println(sum);
    }
}

一、ReentrantLock#lock方法

从构造方法可知,ReentrantLock默认为非公平锁

public ReentrantLock() {
	sync = new NonfairSync();
}

如果compareAndSetState方法是原子化的,如果执行成功则表示获取到锁 

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

 获取到锁成功后则执行方法setExclusiveOwnerThread将lock中的exclusiveOwnerThread属性修改为当前线程,标记被该线程持有。

protected final void setExclusiveOwnerThread(Thread thread) {
	exclusiveOwnerThread = thread;
}

一、ReentrantLock#compareAndSetState

加锁逻辑是通过CAS修改lock本身的state,state为0的时候表示锁为未被持有状态,state大于1的时候表示锁被线程持有。

protected final boolean compareAndSetState(int expect, int update) {
	// See below for intrinsics setup to support this
	return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

二、ReentrantLock#acquire

如果compareAndSetState方法执行失败,那么就会继续执行tryAcquire方法,该方法仍然是尝试获取锁,如果获取失败则将该线程包装为Node节点入队并阻塞。

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

nonfairTryAcquire中当前线程先去获取当前锁state状态,如果为0就再次执行CAS去争抢锁,抢不到就会入队;如果不为0,就判断当前线程跟持有锁的线程是否是同一个线程,如果是就将state加1,那么此时也就再次获取到锁,这里体现了ReentrantLock锁的可重入逻辑。

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;
}
二、ReentranLock#addWaiter

Node node = new Node(Thread.currentThread(), mode);将当前线程包装为一个Node入队,

首次进来的时候队列是null,tail节点也是null,就会执行enq方法,创建一个空节点Node作为队尾,然后再将线程节点加到队尾;如果队列不为null,那么就直接加到当前队尾节点tail节点的后面

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;
	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) { // Must initialize
			if (compareAndSetHead(new Node()))
				tail = head;
		} else {
			node.prev = t;
			if (compareAndSetTail(t, node)) {
				t.next = node;
				return t;
			}
		}
	}
}
三、ReentranLock#acquireQueued 

入队之后开始执行acquireQueued方法,for循环中判断当前节点的前驱节点是头结点的话,就再次执行tryAcquire获取锁,如果获取到了,就将该节点设置为头结点,p.next=null删除前驱节点再垃圾回收。

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;
			}
			if (shouldParkAfterFailedAcquire(p, node) &&
				parkAndCheckInterrupt())
				interrupted = true;
		}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}

如果没有获取到锁则执行到shouldParkAfterFailedAcquire方法中,如果前驱节点的waitStatus为Node.SIGNAL(-1),这个状态上面已经提到,表示该状态节点的后继节点包含的线程需要运行,也就是说node节点后面会通过pred节点执行unpark方法唤醒node节点线程。

如果前驱节点的waitStatus大于0(CANCELLED:1),表示当前线程被取消,无需再争抢线程,需要将这些线程从队列中移除。

如果这两个状态都不是,那么waitStatus就一定是0或-3,这种场景需要将前驱节点waitStatus更新SINGAL(-1),后面通过前驱节点将该线程唤醒。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	int ws = pred.waitStatus;
	if (ws == Node.SIGNAL)
		/*
		 * This node has already set status asking a release
		 * to signal it, so it can safely park.
		 */
		return true;
	if (ws > 0) {
		/*
		 * Predecessor was cancelled. Skip over predecessors and
		 * indicate retry.
		 */
		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.
		 */
		compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	}
	return false;
}

 shouldParkAfterFailedAcquire执行成功后,就执行park方法将该线程阻塞。

private final boolean parkAndCheckInterrupt() {
	LockSupport.park(this);
	return Thread.interrupted();
}
四、ReentranLock#cancelAcquire

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;
	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.
	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
	}
}

三、ReentranLock#unlock

释放锁的过程就两步,将state更新为0,唤醒下个节点线程。

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;
}
一、ReentranLock#tryRelease

将当前state减一,如果减去一之后为0,就讲exclusiveOwnerThread设置为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;
}
二、ReentranLock#unparkSuccessor

如果当前头结点的状态不为0,就执行unparkSuccessor方法。正常情况下,节点状态为-1(SIGNAL),然后需要更新为0,并开始从尾结点循环查找waitStatus小于等于0的最后一个节点,对于节点状态大于0的都是需要取消的,无需获取锁,最终调用LockSupport.unpark(s.thread)唤醒线程,继续获取锁。

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.
	 */
	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)
		LockSupport.unpark(s.thread);
}

四、condition源码流程

public class ProducerConsumerTest {
    private final ReentrantLock lock = new ReentrantLock();

    //非空
    private final Condition notEmpty = lock.newCondition();
    //非满
    private final Condition notFull = lock.newCondition();

    //缓冲区:初始元素都为0
    private final int[] buffer = new int[5];
    //当前元素数量
    private int count = 0;
    
    //生产者
    public void produce() {
        lock.lock();
        try {
            while (count == buffer.length) { // 如果队列满了
                System.out.println("====== 缓冲区已满,生产者正在等待...... ======");
                //等待notFull非满条件
                notFull.await();
            }
            //缓冲区添加1个元素
            buffer[count++] = 1;
            System.out.println("生产了一个项目。缓冲区大小:" + count);
            //唤醒消费者
            notEmpty.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    //消费者
    public void consume() {
        lock.lock();
        try {
            while (count == 0) { // 如果队列空了
                System.out.println("====== 缓冲区是空的,消费者正在等待...... ======");
                //等待notEmpty非空条件
                notEmpty.await();
            }
            //缓冲区消费1个元素
            buffer[--count] = 0;
            System.out.println("消费了一个项目。缓冲区大小:" + count);
            //唤醒生产者
            notFull.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ProducerConsumerTest pc = new ProducerConsumerTest();
        Thread producer = new Thread(() -> {
            try {
                while (true) {
                    pc.produce();
                    Thread.sleep(500); // 模拟生产时间
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread consumer = new Thread(() -> {
            try {
                while (true) {
                    pc.consume();
                    Thread.sleep(1000); // 模拟消费时间
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.start();
        consumer.start();
    }
}

一、condition#await

await中会调用addConditionWaiter方法将节点加入到条件队列,然后fullyRelease方法释放锁,最后将线程给park;

当执行signal方法unpark线程后,线程就会执行accquireQueue方法去重新获取锁。

public final void await() throws InterruptedException {
	if (Thread.interrupted())
		throw new InterruptedException();
	Node node = addConditionWaiter();
	long savedState = fullyRelease(node);
	int interruptMode = 0;
	while (!isOnSyncQueue(node)) {
		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);
}

二、condition#signal

signal方法将条件队列中的中的线程转移到同步队列的末尾,然后unpark该线程。去重新获取锁

private void doSignal(Node first) {
	do {
		if ( (firstWaiter = first.nextWaiter) == null)
			lastWaiter = null;
		first.nextWaiter = null;
	} while (!transferForSignal(first) &&
			 (first = firstWaiter) != null);
}

final boolean transferForSignal(Node node) {
	/*
	 * If cannot change waitStatus, the node has been cancelled.
	 */
	if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
		return false;

	/*
	 * Splice onto queue and try to set waitStatus of predecessor to
	 * indicate that thread is (probably) waiting. If cancelled or
	 * attempt to set waitStatus fails, wake up to resync (in which
	 * case the waitStatus can be transiently and harmlessly wrong).
	 */
	Node p = enq(node);
	int ws = p.waitStatus;
	if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
		LockSupport.unpark(node.thread);
	return true;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

GUI07-学工具栏,懂MVC

MVC模式&#xff0c;是天底下编写GUI程序最为经典、实效的一种软件架构模式。当一个人学完菜单栏、开始学习工具栏时&#xff0c;就是他的一生中&#xff0c;最适合开始认识 MVC 模式的好时机之一。这节将安排您学习&#xff1a; Model-View-Controller 模式如何创建工具栏以及…

C++----类与对象(中篇)

引言 以C语言栈的实现为例&#xff0c;在实际开发中&#xff0c;我们可能会遇到以下两个问题&#xff1a; 1.初始化和销毁管理不当&#xff1a;C语言中的栈实现通常需要手动管理内存&#xff08;如使用malloc和free&#xff09;&#xff0c;这导致初始化和销毁栈时容易出错或…

linux打包qt程序

Linux下Qt程序打包_linuxdeployqt下载-CSDN博客 Linux/Ubuntu arm64下使用linuxdeployqt打包Qt程序_linuxdeployqt arm-CSDN博客 本篇文章的系统环境是 : 虚拟机ubuntu18.04 用下面这个qmake路径 进行编译 在 ~/.bashrc 文件末尾&#xff0c;qmake目录配置到文件末尾 将上图中…

气象与旅游之间的关系,如果借助高精度预测提高旅游的质量

气象与旅游之间存在密切的关系,天气条件直接影响旅游者的出行决策、旅游体验和安全保障。通过高精度气象预测技术,可以有效提升旅游质量,为游客和旅游行业带来显著的优势。 1. 提高游客出行决策效率 个性化天气服务:基于高精度气象预测,旅游平台可以提供个性化的天气预报服…

华为OD --- 靠谱的车

华为OD --- 靠谱的车 题目OJ用例独立实现思路源码 参考实现思路源码实现 题目 OJ用例 测试用例case 独立实现 思路 独立实现的思路比较简单,直接建一个长度为N的数组,然后找出index中不包含4的项数即可 源码 const rl require("readline").createInterface({ …

可视化平台FineReport的安装及简单使用

1. FineReport产品 FineReport介绍 FineReport报表软件是一款纯Java编写的、集数据展示(报表)和数据录入(表单)功能于一身的企业级web报表工具&#xff0c;它专业、简捷、灵活的特点和无码理念&#xff0c;仅需简单的拖拽操作便可以设计复杂的中国式报表&#xff0c;搭建数据决…

OkHttp源码分析:分发器任务调配,拦截器责任链设计,连接池socket复用

目录 一&#xff0c;分发器和拦截器 二&#xff0c;分发器处理异步请求 1.分发器处理入口 2.分发器工作流程 3.分发器中的线程池设计 三&#xff0c;分发器处理同步请求 四&#xff0c;拦截器处理请求 1.责任链设计模式 2.拦截器工作原理 3.OkHttp五大拦截器 一&#…

Nginx主要知识点总结

1下载nginx 到nginx官网nginx: download下载nginx&#xff0c;然后解压压缩包 然后双击nginx.exe就可以启动nginx 2启动nginx 然后在浏览器的网址处输入localhost&#xff0c;进入如下页面说明nginx启动成功 3了解nginx的配置文件 4熟悉nginx的基本配置和常用操作 Nginx 常…

概率论得学习和整理27:关于离散的数组 随机变量数组的均值,方差的求法3种公式,思考和细节。

目录 1 例子1&#xff1a;最典型的&#xff0c;最简单的数组的均值&#xff0c;方差的求法 2 例子1的问题&#xff1a;例子1只是1个特例&#xff0c;而不是普遍情况。 2.1 例子1各种默认假设&#xff0c;导致了求均值和方差的特殊性&#xff0c;特别简单。 2.2 我觉得 加权…

初学stm32 --- 时钟配置

目录 stm32时钟系统 时钟源 &#xff08;1&#xff09; 2 个外部时钟源&#xff1a; &#xff08;2&#xff09;2 个内部时钟源&#xff1a; 锁相环 PLL PLLXTPRE&#xff1a; HSE 分频器作为 PLL 输入 (HSE divider for PLL entry) PLLSRC&#xff1a; PLL 输入时钟源 (PL…

Latex+VsCode+Win10搭建

最近在写论文&#xff0c;overleaf的免费使用次数受限&#xff0c;因此需要使用本地的形式进行编译。 安装TEXLive 下载地址&#xff1a;https://mirror-hk.koddos.net/CTAN/systems/texlive/Images/ 下载完成直接点击iso进行安装操作。 安装LATEX Workshop插件 设置VsCode文…

深度学习之目标检测篇——残差网络与FPN结合

特征金字塔多尺度融合特征金字塔的网络原理 这里是基于resnet网络与Fpn做的结合&#xff0c;主要把resnet中的特征层利用FPN的思想一起结合&#xff0c;实现resnet_fpn。增强目标检测backone的有效性。代码实现如下&#xff1a; import torch from torch import Tensor from c…

Leetcode 面试150题 399.除法求值

系列博客目录 文章目录 系列博客目录题目思路代码 题目 链接 思路 广度优先搜索 我们可以将整个问题建模成一张图&#xff1a;给定图中的一些点&#xff08;点即变量&#xff09;&#xff0c;以及某些边的权值&#xff08;权值即两个变量的比值&#xff09;&#xff0c;试…

python实现Excel转图片

目录 使用spire.xls库 使用excel2img库 使用spire.xls库 安装&#xff1a;pip install spire.xls -i https://pypi.tuna.tsinghua.edu.cn/simple 支持选择行和列截图&#xff0c;不好的一点就是商业库&#xff0c;转出来的图片有水印。 from spire.xls import Workbookdef …

hpe服务器更新阵列卡firmware

背景 操作系统&#xff1a;RHEL7.8 hpe服务器经常出现硬盘断开&#xff0c;阵列卡重启问题&#xff0c;导致系统hang住。只能手动硬重启。 I/O error&#xff0c;dev sda smartpqi 0000:5c:00:0: resettiong scsi 1:1:0:1 smartpqi 0000:5c:00:0: reset of scsi 1:1:0:1:…

excel 使用vlook up找出两列中不同的内容

当使用 VLOOKUP 函数时&#xff0c;您可以将其用于比较两列的内容。假设您要比较 A 列和 B 列的内容&#xff0c;并将结果显示在 C 列&#xff0c;您可以在 C1 单元格中输入以下公式&#xff1a; 这个公式将在 B 列中的每个单元格中查找是否存在于 A 列中。如果在 A 列中找不到…

北邮,成电计算机考研怎么选?

#总结结论&#xff1a; 基于当前提供的24考研复录数据&#xff0c;从报考性价比角度&#xff0c;建议25考研的同学优先选择北邮计算机学硕。主要原因是:相比成电&#xff0c;北邮计算机学硕的目标分数更低&#xff0c;录取率更高&#xff0c;而且北邮的地理位置优势明显。对于…

OpenHarmony和OpenVela的技术创新以及两者对比

两款有名的国内开源操作系统&#xff0c;OpenHarmony&#xff0c;OpenVela都非常的优秀。本文对二者的创新进行一个简要的介绍和对比。 一、OpenHarmony OpenHarmony具有诸多有特点的技术突破和重要贡献&#xff0c;以下是一些主要方面&#xff1a; 架构设计创新 分层架构…

C语言——实现找出最高分

问题描述&#xff1a;分别有6名学生的学号、姓名、性别、年龄和考试分数&#xff0c;找出这些学生当中考试成绩最高的学生姓名。 //找出最高分#include<stdio.h>struct student {char stu_num[10]; //学号 char stu_name[10]; //姓名 char sex; //性别 int age; …

Qt Quick:CheckBox 复选框

复选框不止选中和未选中2种状态哦&#xff0c;它还有1种部分选中的状态。这3种状态都是Qt自带的&#xff0c;如果想让复选框有部分选中这个状态&#xff0c;需要将三态属性&#xff08;tristate&#xff09;设为true。 未选中的状态值为0&#xff0c;部分选中是1&#xff0c;选…