【线程池】Java线程池的内部类Worker详解

news2025/1/9 1:33:53

目录

一、简介

二、Worker类对象的类图

三、Worker类对象的解释

4.2 Worker继承自AQS有何意义?

四、Worker的主要代码

4.1 运行worker

4.2 worker和ThreadPool的关系

五、Worker源码分析

5.1 Worker实现接口Runnable,执行run方法

5.2 核心方法 getTask()

5.3 核心方法 processWorkerExit()

5.4 worker的异常处理

六、Worker工作线程的生命周期


一、简介

Worker是ThreadPoolExecutor中的内部类,它其实就可以相当于线程池中存储的线程,用它来执行提交给线程池的任务,但是这个worker并不是一个单纯的线程,而是它的属性中持有一个线程,它还有其他的属性,用来做一些数据统计以及实现一些执行任务相关的功能。

二、Worker类对象的类图

三、Worker类对象的解释

线程池中的线程,都会被封装成一个Worker类对象,ThreadPoolExecutor维护的其实就是一组Worker对象,其中用集合workers存储这些Worker对象;

Worker类中有两个属性,一个是firstTask,用来保存传入线程池中的任务,一个是thread,是在构造Worker对象的时候,利用ThreadFactory来创建的线程,用来处理任务的线程;

Worker继承AQS,使用AQS实现独占锁,并且是不可重入的构造Worker对象的时候,会把锁资源状态设置成-1,因为新增的线程,还没有处理过任务,是不允许被中断的

Worker使用AQS来实现的不可重入锁,固定state只有1个。以下为它的AQS实现:

private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable{
    ....
    
    protected boolean isHeldExclusively() {
        return getState() != 0;
    }
    
    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }
    
    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(); }
    ....
}

也就是说Worker对象本身就有lock()、unlock()、tryLock()、isLocked()等方法可以调用,用来给Worker对象加锁。

lock 方法一旦获取了独占锁,表示当前线程正在执行任务中;那么它会有以下几个作用

  • 如果正在执行任务,则不应该中断线程;
  • 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断;
  • 线程池在执行 shutdown 方法或 tryTerminate 方法时会调用 interruptIdleWorkers 方法来中断空闲的线程,interruptIdleWorkers 方法会使用 tryLock 方法来判断线程池中的线程是否是空闲状态
  • 之所以设置为不可重入,是因为我们不希望任务在调用像 setCorePoolSize 这样的线程池控制方法时重新获取锁,这样会中断正在运行的线程

4.2 Worker继承自AQS有何意义?

前面我们看了Worker内部类的定义,它继承自AQS,天生自带锁的特性,那么,它的锁是用来干什么的呢?跟任务的执行有关系吗?

答:既然是跟锁(同步)有关,说明Worker类跨线程使用了,此时我们查看它的lock()方法发现只在runWorker()方法中使用了,但是其tryLock()却是在interruptIdleWorkers()方法中使用的。

private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

interruptIdleWorkers()方法的意思是中断空闲线程的意思,它只会中断BlockingQueue的poll()或take()方法,而不会中断正在执行的任务。

一般来说,interruptIdleWorkers()方法的调用不是在本工作线程,而是在主线程中调用的,还记得shutdown()和shutdownNow()方法吗?

观察两个方法中中断线程的方法,shutdown()中就是调用了interruptIdleWorkers()方法来中断线程地,而且interruptIdleWorkers()方法中就用到了tryLock(),只有获取到锁了才能中断线程,如果没有获取到锁则不中断。而调用tryLock()后没获取到锁只有一种原因,那就是lock()所在的地方runWorker()方法中,有任务正在执行。这样shutdown()方法就实现了只中断空闲线程,不会中断正在执行任务的线程。

而shutdownNow()中中断线程则很暴力,并没有tryLock(),而是直接中断了线程,所以调用shutdownNow()可能会中断正在执行的任务。

所以,Worker继承自AQS实际是要使用其锁的能力,这个锁主要是用来控制调用shutdown()时不要中断正在执行任务的线程

那么为什么Worker使用AQS实现锁,而不直接用ReentrantLock呢?我们可以看到Worker的tryAcquire 方法,它是不允许重入的,而 ReentrantLock是允许重入的。所以这是为了实现不可重入的特性去反应线程现在的执行状态。

四、Worker的主要代码

private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
	/**
	 * This class will never be serialized, but we provide a
	 * serialVersionUID to suppress a javac warning.
	 */
	private static final long serialVersionUID = 6138294804551838833L;
	
	/** 这个就是worker持有的线程,也就是线程池中的线程 */    
	final Thread thread;
	
	/** 这个就是提交给线程池的任务 */    
	Runnable firstTask;
	
	/** 每一个线程执行的任务数量的计数器 */  
	volatile long completedTasks;
	
	/**
	 * 我们在调用addWorker方法的时候就会调用这个构造方法,有可能是创建新线程并执行任务,那么firstTask就是传给线程池要执行的任务,如果只是了
	 * 单纯的想创建一个线程,只需要传入null就可以
	 */
	Worker(Runnable firstTask) {
	    setState(-1); // inhibit interrupts until runWorker
	    this.firstTask = firstTask;
	        // 这个是通过线程工厂类创建一个线程,也就是给线程池创建一个线程
	    this.thread = getThreadFactory().newThread(this); 
	}
	
	/** Delegates main run loop to outer runWorker  */
	public void run() {
	    runWorker(this);
	}
	
	省略上面已经讲过的利用AQS实现锁的部分......
}

以上就是Worker内部类的源码,所以Worker内部类的实现很简单,没有多少代码,它调用的很多方法其实仍然是ThreadPoolExecutor中的方法。

总体来看,worker其实就是一个Runable,其也是需要构造成一个Thread对象,然后调用Thread.start()方法运行的。只不过在worker的run方法中是定义了一个runWoker的方法。这个方法的主要内容从 for 循环不停的从task队列中获取对应的runable的task,然后同步调用这个task的run()方法其实就是在某个线程中,不停的拿队列中的任务进行执行。

4.1 运行worker

可以看到构造方法内,有一个Thread对象,其使用了ThreadFactory构造了一个新的线程,并且线程的runable是worker本身。

this.thread = getThreadFactory().newThread(this);

所以需要执行worker的时候,只需要调用worker中的thread的start()方法即可,并且可以调用thread的方法来控制worker的状态,例如interrupt等。

只有运行启动了的线程,才能够去执行线程池中的任务。因为线程去获取任务执行的逻辑是定义在Worker的runWorker()方法中的,而runWorker()方法放在了Worker的run()方法中,想要执行run()方法,就需要去调用Worker类中thread对象的start()方法。所以当向线程池中成功添加worker线程后,需要将该worker线程启动才能正常使用该线程去执行任务。

4.2 workerThreadPool的关系

在ThreadPool中是有一个worker集合的。通过这个集合,我们可以知道有多少worker线程在进行工作等,每一个worker都是各自进行工作,工作的内容就是不停的获取task,然后执行task即可。

五、Worker源码分析

下面我们对Worker的一些关键方法源码进行分析,来帮助我们理解Worker的运作原理。下面就是工作线程执行任务时用到的核心方法分析。

5.1 Worker实现接口Runnable,执行run方法

Worker的run()方法中,实际上执行的是runWorker()方法。该方法实际上就是让线程去线程池中拿任务来执行。

runWorker()方法是线程池真正执行任务的地方。

这里要注意,run()方法是Worker类的方法,但是其调用的runWorker()方法是ThreadPoolExecutor中的方法。

java.util.concurrent.ThreadPoolExecutor#runWorker

// 向线程池中添加线程成功,并且启动也成功,则会执行Worker对象的run方法,进入runWorker方法逻辑
final void runWorker(ThreadPoolExecutor.Worker w) {
    // 获取当前线程,其实这个当前线程,就是worker对象持有的线程,从线程池中拿到的任务就是由这个线程执行的
    Thread wt = Thread.currentThread();
    // 在构造Worker对象的时候,会把一个任务添加进Worker对象
    // 因此需要把其作为新增线程的第一个任务来执行
    Runnable task = w.firstTask;
    // 上面已经将该任务拿出来准备进行执行了(将firstTask取出赋值给task),则需要将该worker对象即线程池中的线程对象持有的任务清空
    w.firstTask = null;
    // 将AQS锁资源的状态由-1变成0,运行该线程进行中断 因为在创建的时候将state设为-1了,现在开始执行任务了,也就需要加锁了,所以要把state再重新变为0,这样在后面执行任务的时候才能用来加锁,保证任务在执行过程中不会出现并发异常
    // 解锁
    w.unlock();
    // 用来判断执行任务的过程中,是否出现了异常
    boolean completedAbruptly = true;
    try {
        // 线程池中的线程循环处理线程池中的任务,直到线程池中的所有任务都被处理完后则跳出循环
        while (task != null || (task = getTask()) != null) {  // 这一步的getTask()就说明Worker一直在轮询的从队列中获取任务,getTask()方法将从队列获取到的任务返回,赋值给task
            // 给该worker加锁,一个线程只处理一个任务。注意加锁是给worker线程加锁,不是给任务线程加锁,因为worker线程之前一直在轮询地在队列中取任务,但是当执行任务的时候,为了避免执行任务出现异常,就对其加锁
            w.lock();
            // 线程池是否是STOP状态
            // 如果是,则确保当前线程是中断状态
            // 如果不是,则确保当前线程不是中断状态
            if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                            runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                // 注意这里中断的是当前线程,也就是worker对象持有的线程
                wt.interrupt();
            
            try {
                // 扩展使用,在执行任务的run方法之前执行
                beforeExecute(wt, task);
                // 记录执行任务过程中,出现的异常
                Throwable thrown = null;
                try {
                    // 执行任务的run方法   当前线程环境就是worker对象持有的线程,所以本质就是woker对象在执行task任务的run()方法
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    // 扩展使用,在执行任务的run方法之后执行
                    afterExecute(task, thrown);
                }
            } finally {
                // 执行完任务后,就将任务对象清空
                task = null;
                w.completedTasks++; // 该worker已经完成的任务数+1
                w.unlock();  // 将worker线程地锁释放
            }
        }
        // 正常执行完任务
        completedAbruptly = false;
    } finally {
        // 线程池中所有的任务都处理完后,或者执行任务的过程中出现了异常,就会执行该方法
        processWorkerExit(w, completedAbruptly);
    }
}

这个方法主要做几件事

  1. 如果 task 不为空,则开始执行 task
  2. 如果 task 为空,则通过 getTask()再去取任务,并赋值给 task,如果取到的 Runnable 不为空,则执行该任务
  3. 执行完毕后,通过 while 循环继续 getTask()取任务
  4. 如果 getTask()取到的任务依然是空,那么整个 runWorker()方法执行完毕

这个方法比较简单,如果忽略状态检测和锁的内容,本质就是如果有第一个任务,就先执行之,之后再从任务队列中取任务来执行,获取任务是通过getTask()来进行的。

Worker执行任务流程图:

5.2 核心方法 getTask()

这个方法用来向队列中轮询地尝试获取任务。该方法也是ThreadPoolExecutor中的方法。

这里重要的地方是第二个 if 判断,目的是控制线程池的有效线程数量。

由上文中的分析可以知道,在执行 execute 方法时,如果当前线程池的线程数量超过了 corePoolSize 且小于maximumPoolSize,并且 workQueue 已满时,则可以增加工作线程,但这时如果超时没有获取到任务,也就是 timedOut 为 true 的情况,说明 workQueue 已经为空了,也就说明了当前线程池中不需要那么多线程来执行任务了,可以把多于 corePoolSize 数量的线程销毁掉,保持线程数量在 corePoolSize 即可。

什么时候会销毁?

当然是 runWorker()方法执行完之后,也就是 Worker 中的 run 方法执行完,由 JVM 自动回收。

// 返回任务Runnable
private Runnable getTask() {
    // timedOut表示 记录上一次从队列中获取任务是否超时
    boolean timedOut = false; // Did the last poll() time out?
    // 自旋
    for (;;) {
        // 这一部分是判断线程池状态
        // 获取线程池的状态和线程池中线程数量组成的整形字段,32位
        // 高3位代表线程池的状态,低29位代表线程池中线程的数量
        int c = ctl.get();
        // 获取高3位的值,即线程池的状态
        int rs = runStateOf(c);
        // 如果线程池状态不是Running状态,并且 线程也不是SHUTDOWN状态 或者任务队列已空
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            // 则将线程池中的线程数量减1  就是说该线程已经不是运行状态了,所以要这个worker线程也没有用了,直接将该worker去掉。这个是原子操作
            decrementWorkerCount();
            //返回一个空任务,因为:
            // 1:如果任务队列已空,则想返回任务也没有
            // 2:如果线程池处于STOP或者之上的状态,则线程池不允许再处理任务
            return null;
        }
        // 这一部分是判断线程池有效线程数量
        // 获取低29位的值,即线程池中线程的数量
        int wc = workerCountOf(c);
        // timed是否需要进行超时控制
        // allowCoreThreadTimeOut默认false
        // 当线程池中线程的数量没有达到核心线程数量时,获取任务的时候允许超时  如果将allowCoreThreadTimeOut设为true,那也不允许超时
        // 当线程池中线程的数量超过核心线程数量时,获取任务的时候不允许超时   
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; // 这个很好理解

        // wc > maximumPoolSize的情况是因为可能在此方法执行阶段同时执行了setMaximumPoolSize方法;
        // timed && timedOut 如果为true,表示当前操作需要进行超时控制,并且上次从阻塞队列中获取任务发生了超时
        // 接下来判断,如果有效线程数量大于1,或者阻塞队列是空的,那么尝试将workerCount减1;
        // 如果减1失败,则continue返回重试
        // 如果wc == 1时,也就说明当前线程是线程池中唯一的一个线程了。
        if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        // 如果上面都没问题,就可以获取任务了
        try {
            // 获取任务
            // 如果timed = true ,说明需要做超时控制,则根据keepAliveTime设置的时间内,阻塞等待从队列中获取任务
            // 如果timed = false,说明不需要做超时控制,则阻塞,直到从队列中获取到任务为止
            Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
            // 如果获取到任务,则把任务返回
            if (r != null)
                return r;
            // 执行到这里,说明在允许的时间内,没有获取到任务
            timedOut = true;
        } catch (InterruptedException retry) {
            // 获取任务没有超时,但是出现异常了,将timedOut设置为false
            timedOut = false;
        }
    }
}

注意,这里取任务会根据工作线程的数量判断是使用BlockingQueue的poll(timeout, unit)方法还是take()方法。

poll(timeout, unit)方法会在超时时返回null,如果timeout<=0,队列为空时直接返回null。

take()方法会一直阻塞直到取到任务或抛出中断异常。

所以,如果keepAliveTime设置为0,当任务队列为空时,非核心线程取不出来任务,会立即结束其生命周期。

默认情况下,是不允许核心线程超时的,但是可以通过下面这个方法设置使核心线程也可超时:

public void allowCoreThreadTimeOut(boolean value) {
    if (value && keepAliveTime <= 0)
        throw new IllegalArgumentException("Core threads must have nonzero keep alive times");
    if (value != allowCoreThreadTimeOut) {
        allowCoreThreadTimeOut = value;
        if (value)
            // 中断空闲的线程
            interruptIdleWorkers();
    }
}

getTask()方法返回 null 时,在 runWorker()方法中会跳出 while 循环,然后会执行processWorkerExit 方法。

5.3 核心方法 processWorkerExit()

runWorker 的 while 循环执行完毕以后,在 finally 中会调用 processWorkerExit()方法,来销毁工作线程。该方法就是判断当前线程是需要将其删除还是继续执行任务。该方法也是ThreadPoolExecutor中的方法。

但这个方法只有在一定情况下才会起作用,如果已经从队列中取不出任务来了,或者在worker执行任务过程中出现了错误,这个方法就起到了作用,如果正常的话这个方法就没啥用。

private void processWorkerExit(ThreadPoolExecutor.Worker w, boolean completedAbruptly) {
    // 如果 completedAbruptly = true ,则线程执行任务的时候出现了异常,需要从线程池中减少一个线程
    // 如果 completedAbruptly = false,则执行getTask方法的时候已经减1,这里无需在进行减1操作
    if (completedAbruptly)
        decrementWorkerCount();
    
    // 获取线程池的锁,因为后面是线程池的操作,为了并发安全,需要对线程池加锁
    final ReentrantLock mainLock = this.mainLock;
    // 线程池加锁
    mainLock.lock();
    try {
        // 统计该线程池完成的任务数
        completedTaskCount += w.completedTasks;
        // 从线程池中移除一个工作线程    works是线程池持有的一个集合  
        workers.remove(w); // 将没用的worker去掉,也就是当前传入的worker
    } finally {
        // 线程池解锁
        mainLock.unlock();
    }
    // 根据线程池的状态,决定是否结束该线程池
    tryTerminate(); // 钩子方法

    // 判断线程池是否需要增加线程
    // 获取线程池的状态
    int c = ctl.get();
    // -当线程池是RUNNING或SHUTDOWN状态时
    // --如果worker是异常结束(即completedAbruptly为false),那么会直接addWorker;
    // ---如果allowCoreThreadTimeOut = true,并且等待队列有任务,至少保留一个worker;
    // ---如果allowCoreThreadTimeOut = false,活跃线程数不少于corePoolSize
    if (runStateLessThan(c, STOP)) { // 线程池状态小于STOP,就说明当前线程池是RUNNING或SHUTDOWN状态
        // 如果worker是异常结束的,不进入下面的分支,直接去addWorker
        if (!completedAbruptly) {
            // 根据allowCoreThreadTimeOut的值,来设置线程池中最少的活跃线程数是0还是corePoolSize
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            // 如果等待队列中有任务,要至少保留一个worker
            if (min == 0 && ! workQueue.isEmpty())
                // 至少保留一个worker
                min = 1;
            // 如果活跃线程数大于等于min,直接返回,不需要再调用addWorker来增加线程池中的线程了
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        // 增加线程池中的worker
        addWorker(null, false);
    }
}

5.4 worker的异常处理

try {
    while (task != null || (task = getTask()) != null) {
        w.lock();
        // If pool is stopping, ensure thread is interrupted;
        // if not, ensure thread is not interrupted.  This
        // requires a recheck in second case to deal with
        // shutdownNow race while clearing interrupt
        if ((runStateAtLeast(ctl.get(), STOP) ||
             (Thread.interrupted() &&
              runStateAtLeast(ctl.get(), STOP))) &&
            !wt.isInterrupted())
            wt.interrupt();
        try {
            beforeExecute(wt, task);
            Throwable thrown = null;
            try {
                task.run();
            } catch (RuntimeException x) {
                thrown = x; throw x;
            } catch (Error x) {
                thrown = x; throw x;
            } catch (Throwable x) {
                thrown = x; throw new Error(x);
            } finally {
                afterExecute(task, thrown);
            }
        } finally {
            task = null;
            w.completedTasks++;
            w.unlock();
        }
    }
    completedAbruptly = false;
} finally {
    processWorkerExit(w, completedAbruptly);
}

通过源代码可以看出,对应的Exception都是保存在thrown中,在finally中交给了afterExecute进行了处理。

所以可以自己实现对应的afterExecute来进行处理系统内部发生的异常问题。

六、Worker工作线程的生命周期

 Worker执行任务的模型如下图所示:


相关文章:【线程池】Java的线程池
                  【线程池】Java线程池的核心参数
                  【线程池】Executors框架创建线程池        
                                  【线程池】ScheduledExecutorService接口和ScheduledThreadPoolExecutor定时任务线程池使用详解                 【线程池】线程池的拒绝策略(饱和策略) 

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

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

相关文章

Mysql主从原理

1.为什么要主从&#xff1f; 什么东西都有丢失或损坏的可能&#xff0c;所以备份是一个重要的手段。 2.备份机制&#xff1f; 对于主从库的分工: "主写从读" 中继日志文件我的理解是一个大的临时区&#xff0c;主库推送过来的数据不用同步进行到从库&#xff0c;这…

零基础学会Python编程——数据也分类:常见数据类型

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 ​编辑 前言 学习目标 一.常见数据类型 1.数字类型 &#xff08;1&#xff09…

Can’t connect to MySQL server on ‘localhost’ (10061)

标题:Can’t connect to MySQL server on ‘localhost’ (10061) 一种情况是&#xff0c;mysql服务没有开启&#xff0c; 解决方式&#xff1a;以管理员身份进入cmd&#xff0c;然后输入net start mysql&#xff0c;注意这里的mysql&#xff0c;指的是你的mysql服务的名称&am…

万字长文,SpringSecurity实现权限系统设计

RBAC权限分析 RBAC 全称为基于角色的权限控制&#xff0c;本段将会从什么是RBAC&#xff0c;模型分类&#xff0c;什么是权限&#xff0c;用户组的使用&#xff0c;实例分析等几个方面阐述RBAC 思维导图 绘制思维导图如下 什么是RBAC RBAC 全称为用户角色权限控制&#xff…

MySQL笔记之一致性视图与MVCC实现

一致性读视图是InnoDB在实现MVCC用到的虚拟结构&#xff0c;用于读提交&#xff08;RC&#xff09;和可重复度&#xff08;RR&#xff09;隔离级别的实现。 一致性视图没有物理结构&#xff0c;主要是在事务执行期间用来定义该事物可以看到什么数据。 一、Read View 事务在正…

怎样高标准地个性化您的电商业务?教你一招搞定

怎样高标准地个性化您的电商业务&#xff1f;教你一招搞定&#xff01; 想要增加销售额和打造一个客户喜爱的品牌&#xff0c;电子商务个性化是您成功的关键。但是&#xff0c;个性化您的在线购物体验的各个方面这个过程简直令人生畏。但是别怕&#xff0c;我将在这里告诉大家利…

计算机 - - - windows电脑搭建FTP局域网文件传输服务器

控制面板&#xff0c;打开程序和功能&#xff0c;启用或关闭Windows功能。 勾选这几个 然后点确定&#xff0c;应用更改成功后重启电脑生效。 打开IIS管理器。 添加ftp站点。 输入ftp站点名称&#xff0c;选择文件存放的物理路径。 ip地址设置为当前电脑的ip地址&#x…

重识Flutter — 探索Slivers的奇妙世界(综合实例)

前言 在前三篇文章中&#xff0c;从为什么要使用Sliver&#xff0c;再根据使用频率逐个解析Slivers系列的组件。相信您已经入门了Sliver的世界。为了更好的将Slivers相关的组件结合起来使用&#xff0c;本文将通过一个综合的案例来帮助你理解。 源代码&#xff1a;https://ww…

【动态规划】LeetCode 583. 两个字符串的删除操作 Java

583. 两个字符串的删除操作 我的代码&#xff0c;错误代码&#xff0c;只考虑到了字母出现的次数&#xff0c;没有考虑到两个字符串中字母出现的顺序 class Solution {public int minDistance(String word1, String word2) {int[] arr1 new int[26];int[] arr2 new int[26];…

Fiddler抓包工具笔记

一、简介 Fiddler代理相当于中介的角色 快捷键ShiftF5去缓存刷新 二、抓包 1. 设置过滤器 没有设置过滤器的话&#xff0c;会抓所有的包&#xff0c;非常乱会混淆 隐藏包含这些内容的URL 2. 快速定位到需要的包 点击&#xff1a;Webforms菜单 界面分析&#xff1a; …

SQlite3 编译

参考博客&#xff1a;https://blog.csdn.net/flowerspring/article/details/121268403 1.下载C源码以及def文件https://www.sqlite.org/download.html 2. 下载完成之后解压 sqlite-amalgamation获取C源码&#xff0c;解压sqlite-dll-win32-xx获取里面的def文件。 3.新建sqlite…

前端vue入门(纯代码)18

不管何时何地&#xff0c;永远保持热爱&#xff0c;永远积极向上&#xff01;&#xff01;&#xff01; 【20.尚硅谷GitHub搜索案例_vue-resource实现】 1.vue-resource vue-resource 是 vue 中一个用于发送请求的插件。 vue 发送请求推荐使用 axios &#xff0c;vue-resourc…

2023上半年软考系统分析师科目一整理-23

2023上半年软考系统分析师科目一整理-23 对于如下所示的序列图所描述的场景&#xff0c;最适合于采用的设计模式是&#xff08;30&#xff09;&#xff1b;该模式适用的场合是&#xff08;31&#xff09;。 A&#xff0e;Visitor B&#xff0e;Strategy C&#xff0e;Observe…

TI AM64x工业核心板规格书(双核ARM Cortex-A53 + 单/四核Cortex-R5F + 单核Cortex-M4F,主频1GHz)

1 核心板简介 创龙科技SOM-TL64x是一款基于TI Sitara系列AM64x双核ARM Cortex-A53 单/四核Cortex-R5F 单核Cortex-M4F设计的多核工业级核心板&#xff0c;通过工业级B2B连接器引出5x TSN Ethernet、9x UART、2x CAN-FD、GPMC、PCIe/USB 3.1等接口。核心板经过专业的PCB Layo…

【C++练习】string:字符串题型训练(5道编程题)

【C练习】string:字符串题型训练 Ⅰ.字符串中的第一个唯一字符Ⅱ.字符串最后一个单词的长度Ⅲ.把字符串转换成整数Ⅳ.字符串相加Ⅴ.反转字符串 Ⅰ.字符串中的第一个唯一字符 解题思路&#xff1a; 第一种方法&#xff1a; 两次遍历 1.第一次遍历&#xff0c;将每个字符出现的次…

【HTTP 协议2】如何构造 HTTP 请求

文章目录 前言一、地址栏输入二、HTML 特殊标签三、form 表单四、ajax总结 前言 各位读者好, 我是小陈, 这是我的个人主页, 希望我的专栏能够帮助到你: &#x1f4d5; JavaSE基础: 基础语法, 类和对象, 封装继承多态, 接口, 综合小练习图书管理系统等 &#x1f4d7; Java数据结…

【Echarts】配置项归纳

【Echarts】配置项归纳 一、title二、legend三、grid四、xAxis/yAxis五、polar六、radiusAxis七、angleAxis八、radar九、dataZoom1. 内置型数据区域缩放组件2. 滑动条型数据区域缩放组件3. 框选型数据区域缩放组件 十、tooltip十一、axisPointer十二、toolbox十三、brush十四、…

不定长图文模型训练

文章目录 生成数据集模型选择计算均值和标准差训练代码测试集测试 生成数据集 import os import random from PIL import Image, ImageDraw, ImageFont, ImageFilter from io import BytesIO import timedef main():_first_num random.randint(1, 1000)_code_style [加, 减,…

【uniapp开发小程序】实现粘贴一段文字后,自动识别到姓名/手机号/收货地址

一、需求 在uni-app中开发小程序&#xff0c;实现粘贴一段文字后自动识别到手机号&#xff0c;并将手机号前面的内容作为姓名&#xff0c;手机号后面的内容作为收货地址&#xff0c;并去除其中的特殊字符和前缀标识。 实现效果&#xff1a; 二、实现方式&#xff1a; <…

【vue】vue.js中引入组件

目录 ⭐️一、点击按钮1弹出弹窗⭐️二、vue.js引入组件具体步骤1、创建自定义组件的文件夹&#xff08;以弹窗组件为例&#xff09;2、在index.vue中引入keyProductsTip.vue模块3、在index.vue中引入组件4、在index.vue中使用组件&#xff0c;点击按钮打开弹窗5、index.vue中的…