图解java.util.concurrent并发包源码系列——Condition条件等待队列深入详解

news2024/11/25 23:36:57

图解java.util.concurrent并发包源码系列——Condition条件等待队列深入详解

  • Condition的作用
  • Condition的原理
  • Condition源码
    • Condition的定义和Condition对象的获取
    • await方法
      • addConditionWaiter方法
        • unlinkCancelledWaiters方法
      • fullyRelease方法
      • isOnSyncQueue方法
      • checkInterruptWhileWaiting方法
      • reportInterruptAfterWait
    • signal方法
    • signalAll方法
  • 总结

往期文章:

  • 人人都能看懂的图解java.util.concurrent并发包源码系列 ThreadPoolExecutor线程池
  • 图解java.util.concurrent并发包源码系列,原子类、CAS、AtomicLong、AtomicStampedReference一套带走
  • 图解java.util.concurrent并发包源码系列——LongAdder
  • 图解java.util.concurrent并发包源码系列——深入理解AQS,看完可以吊打面试官
  • 图解java.util.concurrent并发包源码系列——深入理解ReentrantLock,看完可以吊打面试官
  • 图解java.util.concurrent并发包源码系列——深入理解ReentrantReadWriteLock读写锁,看完可以吊打面试官

Condition的作用

Condition是Java并发包提供的一个条件等待队列工具类,它具有让已获取到锁的线程当所需资源不满足的时候主动释放锁进入条件等待队列的能力,与Object的wait方法作用类似。

我们可以通过ReentrantLock的newCondition方法或者ReentrantReadWriteLock中WriteLock的newCondition方法获取Condition对象。

ReentrantLock与Condition的关系,相当于是synchronized关键字和Object#wait方法的关系。我们在调用Object的wait方法之前,必须先获取到synchronized锁的。相对应的在调用Condition的await方法前,必须要先获取到ReentrantLock的锁。

我们看一个生产者消费者的例子,了解Condition的具体作用。

/**
 * 生产者消费者例子
 * Created by huangjunyi on 2023/8/11.
 */
public class ProviderConsumerDemo {

    private ReentrantLock reentrantLock;

    private Condition notEmpty;

    private Condition notFull;

    private LinkedList<Integer> queue;

    private int size;

    private int capacity;

    private Provider provider;

    private Consumer consumer;

    public ProviderConsumerDemo(int capacity) {
        this.reentrantLock = new ReentrantLock();
        this.notEmpty = reentrantLock.newCondition();
        this.notFull = reentrantLock.newCondition();
        this.queue = new LinkedList();
        this.capacity = capacity;
        this.size = 0;
        this.provider = new Provider();
        this.consumer = new Consumer();
    }

    class Provider {
        public void push(Integer num) {
            try {
                reentrantLock.lock();
                // 如果queue已经满了,生产者在notFull条件队列中等待
                while (size == capacity) notFull.await();
                queue.addLast(num);
                size++;
                // 唤醒在notEmpty条件队列中等待的消费者
                notEmpty.signal();
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException("生产者发生异常");
            } finally {
                reentrantLock.unlock();
            }
        }
    }

    class Consumer {
        public Integer pull() {
            try {
                reentrantLock.lock();
                // 如果queue已经空了,消费者在notEmpty条件队列中等待
                while (size == 0) notEmpty.await();
                Integer num = queue.removeFirst();
                size--;
                // 唤醒在notFull条件队列中等待的生成者
                notFull.signal();
                return num;
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException("消费者发生异常");
            } finally {
                reentrantLock.unlock();
            }
        }
    }


    public Provider provider() {
        return this.provider;
    }

    public Consumer consumer() {
        return this.consumer;
    }

    public static void main(String[] args) throws InterruptedException {
        ProviderConsumerDemo providerConsumerDemo = new ProviderConsumerDemo(5);
        Provider provider = providerConsumerDemo.provider();
        Consumer consumer = providerConsumerDemo.consumer();

        Thread providerThread = new Thread(() -> {
            int num = 0;
            for (int i = 0; i < 10000; i++) {
                provider.push(++num);
            }
        });
        providerThread.start();

        Thread consumerThread = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                Integer num = consumer.pull();
                System.out.println(num);
            }
        });
        consumerThread.start();

        providerThread.join();
        consumerThread.join();

    }

}

notEmpty和notFull是Condition类型的条件等待队列,通过调用ReentrantLock的newCondition()方法生成。

  • 当生产者想要往queue中放入元素时,发现queue的容量已经满了,那么就会调用notFull的await方法,在条件队列中进行等待。当生产者成功往queue中放入元素后,就会调用notEmpty的signal()方法唤醒notEmpty条件等待队列中的消费者。
  • 当消费者想要从queue中获取元素时,发现queue已经空了,那么就会调用notEmpty的await方法,在条件队列中进行等待。当消费者成功从queue中获取到元素后,就会调用notFull的signal()方法唤醒notFull条件等待队列中的生产者。

这样就实现了一个生成者消费者的功能。

在这里插入图片描述

Condition的原理

Condition其实就是一个用于存放因某种资源不充足而处于等待状态的线程的一个队列。比如上面的生产者线程等待queue队列的空间不满,好让它能够往queue中放入它要放的元素,那么就可以调用Condition的await方法把该生产者线程放入在Condition内部的队列中进行等待。

一个线程被转移到Condition时,会被封装为一个Node节点,放入到Condition内部的队列中,这与AQS的逻辑是相似的。不同点是AQS队列是双向链表,而Condition队列是单向链表。然后Node节点的waitStatus属性固定是CONDITION(-2)。

Condition条件等待队列有一个firstWaiter头指针和一个lastWaiter尾指针。

当前线程调用Condition的await方法时,必须是已经获取到锁的,然后它需要释放锁,释放锁的时候会唤醒AQS队列中的下一个节点,如当前线程没有获取到锁就调用Condition的await方法,在尝试释放锁时就会抛异常。

其他线程做完自己的操作之后,如果它自己的操作会使得某个Condition队列中的线程所等待的资源又充足时,可以调用这个Condition的signal方法唤醒Condition队列中的一个线程,或者调用Condition的signalAll方法唤醒Condition队列中的所有线程。

比如上面例子的消费者,由于它的消费,使得queue又有空间了,那么它可以唤醒在Condition队列等待queue有空间的生产者线程。

在这里插入图片描述

Condition源码

Condition的定义和Condition对象的获取

Condition接口:

public interface Condition {

    void await() throws InterruptedException;

    void awaitUninterruptibly();

    long awaitNanos(long nanosTimeout) throws InterruptedException;

    boolean await(long time, TimeUnit unit) throws InterruptedException;

    boolean awaitUntil(Date deadline) throws InterruptedException;

    void signal();

    void signalAll();
}

可以看到Condition接口除了普通的等待和唤醒方法,还提供了不响应中断(默认响应中断)和带超时机制的等待方法。

而在AQS的内部就定义了一个实现了Condition接口的内部类:

public class ConditionObject implements Condition, java.io.Serializable {

        private transient Node firstWaiter;

        private transient Node lastWaiter;
}

可以看到ConditionObject内部带了头指针firstWaiter和尾指针lastWaiter。

在这里插入图片描述

通过ReentrantLock的newCondition方法可以获取到ConditionObject对象。

ReentrantLock#newCondition:

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

ReentrantLock.Sync#newCondition:

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

在这里插入图片描述

await方法

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            // 封装为Node节点,放入Condition队列中
            Node node = addConditionWaiter();
            // 释放锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 如果当前节点不在AQS同步队列中,那么就一直循环park
            while (!isOnSyncQueue(node)) {
            	// park挂起当前线程
                LockSupport.park(this);
                // 唤醒后检查是否被中断,记录一下中断标记interruptMode,方便后续处理
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 在AQS的同步队列等待重新获取锁,如果在此期间再次被中断,则次记录标记interruptMode
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            // 清除条件等待队列中被取消的节点
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            // 对中断的统一处理
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
  1. 首先要把当前线程封装成一个Node,然后把该Node放入到Condition的条件队列中。
  2. Node入队列后,当前线程要释放所有获取的锁。
  3. 释放完所有的锁后,就一直while循环检查当前线程对应的节点是否已经被挪到了AQS的同步队列当中,如果已经挪入了(其他线程调用Condition的signal方法会把Condition队列中的一个节点挪到AQS同步队列中),那么跳出循环,否则就把当前线程挂起。每次线程醒来时,都要检查一下自己是否被中断了,如果是,要记录一个中断标记interruptMode,方便后续处理。
  4. 节点被挪入到AQS同步队列后,当前线程就要等待重新获取锁。获取到锁后再检查一下自己是否被中断了,如果是,则更新一下中断标记。
  5. 获取到锁后,会尝试清除一遍条件队列中已被中断的节点。
  6. 最后对中断的情况进行处理

在这里插入图片描述

addConditionWaiter方法

addConditionWaiter方法的作用是往条件等待队列添加一个节点。

        private Node addConditionWaiter() {
            Node t = lastWaiter;
            // 如果尾节点不是CONDITION状态的,那么清理一遍队列,然后再次获取尾节点
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            // 创建一个新节点,waitStatus状态值为CONDITION
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            // t尾节点为null,表示队列为空,当前节点作为头节点
            if (t == null)
                firstWaiter = node;
            // 队列不为空,就入队列尾部
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

addConditionWaiter方法的大体思路就是拿到一个waitStatus状态值为CONDITION的节点,或者null,然后把当前线程封装为一个Node节点,waitStatus设置为CONDITION,然后放入队列尾部(队列不为空)或者设置为队列头节点(队列为空)。

在这里插入图片描述

unlinkCancelledWaiters方法

unlinkCancelledWaiters()方法的作用是清理条件等待队列。每个进入条件等待队列中的节点的waitStatus属性原本都是CONDITION,但是随着队列中某些节点的线程被中断等待,它的waitStatus属性就不是CONDITION了,那么这些waitStatus属性不是CONDITION的节点,是不需要的,自然要清理出队列。

        private void unlinkCancelledWaiters() {
        	// t指针,指向后面一个节点
            Node t = firstWaiter;
            // trail指针,指向前面一个节点
            Node trail = null;
            while (t != null) {
                Node next = t.nextWaiter;
                // t指针发现waitStatus不为CONDITION的节点
                if (t.waitStatus != Node.CONDITION) {
                    t.nextWaiter = null;
                    if (trail == null)
                    	// 要断连的节点是头节点,那么更新头节点为t指针指向的节点的下一个节点
                        firstWaiter = next;
                    else
                    	// trail指针指向的节点的nextWaiter指针,指向t指针指向的节点的下一个节点
                        trail.nextWaiter = next;
                    if (next == null)
                    	// 没有后续节点了,把trail指针当前所指的节点设置为尾节点
                        lastWaiter = trail;
                }
                else
                    trail = t;
                t = next;
            }
        }

两个指针一前一后地遍历队列,t是后一个节点的指针,trail是前一个节点的指针。当t指针遇到waitStatus属性不为CONDITION的节点的时候,就把trail节点指向t节点的下一个节点(next),这样t指针指向的节点自然断连出队列,后续会被GC回收。而如果要断连的节点刚好是头节点,那么就要更新头节点为要断连的节点的下一个节点。

在这里插入图片描述

fullyRelease方法

fullyRelease方法用于进入了条件等待队列的节点对应的线程释放锁资源。

    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            // 调用AQS的release方法一口气全部释放
            if (release(savedState)) {
                failed = false;
                // 释放了多少,返回多少,后面会再次获取
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

int savedState = getState(); 获取state遍历,然后再调用 release(savedState) 一口气把自己获取的所有锁资源全部释放。然后释放了多少,方法的返回值就是多少,后续重新获取锁时就获取多少。

在这里插入图片描述

isOnSyncQueue方法

isOnSyncQueue方法是用于判断当前node节点是否在AQS同步队列中,

    final boolean isOnSyncQueue(Node node) {
    	// waitStatus 属性还是CONDITION ,或者prev指针是空,代表不在AQS同步队列中,因为AQS同步队列中的节点是没有CONDITION 状态的,而且AQS队列是一个双向链表,prev是不会为null的
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        // node的next指针不为null,那么node肯定再AQS队列中,因为只有再AQS同步队列中,才会用next指针指向下一个节点,在Condition条件队列中用的是nextWaiter指针
        if (node.next != null) 
            return true;
		// node的prev指针不为null,也不代表当前节点就在AQS队列中,因为把一个节点放入AQS同步队列是通过CAS完成的,但是CAS有可能会失败,所以这里调用findNodeFromTail方法从尾部开始寻找,确保node节点确实已经在AQS同步队列中了。
        return findNodeFromTail(node);
    }

首先判断当前节点如果状态是CONDITION ,那么肯定不在AQS同步队列中,因为AQS同步队列中的节点是没有CONDITION 状态的。
如果当前节点的prev指针是null,那么也不可能在AQS队列中。因为AQS同步队列是一个双向链表,而node节点是从链表尾部进去的,如果它再AQS同步队列中,prev指针是不可能为null的。
如果node节点的next指针不为null,那么肯定再AQS同步队列中。因为只有AQS同步队列中的节点才会用next指针记录下一个节点,在Condition条件等待队列中的节点是用nextWaiter指针指向下一个节点的。
当这里还不能确定当前节点已经在AQS队列中,即使当前节点的prev指针不为null,但是一个节点进入AQS队列是通过CAS放进去的,而CAS是有可能失败的,所以还要调findNodeFromTail方法从尾部开始寻找,确保当前节点已经进入了AQS同步队列。

    private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            if (t == node)
            	// 找到了,返回true
                return true;
            if (t == null)
            	// 没有了,返回false
                return false;
            // 通过prev指针网球遍历
            t = t.prev;
        }
    }

findNodeFromTail方法就是从尾部开始,通过prev指针往前遍历,直到找到为止或者遍历完毕。

在这里插入图片描述

checkInterruptWhileWaiting方法

checkInterruptWhileWaiting方法用于检查当前线程是否被中断,也就是取消等待。如果当前线程被中断了,要设置对应的标志,方便后续处理。

        private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }

Thread.interrupted()获取当前线程中断标志位,这个方法被调用后,当前线程的Thread对象的中断标志位就会被复位。

transferAfterCancelledWait(node):

    final boolean transferAfterCancelledWait(Node node) {
    	// compareAndSetWaitStatus尝试把当前节点的waitStatus状态改为0,
    	// 如果修改成功,表示当前节点处于Condition条件等待队列中被中断。
    	// 如果CAS不成功,表示当前节点的waitStatus状态不为CONDITION,当前节点不在Condition队列中,也不是在条件队列中被中断,那么当前节点就是在signal之后被中断。
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            enq(node);
            return true;
        }
        // 到这里,说明当前节点node,是在signal之后被中断的,确保节点已经在AQS队列中之后,返回false。
        while (!isOnSyncQueue(node))
            Thread.yield();
        return false;
    }

这里中断分两种情况,一种当前节点还是CONDITION状态,在Condition队列中被中断;另一种情况就是被其他线程调用了signal方法,修改了当前node节点的waitStatus,然后当前node节点才被中断。

分这两种情况是因为后面对于这两种情况的处理是不同的,如果是在Condition队列中被中断的,那么后续要抛出中断异常。如果是被别的线程调用了signal方法,修改了当前node节点的waitStatus后,才中断当前线程,由于这里通过Thread.interrupted()检查当前线程是否被中断时,把当前线程的中断标志位复位了,这里只需要把中断标志位重新置位即可。

在这里插入图片描述

reportInterruptAfterWait

        private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            // 在Condition条件队列中被打断的,抛异常
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
            // 被别的线程signal之后才被打断的,中断标志位重新置位
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }
        
		static void selfInterrupt() {
        	Thread.currentThread().interrupt();
    	}
  1. 如果是在Condition条件队列中被打断的,那么前面记录的中断标记interruptMode就是THROW_IE,这里要抛出异常。
  2. 如果是被别的线程signal之后才被打断的,中断标志位重新置位即可。
  3. 两个分支没进,那么就是没有被中断。

在这里插入图片描述

signal方法

signal方法用于唤醒等待在Condition条件队列中的线程。

        public final void signal() {
        	// 非持有锁的线程,调signal方法会抛出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

首先判断如果当前线程不是持有锁的线程,那么会抛出一个异常。如果当前线程是持有锁的线程,那么会调用doSignal方法。

        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            // 调用transferForSignal(first)方法转移当前节点到AQS同步队列中,成功转移一次就退出循环
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

doSignal方法里面就是拿到Condition条件队列中的头节点,转移到AQS同步队列中,转移成功就退出循环,方法结束。

    final boolean transferForSignal(Node node) {
		// 转移前,先修改节点状态
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
		
		// 节点如AQS同步队列
        Node p = enq(node);

		// enq(node)会返回node入队后的前驱节点,
		// 如果前驱节点的waitStatus属性是CANCELLED状态,或者CAS修改前驱节点waitStatus属性不成功,那么就直接唤醒node节点的线程,
		// 否则node节点的线程将会由AQS中该节点的前驱节点的线程释放锁后唤醒
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

在这里插入图片描述

signalAll方法

signalAll方法与signal方法功能相同,区别是signal方法只转一个节点到AQS队列中,而signalAll方法则是转移所有节点到AQS队列中。

        public final void signalAll() {
        	// 当前线程没有获取锁,抛异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignalAll(first);
        }

如果发现当选线程没有获取到锁就调用了signalAll方法,那么也是抛一个异常。否则就调用doSignalAll方法。

        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }

可以看到也是一个do-while循环,但是这个循环不是转移成功一个节点就退出,而是直到没有节点可以转移为止。

在这里插入图片描述

总结

Condition的源码到此就分析完毕了,Condition的核心逻辑就是把调用了await方法的线程,封装为Node节点放入到条件队列中,释放掉该线程获取的所有锁资源,然后挂起,等待其他线程调用signal方法或者signalAll方法把他移入AQS同步队列中然后将其唤醒。

在这里插入图片描述

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

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

相关文章

一台阿里云服务器怎么部署多个网站?以CentOS系统为例

本文阿里云百科介绍如何在CentOS 7系统的ECS实例上使用Nginx搭建多个Web站点。本教程适用于熟悉Linux操作系统&#xff0c;希望合理利用资源、统一管理站点以提高运维效率的用户。比如&#xff0c;您可以在一台云服务器上配置多个不同分类的博客平台或者搭建多个Web站点实现复杂…

为新手和非技术人员提供扩展Web网站提供一个升级指南

本指南总结了扩展的基本原则&#xff0c;从一台服务器扩展到能够服务数百万用户的Web应用程序。它面向在技术领域工作的新手和非开发人员。因此&#xff0c;如果您刚刚部署了您的多云平台VPN设置&#xff0c;那么本文并不适合您。 话不多说&#xff0c;那就让我们开始吧&#x…

STM32CubeMX工程配置说明

一、STM32CubeMX配置 1.1 设置时钟 单片机的时钟&#xff0c;相当于人的心跳。只要单片机工作&#xff0c;必须要开启时钟&#xff01; STM32单片机共有4个时钟来源&#xff1a; 名称缩写频率外部连接功能用途特性外部高速晶体振荡器HSE4~16MHz4~16MHz晶体 系统时钟/RTC成…

流水线时序调度之规避冲突

1 写在前面的&#xff1a; 其实略微一个大点的机器&#xff0c;一个测试流程需要若干个步骤&#xff0c;都可以用流水线的思维去看待它&#xff1b; 我之前也没往流水线的角度去考虑&#xff0c;那有些机器的时序调度是不好理解的&#xff0c;甚至计算个通量都很麻烦&#xff…

p5.js 视频播放指南

theme: smartblue 本文简介 在刚接触 p5.js 时我以为这只是一个艺术方向的 canvas 库&#xff0c;没想到它还支持视频文件和视频流的播放。 本文简单讲讲如何使用 P5.js 播放视频。 播放视频文件 p5.js 除了可以使用 video 元素播放视频外&#xff0c;还支持使用 image 控件播放…

Linux 终端操作命令(3)内部命令用法

Linux 终端操作命令 内部命令用法 A- alias NAME alias - Define or display aliases. SYNOPSIS alias [-p] [name[value] ... ] DESCRIPTION Define or display aliases. Without arguments, alias prints the list of aliases in the reusable form al…

创建MySQL数据库和创建表的详细步骤(navicat)

目录 一、介绍 二、操作步骤 &#xff08;一&#xff09;新建连接 &#xff08;二&#xff09;新建数据库 &#xff08;三&#xff09;新建表 插入数据测试 对字段进行增加或者修改 三、关于MySQL的其他文章&#xff08;额外篇&#xff09; 一、介绍 在创建数据库…

(统计学习方法|李航)第一章统计学习方法概论-一二三节统计学习及统计学习种类,统计学习三要素

目录 一&#xff0c;统计学习 1.统计学习的特点 2.统计学习的对象 3.统计学习的目的 4.统计学习的方法 5.统计学习方法的研究 6.重要性 二&#xff0c;统计学习的基本种类 1.监督学习 &#xff08;1&#xff09;输入空间&#xff0c;输出空间和特征空间 &#xff08…

sklearn机器学习库(一)sklearn中的决策树

sklearn机器学习库(一)sklearn中的决策树 sklearn中决策树的类都在”tree“这个模块之下。 tree.DecisionTreeClassifier分类树tree.DecisionTreeRegressor回归树tree.export_graphviz将生成的决策树导出为DOT格式&#xff0c;画图专用tree.export_text以文字形式输出树tree.…

成像镜头均匀性校正——360°超广角均匀校准光源

随着空间技术的不断发展&#xff0c;遥感仪器在对地观测、大气探测及海洋探测等方面的应用也不断拓展&#xff0c;以实现不同任务的观测精度。空间遥感仪器热控技术旨在保证遥感器各部件所需温度水平、温度梯度和温度稳定度&#xff0c;以满足遥感器高质量成像要求。 近年来我国…

ubuntu20.04磁盘满了 /dev/mapper/ubuntu--vg-ubuntu--lv 占用 100%

问题 执行 mysql 大文件导入任务&#xff0c;最后快完成了&#xff0c;查看结果发现错了&#xff01;悲催&#xff01;都执行了 两天了 The table ‘XXXXXX’ is full &#xff1f; 磁盘满了&#xff1f; 刚好之前另一个 centos 服务器上也出现过磁盘满了&#xff0c;因此&a…

变形金刚在图像识别方面比CNN更好吗?

链接到文 — https://arxiv.org/pdf/2010.11929.pdf 一、说明 如今&#xff0c;在自然语言处理&#xff08;NLP&#xff09;任务中&#xff0c;转换器已成为goto架构&#xff08;例如BERT&#xff0c;GPT-3等&#xff09;。另一方面&#xff0c;变压器在计算机视觉任务中的使用…

关于技术转管理角色的认知

软件质量保障&#xff1a;所寫即所思&#xff5c;一个阿里质量人对测试的所感所悟。 程序员发展的岔路口 技术人做了几年专业工作之后&#xff0c;会来到一个重要的“分岔路口”&#xff0c;一边是专业的技术路线&#xff0c;一边是技术团队的管理路线。不少人就开始犯难&…

sqlsessionfactory和sqlsession是否线程安全?

判断是否线程安全的规则&#xff1a;是否存在多线程间可共享的变量 sqlsessionfactory是线程安全的&#xff0c;默认的实现类只有一个final属性。 sqlsession单独来看是线程不安全的&#xff0c;但是我们用mybatis时&#xff0c;mapper接口的使用是基于动态代理&#xff0c;这…

计算机竞赛 GRU的 电影评论情感分析 - python 深度学习 情感分类

1 前言 &#x1f525;学长分享优质竞赛项目&#xff0c;今天要分享的是 &#x1f6a9; GRU的 电影评论情感分析 - python 深度学习 情感分类 &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;3分工作量&#xff1a;3分创新点&#xff1a;4分 这…

python爬虫5:requests库-案例3

python爬虫5&#xff1a;requests库-案例3 前言 ​ python实现网络爬虫非常简单&#xff0c;只需要掌握一定的基础知识和一定的库使用技巧即可。本系列目标旨在梳理相关知识点&#xff0c;方便以后复习。 申明 ​ 本系列所涉及的代码仅用于个人研究与讨论&#xff0c;并不会对网…

uniapp 小兔鲜儿 - 首页模块(1)

目录 自定义导航栏 静态结构 安全区域​ 通用轮播组件 静态结构 自动导入全局组件 全局组件类型声明 .d.ts文件 注册组件 vue/runtime-core 首页 – 轮播图指示点 首页 – 获取轮播图数据 首页 – 轮播图数据类型并渲染 首页 – 轮播图总结 首页分类 首页 – 前…

计算机竞赛 opencv python 深度学习垃圾图像分类系统

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; opencv python 深度学习垃圾分类系统 &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;3分工作量&#xff1a;3分创新点&#xff1a;4分 这是一个较为新颖的竞…

VR安全宣传系列:防触电虚拟现实体验

在电气工作中&#xff0c;安全问题始终是重中之重。为了更好地提高公众的电气安全意识和技能&#xff0c;广州华锐互动开发了一种基于虚拟现实技术的模拟系统——VR防触电虚拟体验系统。这种系统可以模拟各种因操作不当导致的触电事故场景&#xff0c;并提供沉浸式的体验&#…

浅谈机器人流程自动化(RPA)

1.什么是RPA RPA代表机器人流程自动化&#xff08;Robotic Process Automation&#xff09;&#xff0c;是一种利用软件机器人或机器人工作流程来执行重复性、规范性和高度可预测性的业务流程的技术。这些流程通常涉及许多繁琐的、重复的任务&#xff0c;例如数据输入、数据处…