线程池源码解析 1.前导_FutureTask源码解析

news2025/7/12 15:09:48

线程池—FutureTask源码解析

简介

  • 在学习线程池之前,需要先学习一下 FutureTask,因为线程池 submit 的结果就是 FutureTask。

  • 那么什么是 FutureTask,其表示一个未来的任务。也就是说这个任务比较耗时,当前调用线程会阻塞拿到这个结果。

  • FutureTask 接口继承体系

    image-20221016191736029

  • FutureTask 实现了 Future 和 Runnable 接口,又能执行,又能拿到执行结果。

  • 所有的通过 submit 方式提交到线程池的任务(Runnable、Callable),进入到队列或被线程执行之前,都会被封装成 FutureTask,然后提交到线程池中。

Future 接口

  • Future 接口表示异步计算的结果,通过 Future 接口提供的方法,可以很方便的查询异步计算任务是否执行完成,获取异步计算的结果,取消未执行的异步任务,或者中断异步任务的执行,接口定义如下:

    public interface Future<V> {
        
        boolean cancel(boolean mayInterruptIfRunning);
        
        boolean isCancelled();
        
        boolean isDone();
        
        V get() throws InterruptedException, ExecutionException;
        
        V get(long timeout, TimeUnit unit)
                throws InterruptedException, ExecutionException, TimeoutException;
    }
    

Runnable 接口,就不做过多的解释了。

FutureTask 的使用场景

  • FutureTask 可用于异步获取执行结果或取消执行任务的场景。通过传入 Runnable 或者 Callable 的任务给 FutureTask,直接调用其 run 方法或者放入线程池执行,之后可以在外部通过 FutureTask 的 get 方法异步获取执行结果,因此,FutureTask非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。另外,FutureTask 还可以确保即使调用了多次 run 方法,它都只会执行一次 Runnable 或者 Callable 任务,或者通过 cancel 取消 FutureTask 的执行等。
  • 关于 FutureTask 的基本使用可以参考文章:FutureTask的用法

在 JDK1.5 时,增加了一种开启线程的方式,实现 Callable 接口,这种开启线程的方式可以拿到返回值并且可以捕获异常。但是我们必须配合着FutureTask 类使用。FutureTask 类上就是 Runnable 接口的一个实现类。因为最终开启线程还是得通过new Thread().start()的方式开启线程。

线程池返回的 Future

  • 简单的线程池案例

    public class FutureTaskDemo {
        public static void main(String[] args) throws Exception{
            ExecutorService threadPool = Executors.newFixedThreadPool(5);
            Future<?> future = threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("FutureTaskDemo.run");
                }
            });
            future.get();
        }
    }
    
  • 我们可以看到线程池返回的 Future 是 FutureTask

    image.png

开启线程的简单方式

public static void main(String[] args) throws ExecutionException, InterruptedException {

    // 匿名类的方式构造一个Callable的实现类
    Callable<Integer> callable = new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            return 1;
        }
    };

    // 将Callable的实现类传入
    FutureTask<Integer> task = new FutureTask<>(callable);

    // FutureTask就是Runnable的实现类,直接传到构造函数中即可
    Thread thread = new Thread(task);

    // 开启线程
    thread.start();

    // 阻塞拿到返回值
    int x = task.get();

    System.out.println(x);
}

接下来我们来分析一下 FutureTask 的源码,为接下来的线程池源码解析打好基础!

FutureTask 源码

属性

public class FutureTask<V> implements RunnableFuture<V> {
    
    // 表示当前task任务的状态 
    private volatile int state;
    
    // 新建状态,当前任务尚未执行	
	private static final int NEW          = 0;
    
    // 当前任务正在结束,尚未完全结束,一种临界状态
    private static final int COMPLETING   = 1;
    
    // 当前任务正常结束
    private static final int NORMAL       = 2;
    
    // 当前任务执行过程中发生了异常,内部封装的callable.run() 向上抛出了异常
    private static final int EXCEPTIONAL  = 3;
    
    // 当前任务被取消
    private static final int CANCELLED    = 4;
    
    // 当前任务中断中
    private static final int INTERRUPTING = 5;
    
    // 当前任务已中断
    private static final int INTERRUPTED  = 6; 
    
    /*
     * 线程池调用submit()方法时将Runnable变为Callable(适配器模式)
     */
    private Callable<V> callable;
    
    /*
     * 正常情况下:任务执行结束,outcome保存执行结果(callable的返回值)
     * 非正常情况:callable向上抛出异常,outcome保存异常
     */
    private Object outcome; 
    
    // 当前任务被线程执行期间,保存当前执行任务的线程引用
    private volatile Thread runner;
    
    // 因为会有很多线程去get当前任务的结果,所以这里使用了一种Stack的数据结构来存储线程
    private volatile WaitNode waiters;
    
    // WaitNode节点
    static final class WaitNode {
        // 引用当前线程
        volatile Thread thread;
        // 下一个节点
        volatile WaitNode next;
        WaitNode() { thread = Thread.currentThread(); }
    }

构造方法

  • 传入一个 Callable 接口的实现类

        public FutureTask(Callable<V> callable) {
            if (callable == null)
                throw new NullPointerException();
            // 为callable属性赋值 这里的callable就是程序员自己实现的业务类
            this.callable = callable;
            // 设置当前任务状态为NEW(0)
            this.state = NEW;       
        }
    
  • 传入一个 Runnable 接口的实现类

        public FutureTask(Runnable runnable, V result) {
            /*
             * 使用适配器模式将runnable转换为了callable 外部线程 通过get获取当前任务执行结果时,结果可能为 null 也可能为 传进来的值
             * (submit的重载方法中有一个只传入Runnable的方法,而它的result就为null:RunnableFuture<Void> ftask = newTaskFor(task, null);)
             * 底层实际上就是创建了Callable接口的实现类,在call方法里调用了传的runnable的run方法,然后将我们传入的result原生不动的返回了
             * RunnableAdapter 通过将 Runnable 组合到自身内部,再实现 Callable 接口来完成从 Runnable 到 Callable 的适配
             */
            // 为内部的callable属性赋值
            this.callable = Executors.callable(runnable, result);
            // 设置任务为初始状态
            this.state = NEW;
        }
    //                                  ||
    //                                  ||
    //                                  \/
       public static <T> Callable<T> callable(Runnable task, T result) {
            if (task == null)
                throw new NullPointerException();
            return new RunnableAdapter<T>(task, result);
        }
    //                         ||
    //                         ||
    //                         \/
        static final class RunnableAdapter<T> implements Callable<T> {
            final Runnable task;
            final T result;
            RunnableAdapter(Runnable task, T result) {
                this.task = task;
                this.result = result;
            }
            public T call() {
                // 直接运行了run方法
                task.run();
                // 直接将传来的result返回了 传什么返回什么
                return result;
            }
        }
    
  • 总结

    FutureTask 的构造器主要就是传入一个 Callable 或者 Runnable 的实现类,为内部的 callable 属性赋值,当我们传入的是 Runnable 时,会将我们的 Runnable 通过适配器模式进行包装,包装成一个 Callable,返回值我们自定义(返回值没有意义)。

run()

大致流程解析

  1. 先判断任务的状态是否处于 NEW 的状态,不处于 NEW 的状态直接返回不执行。
  2. 当前线程尝试使用 CAS修改内部的runner的引用 的方式去执行这个任务,CAS 失败说明其他线程抢到了这个任务,当前线程也直接返回。
  3. 当前线程如果尝试 CAS修改runner指向为当前线程 成功就会去执行任务。
  4. 如果任务正常执行(没有出现异常),会拿到执行结果,并且调用 set() 方法,将结果设置到 FutureTask 内部的 outcome 属性中,并将线程状态修改为NORMAL(正常结束),set() 方法调用 finishCompletion() 方法将所有的读线程全部唤醒,最后将 FutureTask 内部的 callable 置为 null。
  5. 如果任务执行过程中出现了异常,首先将执行结果置为 null,然后调用 setException() 方法,先将异常信息设置到 outcome 中,然后将任务状态设置为 EXCEPTIONAL,最终调用 finishCompletion 方法将读线程全部唤醒。

img

    // submit(runnable/callable) -> newTaskFor(runnable) -> execute(task) -> pool
    // 任务执行入口
	public void run() {
        /* 
         * 1.先判断state是否处于NEW状态,非NEW状态的任务线程都不会执行
         * 2.尝试使用CAS更新内部的runner(Thread)属性为当前线程,CAS失败表示有其他线程竞争这个任务 
         */
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        /*
         * 执行到这里表示当前任务一定是NEW状态 并且 当前线程也抢占任务成功
         */
        try {
            // 内部的callable为c赋值 就是程序员自己封装逻辑的callable 或者 装饰后的runnable
            Callable<V> c = callable;
            /*
             * 条件1:c != null 防止空指针异常
             * 条件2:state == NEW 防止外部线程 cancel掉当前任务
             */
            if (c != null && state == NEW) {
                // 保留结果的引用
                V result;
                // true  表示callable.run 代码块执行成功 未抛出异常
                // false 表示callable.run 代码块执行失败 抛出异常
                boolean ran;
                try {
                    // 调用程序员自定义的callable或者适配之后的runnable拿到结果
                    result = c.call();
                    // c.call未抛出任何异常,ran会设置为true 代码块执行成功
                    ran = true;
                } catch (Throwable ex) {
                    // 说明程序员自己写的逻辑块有bug了 抛出异常 将结果置为null
                    result = null;
                    // ran置为false
                    ran = false;
                    // 后面讲
                    // 非正常情况下 有异常 会执行这个set方法
                    setException(ex);
                }
                // 运行成功
                if (ran)
                    // 说明当前c.call正常执行结束了
                    // set就是用CAS方式设置任务的状态并将结果设置到outcome中
                    set(result);
            }
        } finally {
            // 将当前线程的引用runner置为null
            runner = null;
            // 关于中断..后面讲
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

set()

    /*
     * 使用CAS的方式设置任务状态并将结果写入到outcome中
     */ 
	protected void set(V v) {
        // 使用CAS方式设置当前任务状态为 COMPLETING(1)完成中..
        // 有没有可能失败呢?外部线程等不及了,直接在set执行CAS之前 将task取消了(很小概率事件)
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            // 结果赋值给outcome
            outcome = v;
            // 将结果赋值给outcome之后,马上会将任务状态修改为NORMAL(2)正常结束状态(非cAS,直接操作内存进行写的过程)
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            // 后面讲
            // 猜一猜?最起码得把get()在此阻塞的线程 唤醒..
            finishCompletion();
        }
    }

setException()

    protected void setException(Throwable t) {
        /*
         * 使用CAS方式设置当前任务状态为 COMPLETING(1)完成中..
         */
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            // 这里的t引用的就是 callable 向上层抛出来的异常,赋值给outcome
            outcome = t;
            // 修改任务状态为EXCEPTIONAL(3),即出现异常的状态
            UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL);
            // 讲完get()再说
            finishCompletion();
        }
    }

get()

大致流程

  • 先获取任务的状态 state,如果说任务处于NEW 或者 COMPLETING(未完全完成),会进入 awaitDone() 方法进行挂起。否则说明任务已完成,调用 report() 方法,取出 run 方法完成时向 outcome 写入的结果或者异常信息进行返回,可能抛出异常。
  /*
   * 场景:多个线程等待当前任务执行完成后的结果
   */
	public V get() throws InterruptedException, ExecutionException {
    	int s = state;
        /*
         * 条件成立:当前任务未执行、正在执行、正完成 
         * 调用get()的外部线程(非runner线程)会被阻塞在get()方法上
         */
        if (s <= COMPLETING)
            // 返回当前任务的状态(可能当前线程已经在里面睡了一会了..),如果内部抛出中断异常,get()方法也会向上抛
            // awaitDone 实现阻塞的方法 FutureTask最核心的地方
            s = awaitDone(false, 0L);
        // 返回结果
        return report(s);
    }

awaitDone()

整个方法是一个自旋

(下面的第一次第二次第三次的前提条件时任务状态没有完成并且不处于接近完成时)

  1. 第一次进入会将当前线程构造成一个 WaitNode 节点。
  2. 第二次进入会将构造出来的节点入栈(头插头出的队列)
  3. 第三次进入会调用 LockSupport 当前线程挂起
  4. 当当前线程被唤醒时,首先会判断唤醒的原因是否是因为其他线程对当前线程进行了中断操作,如果是的话,就将当前线程对应的WaitNode节点出栈,然后抛出中断异常。
  5. 判断当任务状态处于接近完成的状态,那么就释放 CPU,然后进入下一次的抢占过程中。
  6. 当任务处于已完成时,将 WaitNode 中的线程属性设置为 null,最终返回任务的最终状态state
 	 /*
	  * 返回值是任务的状态
	  */
     private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
        // 假设此次是不带超时的方法 拿到的时间为0
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        // 引用当前线程 封装成WaitNode对象
        WaitNode q = null;
        // 表示当前线程WaitNode对象有没有入队/压栈
        boolean queued = false;

        // 自旋
        for (;;) {
            /*
             * 进入这个if 说明当前线程已经被阻塞后唤醒了,并且是被其他线程使用中断的方式唤醒的, 
             * interrupted()返回true后会将Thread的中断标记重置为false(即下一次再调用此方法时就会返回false)
             */
            if (Thread.interrupted()) {
                // 将Node节点出栈
                removeWaiter(q);
                // get方法抛出中断异常
                throw new InterruptedException();
            }
            /*
             * 线程被阻塞(挂起)后,被其他线程使用unpark()正常唤醒,会正常自旋,走下面逻辑
             */ 
            // 读取当前任务的最新状态
            int s = state;
            // 条件成立:表示当前任务已经有结果了.. 可能是好 可能是坏
            if (s > COMPLETING) {
            	// 条件成立:说明已经为当前线程创建过node了,此时需要将node.thread = null
                if (q != null)
                    q.thread = null; // help GC
                return s; // 返回当前状态
            }
       		// 条件成立:说明任务接近完成状态,这里让当前线程释放CPU,进入下一次抢占CPU的状态
            else if (s == COMPLETING) // cannot time out yet
                Thread.yield();
            /*
             * 第一次自旋:当前线程还未创建WaitNode对象,此时根据当前线程封装一个WaitNode对象
             */ 	
            else if (q == null)
                q = new WaitNode();
            /*
             * 第二次自旋:WaitNode对象已经创建,但是node对象还未入队
             */
            else if (!queued)
                // q.next = waiters 当前线程node节点 next指向原队列的头节点(waiters一直指向队列的头)
                // CAS方式设置waiters引用指向 当前线程node(队列头结点),成功的话 queued == true 否则,可能其它线程先你一步入队了
       		    queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q);
            /*
             * 第三次自旋:就是直接挂起当前线程
             */
            // 带有超时的挂起
            else if (timed) {
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    removeWaiter(q);
                    return state;
                }
                LockSupport.parkNanos(this, nanos);
            }
            // 不带超时的挂起
            else
                // 当前get()操作的线程就会被park掉(阻塞) 线程状态变为WAITING状态 相当于休眠了
                // 除非有其他线程将你唤醒 或者 将当前线程中断
                LockSupport.park(this);
        }
    }

removeWaiter()

    private void removeWaiter(WaitNode node) {
        if (node != null) {
            // 将node内部的thread置为null
            node.thread = null; 
            
            // 自旋
            retry:
            for (;;) {
                /*
                 * 遍历链表,将指定节点删除,可能有些节点因为中断没有出队,但是内部的thread被设置为null了(见上面的方法)
                 * 也将这些节点一起出队
                 */
                /*
                 * pred 当前节点的前一个节点
                 * q    当前节点
                 * s    当前节点的下一个节点
                 * 循环条件为:当前节点不为空
                 * 每次将q指向下一位
                 */
                for (WaitNode pred = null, q = waiters, s; q != null; q = s) {
                    s = q.next; // 赋值
                    if (q.thread != null)
                        pred = q; // pred指向q 开始下一轮循环
                    // 前置条件:当前节点为null
                    // 条件成立:说明当前节点不是头结点
                    else if (pred != null) {
                        // 将前置节点的指针指向下一个节点 相当于删除当前节点
                        pred.next = s;
                        // 如果前置节点此时也为空 直接continue再次进行自旋
                        if (pred.thread == null) // check for race
                            continue retry;
                    }
                    // 说明当前节点是头结点 进行CAS替换操作 将waiters当前节点的下一个节点
                    else if (!UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                          q, s))
                        continue retry;
                }
                break; // 全部处理完则退出自旋
            }
        }
    }

report()

  • 去获取 run() 方法结束后存储在 outcome 中的结果并返回
    /*
     * 处理 get 方法的返回结果
     */
	private V report(int s) throws ExecutionException {
        /*
         * 正常情况下,outcome保存的是callable运行结束的结果
         * 非正常情况下,outcome保存的是callable抛出的异常对象
         */
        Object x = outcome;
        // 条件成立:当前任务状态正常结束
        if (s == NORMAL)
            // 直接返回callable运算结果
            return (V)x;
        // 任务被结束
        if (s >= CANCELLED)
            // 抛出取消异常
            throw new CancellationException();
        // 执行到这,说明callable接口实现中,是有bug的.. 抛出异常
        throw new ExecutionException((Throwable)x);
    }

finishCompletion()

  • 大致流程就是遍历队列中的所有 WaitNode 节点,将所有等待的线程全部唤醒,并将所有的 WaitNode 设置为null。
	/*
     * 在 set 方法中调用该方法
     */
	private void finishCompletion() {
        // q指向waiters链表的头结点
        for (WaitNode q; (q = waiters) != null;) {
            /*
             * 使用CAS设置waiters为null,是因为怕外部线程使用cancel取消当前任务也会触发finishCompletion(小概率事件)
             */
            if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
                // 自旋
                for (;;) {
                    // 获取当前node节点封装的thread
                    Thread t = q.thread;
                    // 条件成立,说明当前线程不为null
                    if (t != null) {
                        q.thread = null; // help GC
                        // 唤醒当前节点对应的线程
                        LockSupport.unpark(t); 
                    }
                    // next 当前节点的下一个节点
                    WaitNode next = q.next;
                    // 说明当前的节点时最后一个节点
                    if (next == null)
                        break; // 说明都唤醒完成 直接退出自旋
                    q.next = null; // help gc
                    q = next; // 指向下一个节点
                }
                break; // 此时也说明全部处理完成 跳出最外层自旋
            }
        }
    	// done接口 里面什么都没有实现 是一个可以进行扩展的方法
        done();
        // 将callable设置为null help GC
        callable = null;        // to reduce footprint
    }

cancel()

  • cancel 方法也会调用 finishCompletion 方法。
    /* 
     * 如果通过线程池submit提交了任务之后,这个任务迟迟没有结束,这时在外部可以通过future句柄调用它的 cancel 方法,尝试中断/取消当前任务
     * @param:mayInterruptIfRunning
     * 		   true->如果当前任务处于正在运行状态,会给线程发一个中断信号
     *         false->直接改变当线程状态
     * @return:cancel成功则返回true 否则返回false
     */
	public boolean cancel(boolean mayInterruptIfRunning) {
        /* 
         * 条件1:state == NEW 成立,表示当前任务处于运行中 或者 处于线程池 任务队列中..
         * 条件2:UNSAFE.compareAndSwapInt(this, stateOffset, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED))
         * CAS操作 尝试修改线程状态为中断或者是取消(看传来的参数)
         * 条件成立:说明修改状态成功,可以去执行下面逻辑了,否则 返回false 表示cancel失败
         */
        if (!(state == NEW &&
              UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                  mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
            return false;
        try {    
            // true,将线程设置为中断状态
            if (mayInterruptIfRunning) {
                try {
                    // 执行任务的线程 有可能现在是null
                    // null的情况就是:当前任务还在队列中,还没有线程去获取它
                    Thread t = runner;
                    // 条件成立:说明当前线程runner正在执行task
                    if (t != null)
                        /*
                         * 调用线程的中断方法,如果程序是响应中断的话,会走中断逻辑
                         * 假设程序不是响应中断的,那么啥也不会发生
                         */
                        t.interrupt();
                } finally { // final state
                    // 中断完成后将状态改为中断完成
                    UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
                }
            }
        } finally {
            // 调用 finishCompletion() 完成线程 会唤醒所有get阻塞的线程
            finishCompletion();
        }
        return true;
    }

参考

  • 视频参考
    • b站_小刘讲源码公开课【面试必问】硬核手撕Java线程池FutureTask源码
  • 文章参考
    • shstart7_线程池源码解析1.FutureTask源码解析
    • 兴趣使然的草帽路飞_线程池源码分析_01 FutureTask源码分析
    • 肆华_FutureTask阅读理解

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

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

相关文章

某程序员发现 CSDN官方“漏洞”,立省¥10000+,抓紧薅吧

是一个省钱的组织&#xff01;&#xff01;&#xff01;它叫做勤学会&#xff0c;CSDN 官方背书。 打开这篇博客&#xff0c;你首先就要面对一个问题。 勤学会到底是什么&#xff1f;活动&#xff1f;社区&#xff1f;还是一个名词。 今天这篇博客就从解释【勤学会】这三个字开…

【Wins+VSCode】配置C++开发环境

目录1、安装vscode2、安装中文包和c扩展包3、安装c编译工具&#xff1a;g4、运行代码测试5、lauch.json6、tasks.json7、问题7.1、找不到gcc任务Reference1、安装vscode 官网下载就可以了&#xff0c;免费的&#xff1a; https://code.visualstudio.com/ 2、安装中文包和c扩展…

【Java项目】讲讲我用Java爬虫获取LOL英雄数据与图片(附源码)

&#x1f680;用Java爬一下英雄联盟数据 &#x1f4d3;推荐网站(不断完善中)&#xff1a;个人博客 &#x1f4cc;个人主页&#xff1a;个人主页 &#x1f449;相关专栏&#xff1a;CSDN专栏 &#x1f3dd;立志赚钱&#xff0c;干活想躺&#xff0c;瞎分享的摸鱼工程师一枚 &…

【Asesprite】快速自制Tileset瓦片地图集(俯视角)

使用Aseprite软件完成一个Tileset素材的制作&#xff0c;用于2D游戏开发。 目录 一、基础配置 二、草地和泥土 三、导出为TileSet素材 一、基础配置 1.创建一个96x48的画布。 2.在菜单中选择View-》GridSettings。 3.设置网格宽高为16x16。 4.点击View-》Show-》Grid显…

IOS手机和车机互联自动化测试

在酷热的夏天&#xff0c;提前打开空调&#xff1b;在寒冷的冬天&#xff0c;提前加热座椅和方向盘。这些贴心的功能都是通过手机远程控制汽车实现的。随着汽车新四化的进程推进&#xff0c;类似手机和车机连接的功能必然越来越多。 作为汽车行业的工程师&#xff0c;我们都知道…

Jenkins 忘记登录密码解决办法

今天给大家分享下 jenkins 登录密码忘记的解决办法&#xff0c;方法不唯一&#xff0c;都能解决问题&#xff0c;按照自己的习惯来做更好。 1、先停止 jenkins 服务 systemctl stop jenkins 此步骤可以结合 ps -ef | grep jenkins 和 kill -9 jenkins进程号 一起解决2、找到…

简单实用的Python图像处理库Pillow

Pillow图像处理Pillow 库的安装图象处理基本知识图像的RGB 色彩模式像素阵列Image 模块打开和新建混合透明度混合处理遮罩混合处理复制和缩放复制图像缩放像素缩放图像粘贴和裁剪粘贴裁剪图像图像旋转格式转换covert()transpose()分离和合并分离合并滤镜其他内置函数ImageFilte…

A Blockchain-Driven IIoT Traffic Classification Service for Edge Computing 论文学习

A Blockchain-Driven IIoT Traffic Classification Service for Edge Computing IEEE INTERNET OF THINGS JOURNAL&#xff0c;2021 文章目录A Blockchain-Driven IIoT Traffic Classification Service for Edge ComputingSummaryResearch Objective(s)Background / Problem S…

用Canvas绘制一个数字键盘

Hello啊老铁们&#xff0c;这篇文章还是阐述自定义View相关的内容&#xff0c;用Canvas轻轻松松搞一个数字键盘&#xff0c;本身没什么难度&#xff0c;这种效果实现的方式也是多种多样&#xff0c;这篇只是其中的一种&#xff0c;要说本篇有什么特别之处&#xff0c;可能就是纯…

OpenGL 灰度图

目录 一.OpenGL 灰度图 1.IOS Object-C 版本1.Windows OpenGL ES 版本2.Windows OpenGL 版本 二.OpenGL 灰度图 GLSL Shader三.猜你喜欢 零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >> OpenGL ES 基础 零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 &…

振弦采集模块AABB 通讯协议

振弦采集模块AABB 通讯协议 AABB 通讯协议是一种非标准自定义协议&#xff0c; 相较于 MODBUS 通讯协议&#xff0c;结构更简单&#xff0c;指令生成方法更容易&#xff0c;便于进行快速测试。 AABB 通讯协议支持单寄存器读写两种指令。 &#xff08; 1&#xff09; 读取单个寄…

《Care Bears 爱心熊》人物化身来到 The Sandbox 元宇宙!

无论你想要快乐、和谐、幸运还是温暖&#xff0c;都会有一个适合你的爱心熊人物化身&#xff01;&#x1f43b; 想成为一只爱心熊吗&#xff1f; 《爱心熊》作品集是由 3060 个独特的 The Sandbox 人物化身组成的作品集&#xff0c;可在欢快而多彩的元宇宙世界中玩耍。每个人物…

【Linux】软件包管理器yum和编辑器vim(内附动图)

大家好我是沐曦希&#x1f495; 文章目录1.Linux 软件包管理器 yum1.1 什么是软件包1.2 第一个软件rzsz2.Linux编辑器-vim使用2.1 vim的基本概念2.2 vim的基本操作2.3 命令模式2.3.1 光标定位2.3.2 文本复制2.4 插入模式2.5 底行模式2.5.1 调用和取消行号2.5.2 底行&#xff01…

Node.js | 详解 Cookie-Session登录验证 的工作原理

&#x1f9d1;‍&#x1f4bc; 个人简介&#xff1a;一个不甘平庸的平凡人&#x1f36c; &#x1f5a5;️ 本系列专栏&#xff1a;Node.js从入门到精通 &#x1f449; 你的一键三连是我更新的最大动力❤️&#xff01; &#x1f4e2; 欢迎私信博主加入前端交流群&#x1f339; …

Maven版本3.6.1环境配置安装

官网下载安装包配置maven环境变量配置本地仓库以及阿里云镜像官网下载安装包 下载maven安装包官网地址&#xff0c;解压即可使用&#xff0c;推荐下载apache-maven-3.6.1-bin.zip 配置maven环境变量 找到此电脑右键-->点击属性-->选择高级系统设置-->点击环境变量--&g…

EPICS记录参考--计算输出记录(calcout)

计算输出或"Calcout"记录类似于Calc记录&#xff0c;其增加了能够输出的特性(一个"output link"和一个"output event")&#xff0c;根据计算结果条件地执行它们。这种特性允许在一个EPICS数据库内实现了条件分支(例如&#xff1a;只在Record_B有…

BERT预训练模型学习笔记

1.Transforme 1.1 要做一件什么事 基本组成依旧是机器翻译模型中常见的Seq2Seq网络输入输出都很直观&#xff0c;其核心架构就是中间的网络设计了MxN&#xff0c;输入M&#xff0c;输出N 1.2 传统的RNN网络有什么问题 传统RNN是一个时序模型&#xff0c;下一个RNN的输入依靠…

野火FPGA入门(4):时序逻辑电路

文章目录第11讲&#xff1a;寄存器第12讲&#xff1a;阻塞赋值与非阻塞赋值第13讲&#xff1a;计数器第14讲&#xff1a;分频器&#xff1a;偶分频第15讲&#xff1a;分频器&#xff1a;奇分频第16讲&#xff1a;按键消抖组合逻辑存在竞争冒险 第11讲&#xff1a;寄存器 寄存…

【Debug】关于 nginx 上传文件时出现 413 及 500 错误码解决方法

先简单介绍一下 Nginx…   Nginx 作为一个高性能的 HTTP 和 反向代理 web 服务器具有占用内存少, 并发能力强等特点,可以说 Nginx 专为性能和效率而生, 如 tomcat 的并发量大约在 100 多, 而 Nginx 的并发量可以达到 5 万之多;   Nginx 的主要作用还是反向代理, 实现负载均衡…

什么是扩散模型(Diffusion Model)?

扩散模型是什么&#xff1f;如何工作以及他如何解决实际的问题 在计算机视觉中&#xff0c;生成模型是一类能够生成合成图像的模型&#xff08;文本生成图像【DALL2、Stable Diffusion】、图像生成图像【Diffusion-GAN】&#xff09;。例如&#xff0c;一个被训练来生成人脸的…