线程池原理(二)关键源码剖析

news2024/11/15 5:21:14

更好的阅读体验 \huge{\color{red}{更好的阅读体验}} 更好的阅读体验

属性 & 构造方法


对于 ThreadPoolExecutor 有几个关键的属性,这里需要先大致了解:

public class ThreadPoolExecutor extends AbstractExecutorService {
 
    // 控制变量-存放状态和线程数
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
 
    // 任务队列,必须是阻塞队列
    private final BlockingQueue<Runnable> workQueue;
 
    // 工作线程集合,存放线程池中所有的(活跃的)工作线程,只有在持有全局锁mainLock的前提下才能访问此集合
    private final HashSet<Worker> workers = new HashSet<>();
    
    // 全局锁
    private final ReentrantLock mainLock = new ReentrantLock();
 
    // awaitTermination方法使用的等待条件变量
    private final Condition termination = mainLock.newCondition();
    
    // 线程工厂,用于创建新的线程实例
    private volatile ThreadFactory threadFactory;
 
    // 拒绝执行处理器,对应不同的拒绝策略
    private volatile RejectedExecutionHandler handler;
    
    // 空闲线程等待任务的时间周期,单位是纳秒
    private volatile long keepAliveTime;
    
    // 是否允许核心线程超时,如果为true则keepAliveTime对核心线程也生效
    private volatile boolean allowCoreThreadTimeOut;
    
    // 核心线程数
    private volatile int corePoolSize;
 
    // 线程池容量
    private volatile int maximumPoolSize;
 
    ......
}    

对于其构造方法如下:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

由此可见,当我们手动创建线程池时,线程池并不会直接工作,而是将我们手动设置的关键参数进行初始化。


execute 方法


ThreadPoolExecutor 的最基本使用方式就是通过 execute 方法提交一个 Runnable 任务,首先看图理解 execute 的执行逻辑

在这里插入图片描述

查看源码中的 execute 方法,这里我们分层次来看,先看最主要的外部的几个条件判断:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
    	// 获取当前线程池里的线程数
        if (workerCountOf(c) < corePoolSize) {  // 判断工作线程数,如果小于 corePollSize,则执行addWork
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
    	//否则,如果线程池还在运行,offer 到等待队列中
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }  // 这里判断是否可以通过创建非核心线程的方式创建线程并执行
        else if (!addWorker(command, false)) 
            reject(command);  // 如果不能,则执行拒绝策略
    }

其实源码里面的注释解释的已经很明白了,这里的逻辑其实已经说明了一些常见的问题:

  • 当提交任务时,只要核心线程数没有满,都会新建核心线程执行任务,不会使用以前创建好的空闲的核心线程去执行
  • 当无法向队列提交任务时,如果可以新建非核心线程,则会新建非核心线程并执行任务,不会在创建之后从队列中取任务执行

addWorker 方法


接下来我们灌注一下 addWorker 方法:

// 添加工作线程,如果返回false说明没有新创建工作线程,如果返回true说明创建和启动工作线程成功
private boolean addWorker(Runnable firstTask, boolean core) {
    //第一步,计数判断,不符合条件打回false
    retry:
    for (int c = ctl.get();;) {
        // 这个是十分复杂的条件,这里先拆分多个与(&&)条件:
        // 1. 线程池状态至少为SHUTDOWN状态,也就是rs >= SHUTDOWN(0)
        // 2. 线程池状态至少为STOP状态,也就是rs >= STOP(1),或者传入的任务实例firstTask不为null,或者任务队列为空
        // 其实这个判断的边界是线程池状态为shutdown状态下,不会再接受新的任务,在此前提下如果状态已经到了STOP、或者传入任务不为空、或者任务队列为空(已经没有积压任务)都不需要添加新的线程
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP)
                || firstTask != null
                || workQueue.isEmpty()))
            return false;
        // 注意这也是一个死循环 - 二层循环
        for (;;) {
            // 这里每一轮循环都会重新获取工作线程数wc
            // 1. 如果传入的core为true,表示将要创建核心线程,通过wc和corePoolSize判断,如果wc >= corePoolSize,则返回false表示创建核心线程失败
            // 2. 如果传入的core为false,表示将要创非建核心线程,通过wc和maximumPoolSize判断,如果wc >= maximumPoolSize,则返回false表示创建非核心线程失败
            if (workerCountOf(c)
                >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                return false;
            // 成功通过CAS更新工作线程数wc,则break到最外层的循环
            if (compareAndIncrementWorkerCount(c))
                break retry;
            // 走到这里说明了通过CAS更新工作线程数wc失败,这个时候需要重新判断线程池的状态是否由RUNNING已经变为SHUTDOWN
            c = ctl.get();  // Re-read ctl
            // 如果线程池状态已经由RUNNING已经变为SHUTDOWN,则重新跳出到外层循环继续执行
            if (runStateAtLeast(c, SHUTDOWN))
                continue retry;
            // 如果线程池状态依然是RUNNING,CAS更新工作线程数wc失败说明有可能是并发更新导致的失败,则在内层循环重试即可 
            // else CAS failed due to workerCount change; retry inner loop 
        }
    }
    
    //第二步,创建新work放入线程集合works(一个HashSet)
    
    // 标记工作线程是否启动成功
    boolean workerStarted = false;
    // 标记工作线程是否创建成功
    boolean workerAdded = false;
    Worker w = null;
    try {
        // 传入任务实例firstTask创建Worker实例,Worker构造里面会通过线程工厂创建新的Thread对象,所以下面可以直接操作Thread t = w.thread
        // 这一步Worker实例已经创建,但是没有加入工作线程集合或者启动它持有的线程Thread实例
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            // 这里需要全局加锁,因为会改变一些指标值和非线程安全的集合
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                int c = ctl.get();
                // 这里主要在加锁的前提下判断ThreadFactory创建的线程是否存活或者判断获取锁成功之后线程池状态是否已经更变为SHUTDOWN
                // 1. 如果线程池状态依然为RUNNING,则只需要判断线程实例是否存活,需要添加到工作线程集合和启动新的Worker
                // 2. 如果线程池状态小于STOP,也就是RUNNING或者SHUTDOWN状态下,同时传入的任务实例firstTask为null,则需要添加到工作线程集合和启动新的Worker
                // 对于2,换言之,如果线程池处于SHUTDOWN状态下,同时传入的任务实例firstTask不为null,则不会添加到工作线程集合和启动新的Worker
                // 这一步其实有可能创建了新的Worker实例但是并不启动(临时对象,没有任何强引用),这种Worker有可能成功下一轮GC被收集的垃圾对象
                if (isRunning(c) ||
                    (runStateLessThan(c, STOP) && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    // 把创建的工作线程实例添加到工作线程集合
                    workers.add(w);
                    int s = workers.size();
                    // 尝试更新历史峰值工作线程数,也就是线程池峰值容量
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    // 这里更新工作线程是否启动成功标识为true,后面才会调用Thread#start()方法启动真实的线程实例
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            // 如果成功添加工作线程,则调用Worker内部的线程实例t的Thread#start()方法启动真实的线程实例
            if (workerAdded) {
                t.start();
                // 标记线程启动成功
                workerStarted = true;
            }
        }
    } finally {
        // 线程启动失败,需要从工作线程集合移除对应的Worker
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}
 
// 添加Worker失败
private void addWorkerFailed(Worker w) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // 从工作线程集合移除之
        if (w != null)
            workers.remove(w);
        // wc数量减1    
        decrementWorkerCount();
        // 基于状态判断尝试终结线程池
        tryTerminate();
    } finally {
        mainLock.unlock();
    }
}

先看看 addWork 的两个参数,第一个是需要提交的线程Runnable firstTask,第二个参数是 boolean 类型,表示是否为核心线程。

其中三处调用 addWork:

  • 第一次,条件if (workerCountOf© < corePoolSize):这个很好理解,工作线程数少于核心线程数,提交任务。所以addWorker(command, true)
  • 第二次,如果 workerCountOf(recheck) == 0: 如果worker的数量为0,那就 addWorker(null,false),由于之前已经把command提交到阻塞队列了workQueue.offer(command)。所以提交一个空线程,直接从阻塞队列里面取就可以了。
  • 第三次,如果线程池没有RUNNING或者offer阻塞队列失败,addWorker(command,false):说明阻塞队列满了,将任务提交到非核心线程池。

Worker 对象


线程池中的每一个线程被封装成了内部的一个Worker对象,ThreadPool维护的其实就是一组Worker对象:

private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
    private static final long serialVersionUID = 6138294804551838833L;
 
    // 保存ThreadFactory创建的线程实例,如果ThreadFactory创建线程失败则为null
    final Thread thread;
    // 保存传入的Runnable任务实例
    Runnable firstTask;
    // 记录每个线程完成的任务总数
    volatile long completedTasks;
    
    // 唯一的构造函数,传入任务实例firstTask,注意可以为null
    Worker(Runnable firstTask) {
        // 禁止线程中断,直到runWorker()方法执行
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        // 通过ThreadFactory创建线程实例,注意一下Worker实例自身作为Runnable用于创建新的线程实例
        this.thread = getThreadFactory().newThread(this);
    }
 
    // 委托到外部的runWorker()方法,注意runWorker()方法是线程池的方法,而不是Worker的方法
    public void run() {
        runWorker(this);
    }
 
    // Lock methods
    //
    // The value 0 represents the unlocked state.
    // The value 1 represents the locked state.
    //  是否持有独占锁,state值为1的时候表示持有锁,state值为0的时候表示已经释放锁
    protected boolean isHeldExclusively() {
        return getState() != 0;
    }
 
    // 独占模式下尝试获取资源,这里没有判断传入的变量,直接CAS判断0更新为1是否成功,成功则设置独占线程为当前线程
    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }
    
    // 独占模式下尝试是否资源,这里没有判断传入的变量,直接把state设置为0
    protected boolean tryRelease(int unused) {
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }
    
    // 加锁
    public void lock()        { acquire(1); }
 
    // 尝试加锁
    public boolean tryLock()  { return tryAcquire(1); }
 
    // 解锁
    public void unlock()      { release(1); }
 
    // 是否锁定
    public boolean isLocked() { return isHeldExclusively(); }
    
    // 启动后进行线程中断,注意这里会判断线程实例的中断标志位是否为false,只有中断标志位为false才会中断
    void interruptIfStarted() {
        Thread t;
        if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
            try {
                t.interrupt();
            } catch (SecurityException ignore) {
            }
        }
    }
}

runWorker 方法


Worker 方法中的 run 方法实际调用的是线程池中的 runWorker 方法:

final void runWorker(Worker w) {
    // 获取当前线程,实际上和Worker持有的线程实例是相同的
    Thread wt = Thread.currentThread();
    // 获取Worker中持有的初始化时传入的任务对象,这里注意存放在临时变量task中
    Runnable task = w.firstTask;
    // 设置Worker中持有的初始化时传入的任务对象为null
    w.firstTask = null;
    // 由于Worker初始化时AQS中state设置为-1,这里要先做一次解锁把state更新为0,允许线程中断
    w.unlock(); // allow interrupts
    // 记录线程是否因为用户异常终结,默认是true
    boolean completedAbruptly = true;
    try {
        // 1. 如果线程池外部传递了任务则直接执行外部传递的任务
        // 2. 如果没有获取到外部传递进来的任务则调用getTask()去队列中获取任务并执行
        // 2.1. 如果在任务队列中获取到了任务则直接执行已经获取的任务
        // 2.2. 如果任务队列为空,没有任务则反复执行空循环阻塞当前线程死亡
        // getTask()由于使用了阻塞队列,这个while循环如果命中后半段会处于阻塞或者超时阻塞状态,getTask()返回为null会导致线程跳出死循环使线程终结
        while (task != null || (task = getTask()) != null) {
            // Worker加锁,本质是AQS获取资源并且尝试CAS更新state由0更变为1
            w.lock();
            // 如果线程池正在停止(也就是由RUNNING或者SHUTDOWN状态向STOP状态变更),那么要确保当前工作线程是中断状态
            // 否则,要保证当前线程不是中断状态
            if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                    runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                // 钩子方法,任务执行前
                beforeExecute(wt, task);
                try {
                    // 调用任务的run方法,而不是start()方法,因为Worker本身就是一个线程类
                    task.run();
                    // 钩子方法,任务执行后 - 正常情况
                    afterExecute(task, null);
                } catch (Throwable ex) {
                    // 钩子方法,任务执行后 - 异常情况
                    afterExecute(task, ex);
                    throw ex;
                }
            } finally {
                // 清空task临时变量,这个很重要,否则while会死循环执行同一个task
                task = null;
                // 执行完成后自增当前工作线程执行的任务数量
                w.completedTasks++;
                // Worker解锁,本质是AQS释放资源,设置state为0
                w.unlock();
            }
        }
        // 如果线程能够执行到最后一行代表线程执行过程中没有由于发生异常导致跳出循环,将 突然结束 标志改为false
        completedAbruptly = false;
    } finally {
        // 处理线程退出,completedAbruptly为true说明由于用户异常导致线程非正常退出
        // 执行回收工作线程的逻辑
        processWorkerExit(w, completedAbruptly);
    }
}

runWorker 方法的本质其实就是通过一个 while 循环不断地通过 getTask() 方法获取任务,在调用方法执行的时候会先获取外部传递的任务,如果没有获取到外部传递的任务则调用 getTask() 方法获取任务队列中的任务并执行


getTask 方法


private Runnable getTask() {
    // timeOut变量的值表示上次从阻塞队列中取任务时是否超时
    boolean timedOut = false; // Did the last poll() time out?
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        // Check if queue empty only if necessary.
        /*
         * 如果线程池状态rs >= SHUTDOWN,也就是非RUNNING状态,再进行以下判断:
         * 1. rs >= STOP,线程池是否正在stop;
         * 2. 阻塞队列是否为空。
         * 如果以上条件满足,则将workerCount减1并返回null。
         * 因为如果当前线程池状态的值是SHUTDOWN或以上时,不允许再向阻塞队列中添加任务。
         */
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }
        int wc = workerCountOf(c);
        // 判断是不是要超时处理
        // timed变量用于判断是否需要进行超时控制。
        // allowCoreThreadTimeOut默认是false,也就是核心线程不允许进行超时;
        // wc > corePoolSize,表示当前线程池中的线程数量大于核心线程数量;
        // 对于超过核心线程数量的这些线程,需要进行超时控制
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        
        /*
         * 将线程队列数量原子性减
         * wc > maximumPoolSize的情况是因为可能在此方法执行阶段同时执行了setMaximumPoolSize方法;
         * timed && timedOut 如果为true,表示当前操作需要进行超时控制,并且上次从阻塞队列中获取任务发生了超时
         * 接下来判断,如果有效线程数量大于1,或者阻塞队列是空的,那么尝试将workerCount减1;
         * 如果减1失败,则返回重试。
         * 如果wc == 1时,也就说明当前线程是线程池中唯一的一个线程了。
         */
        if ((wc > maximumPoolSize || (timed && timedOut))
            //计数器做原子递减,递减成功后,返回null,for被中止
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            //递减失败,继续下一轮循环,直到成功
            continue;
        }
        try {
            //如果线程可被释放,那就poll,释放的时间为:keepAliveTime
			//否则,线程是不会被释放的,take一直被阻塞在这里,直到来了新任务继续工作
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            // 到这里说明可被释放的线程等待超时,已经销毁,设置该标记,下次循环将线程数减少
            timedOut = true;
        } catch (InterruptedException retry) {
            // 如果获取任务时当前线程发生了中断,则设置timedOut为false并返回循环重试
            timedOut = false;
        }
    }
}

综上所述,线程池内部将我们提交的任务封装成了一个个 Woker 对象来管理,我们调用线程池的 execute 方法,实际上:

  • execute 根据策略调用 -> addWoker 方法
  • addWoker 方法调用 Worker 的 run 方法
  • Worker 的 run 方法调用 runWorker 方法
  • runWorker 方法不断去用 getTask 获取任务执行

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

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

相关文章

什么是NLP分词(Tokenization)

在自然语言处理和机器学习的领域里&#xff0c;咱们得聊聊一个超基础的技巧——就是“分词”啦。这个技巧啊&#xff0c;就是把一长串的文字切分成小块&#xff0c;让机器能更容易地“消化”。这些小块&#xff0c;不管大小&#xff0c;单个的字符也好&#xff0c;整个的单词也…

汽车EDI:德国大众 EDI 项目案例

德国大众&#xff08;Volkswagen&#xff09;是成立于1937年的大型汽车制造商&#xff0c;总部位于德国沃尔夫斯堡。大众集团拥有众多知名汽车品牌&#xff0c;如奥迪、保时捷、宾利、兰博基尼、布加迪等&#xff0c;业务遍及全球。作为一个全球性企业&#xff0c;大众集团依赖…

时序预测|基于变分模态分解-时域卷积-双向长短期记忆-注意力机制多变量时间序列预测VMD-TCN-BiLSTM-Attention

时序预测|基于变分模态分解-时域卷积-双向长短期记忆-注意力机制多变量时间序列预测VMD-TCN-BiLSTM-Attention 文章目录 前言时序预测|基于变分模态分解-时域卷积-双向长短期记忆-注意力机制多变量时间序列预测VMD-TCN-BiLSTM-Attention 一、VMD-TCN-BiLSTM-Attention模型1. **…

SystemUI plugin 开发

一、前言 SystemUI结构复杂,模块数量众多,最重要的是SystemUI属于常驻进程是一个系统的门面,且不能自升级,如果定制功能对主项目做复杂的修改,首先会造成适配压力,如果对主框架不甚理解,有可能会造成很多隐藏的Bug,且不易修复,一旦崩溃对整个系统的影响很大,那么怎…

【从零开始一步步学习VSOA开发】VSOA数据流

VSOA数据流 概念 实际业务中常常存在既有实时命令通信&#xff0c;又有非实时的大数据通信&#xff0c;如文件、音视频传输服务等&#xff0c;如果使用常规的 RPC 或订阅/发布功能来实现&#xff0c;将实时命令和大数据传输混在一起&#xff0c;则会影响 RPC 通道响应的实时性…

C语言程序设计-[10] for语句循环结构

1、for语句循环结构定义 for语句循环结构的一般形式、流程图和执行过程如下&#xff1a; ​ 注1&#xff1a;计算表达式2是循环的判定表达式。与前面一样&#xff0c;这个表达式可以是任意的&#xff0c;只要有值就行&#xff0c;遵循非0即真的原则。 注2&#xff1a;一个循…

(源码)Springboot项目集成Activiti工作流,前端Vue,Bpmn.js

前言 activiti工作流引擎项目&#xff0c;企业erp、oa、hr、crm等企事业办公系统轻松落地&#xff0c;一套完整并且实际运用在多套项目中的案例&#xff0c;满足日常业务流程审批需求。 一、项目形式 springbootvueactiviti集成了activiti在线编辑器&#xff0c;流行的前后端…

【Python】nn.nn.CircularPad1、2、3d函数和nn.ConstantPad1、2、3d函数详解和示例

前言 在深度学习中&#xff0c;尤其是在处理图像、音频或其他多维数据时&#xff0c;数据填充&#xff08;Padding&#xff09;是一个常见的操作。填充不仅可以保持数据的空间维度&#xff0c;还能在卷积操作中避免信息丢失。PyTorch提供了多种填充方式&#xff0c;其中nn.Cir…

unity 本地使用Json(全套)

提示&#xff1a;文章有错误的地方&#xff0c;还望诸位大神不吝指教&#xff01; 文章目录 前言一、Json是什么&#xff1f;二、创建Json文件1.在线编辑并转实体类&#xff08;C#&#xff09;2.Json文件 三、解析Json并使用四、报错&#xff1a;JsonError&#xff1a;JsonExce…

使用frp内网穿透将个人主机上的MySQL发布到公网上,再通过python管理MySQL

目录 1.frp内网穿透部署 1.frp服务器 1.开放端口 2.上传软件包 3.解压 4.配置文件 2.frp客户端 1.上传软件包 2.配置文件 3.启动测试 1.浏览器查看服务器上连接的客户端数量 2.启动测试 2.MySQL安装 3.python3的安装使用 4.python管理MySQL 1.pip 2.pandas 3.p…

Axure 变量魔法:揭秘局部与全局的动态协同

前言 在 Axure 的世界中&#xff0c;变量是连接设计者意图与用户行为的桥梁。 局部变量&#xff0c;以其独特的灵活性和针对性&#xff0c;允许我们在特定情境下快速响应用户的操作。 而全局变量&#xff0c;则以其广泛的覆盖范围&#xff0c;为跨页面的一致性和连贯性提供了…

003集——C#数据类型 及大小端序转换——C#学习笔记

如需得到一个类型或一个变量在特定平台上的准确尺寸&#xff0c;可以使用 sizeof 方法。表达式 sizeof(type) 产生以字节为单位存储对象或类型的存储尺寸。下面举例获取任何机器上 int 类型的存储尺寸&#xff1a; using System;namespace DataTypeApplication {class Program{…

第26课 Scratch入门篇:乘坐公交车

乘坐公交车 故事背景&#xff1a; 又是一天结束了&#xff0c;在繁忙的城市里&#xff0c;深夜加班的上班族们挤上最后一班公交车&#xff0c;回到自己温馨的家 程序原理&#xff1a; 这节课最大的难度就是角色的设计以及角色的切换&#xff0c;背景的不停移动其实跟“猫咪跑…

论MATLAB强大的容错性

如何看待“低代码”开发平台的兴起&#xff1f; “低代码”让非专业人士也能快速构建应用程序。这种新兴技术正在挑战传统软件开发模式&#xff0c;引发了IT行业的广泛讨论。低代码平台是提高效率的利器&#xff0c;还是降低了编程门槛导致质量下降&#xff1f;它会改变开发者…

【报错解决】MySQL报错:sql_mode=only_full_group_by

文章目录 报错信息DataGrip 报错还原Navicat 报错还原 报错原因解决方案查看当前 sql mode方案一&#xff1a;临时解决方案二&#xff1a;永久解决方案三&#xff1a;使用 any_value() 或 group_concat()方案四&#xff1a;调整实现思路&#xff0c;避开 GROUP BY 使用 我是一名…

赛博朋克未来的第一个创想,低空飞行走近现实

英特尔创始人Andy Grove曾在《Only the Paranoid Survive》&#xff08;只有偏执狂才能生存&#xff09;这本书中提到一个观点&#xff1a;战略拐点往往发生在一个竞争因素&#xff0c;或者多个竞争因素突然变成原来10倍的时候&#xff0c;这时候往往预示着生意本质已经发生改变…

基于大数据的气象数据分析与可视化系统设计与实现【爬虫海量数据,LSTM预测】

文章目录 有需要本项目的代码或文档以及全部资源&#xff0c;或者部署调试可以私信博主项目介绍研究目的研究意义研究思路可视化展示每文一语 有需要本项目的代码或文档以及全部资源&#xff0c;或者部署调试可以私信博主 项目介绍 本课题主要针对气象数据进行分析以及可视化…

【CPP】slt-list由认识到简化模拟实现深度理解~

关于我&#xff1a; 睡觉待开机&#xff1a;个人主页 个人专栏: 《优选算法》《C语言》《CPP》 生活的理想&#xff0c;就是为了理想的生活! 作者留言 PDF版免费提供&#xff1a;倘若有需要&#xff0c;想拿我写的博客进行学习和交流&#xff0c;可以私信我将免费提供PDF版。…

大数据信用报告查询哪家平台的比较好?

相信在搜索大数据信用的你&#xff0c;已经因为大数据信用不好受到了挫折&#xff0c;想详细了解一下自己的大数据信用&#xff0c;但是找遍了网络上的平台之后才发现&#xff0c;很多平台都只提供查询服务&#xff0c;想要找一个专业的平台查询和讲解很困难。下面本文就为大家…

【MongoDB】2.MongoDB导入文件

目录 一、MongoDB Compass 二、mongoimport 1、安装 2、语法&#xff1a; 3、可能出现的错误 三、MongoDB的GridFS 1、介绍 2、语法 一、MongoDB Compass 这个简单&#xff0c;不做赘述 二、mongoimport Mongoimport是一个用于导入数据到MongoDB的工具&#xff0c;默…