线程池ThreadPoolExecutor源码解析

2023-11-11

参考视频

首先回顾一下创建线程等的三种方式

第一个是直接继承Thread类,重写run方法,这个其实内部也是继承了Runnable接口重写run方法。

比如:

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("这是我自己的线程");
    }

    public static void main(String[] args) {
        new MyThread().start();
    }
}

Thread类也是继承Runable接口,内部也是重写了run方法,

public
class Thread implements Runnable 

但是内部的run方法就是直接调用target的run,这个target也就是一个Runable接口的实现类。
@Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

第二种就是直接继承Runnable接口重写Run方法了。
第三种就是重写Callable接口然后用FutureTask进行包装,最后利用Thread进行启动。这种方式创建的线程可以抛出异常并且是有返回值的。

public class MyCallable implements Callable {
    @Override
    public Object call() throws Exception {
        System.out.println("这是call方法,我自己写的");
        return "call function";
    }

    public static void main(String[] args) {
        MyCallable myCallable = new MyCallable();

        try {
            FutureTask futureTask = new FutureTask<>(myCallable);

            new Thread(futureTask).start();
//futureTask.get()这个方法会阻塞当前线程的执行。
            System.out.println(futureTask.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

为什么会出现线程池?

试想一下,如果我们一个比价大型的项目,,每次需要用到多线程都new一个线程,那么这就会导致线程的创建数量我们不好管理和控制,另外创建和销毁线程的开销还是比较大的,甚至有的时候都已经超过了我们这个线程本身执行这个任务所需要的资源耗,这就是非常不合理的了。也有可能会存在线程切换之间的开销过大,资源浪费严重,因此线程池就应运而生了。

我们先看一下这个ThreadPoolExecutor
在这里插入图片描述

这个线程的构造方法中可以传递7个参数,具体的作用和意义这里就不再过多赘述了。这里主要讲解具体这些参数的功能是怎么样实现的?比如为什么线程能够复用,为什么能够保活?

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {

我们在介绍这个类的时候首先先来看一下这个类中的一些属性,避免直接看源码啥也看不懂。

这个ctl是整个线程池的状态控制参数,用这个32位的整数来表示两个状态

  • 第一个就是线程池的运行状态,用最高的三位来表示。
  • 第二个是线程池中的工作线程的数量,用低的29位来表示。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
	//Integer.SIZE - 3 = 29
    private static final int COUNT_BITS = Integer.SIZE - 3;
    //线程池定义的最大工作线程的数量2^29-1
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    // runState is stored in the high-order bits
    // 下面是线程池的运行状态,保存在ctl的高三位。
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;
	//ctlOf其实就是按位异或
	private static int ctlOf(int rs, int wc) { return rs | wc; }

这里顺便介绍一些-1的二进制,我们可以用下面的代码查看-1的二进制

//11111111111111111111111111111111
原码取反变反码
反码加1变成补码
-1的原码:1000 0001
反码:1111 1110
补码:1111 1111-1使用的时候为1111 1111
System.out.println(Integer.toBinaryString(-1));

在这个线程池的类中还有一个属性,这个属性如果为false(默认值),则核心线程即使在空闲时也保持活动状态。如果为true,则核心线程使用keepAliveTime超时等待工作。也就是核心线程也会在超时时间过后被销毁。

private volatile boolean allowCoreThreadTimeOut;

下面我们再介绍几个方法

这个方法是用来计算核心线程数的,调用的时候传递的是ctl的值,有没有发现这里用按位与运算就可以实现,所以上面定义工作线程的最大数量的时候是不是很巧妙?这样运算好像效率更高。

private static int workerCountOf(int c)  { return c & CAPACITY; }

上面这个方法再下面这个方法中被调用,当然,下面这个方法也是线程池中的方法。
源码中的注释:启动核心线程,使其空闲等待工作。这将覆盖仅在执行新任务时启动核心线程的默认策略。
如果所有核心线程都已启动,则此方法将返回false。
返回值:如果启动了线程,则为true

public boolean prestartCoreThread() {
        return workerCountOf(ctl.get()) < corePoolSize &&
            addWorker(null, true);
    }

动态设置核心线程数。
这里

public void setCorePoolSize(int corePoolSize) {
        if (corePoolSize < 0)
            throw new IllegalArgumentException();
        int delta = corePoolSize - this.corePoolSize;
        this.corePoolSize = corePoolSize;
        //这里如果发现设置的核心线程数比原来的核心线程数小就会
        //调用interruptIdleWorkers打断闲置的线程
        if (workerCountOf(ctl.get()) > corePoolSize)
            interruptIdleWorkers();
            //如果这里发现要设置的线程数大于已经存在的核心线程数,那么就会在阻塞队列不为空的情况下
            //不断低循环创建核心线程,直到队列为空或者核心线程数达到要求。
        else if (delta > 0) {
            // We don't really know how many new threads are "needed".
            // As a heuristic, prestart enough new workers (up to new
            // core size) to handle the current number of tasks in
            // queue, but stop if queue becomes empty while doing so.
            int k = Math.min(delta, workQueue.size());
            while (k-- > 0 && addWorker(null, true)) {
                if (workQueue.isEmpty())
                    break;
            }
        }
    }

interruptIdleWorkers内补调用了下面的代码,这里面有个可重入锁,这是线程池类中的一个属性,源码中是这样解释的:
锁定对工人集合和相关簿记的访问。虽然我们可以使用某种类型的并发集合,但事实证明,使用锁通常更可取。其中一个原因是,它序列化了interruptIdleWorkers,这避免了不必要的中断风暴,尤其是在关机期间。否则,退出线程将同时中断那些尚未中断的线程。它还简化了largestPoolSize等的一些相关统计记账。我们还保留mainLock on shutdown和shutdown Now,以确保工人设置稳定,同时分别检查中断和实际中断的权限。

这代码里面有个Worker ,其实就是对工作线程的一个包装,感兴趣的话可以看一下源码。

private final ReentrantLock mainLock = new ReentrantLock();

private void interruptIdleWorkers() {
        interruptIdleWorkers(false);
    }

private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        // 这里首先获取到锁然后对工人workers进行遍历,将所有的工作线程的打断状态设置位true
        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();
        }
    }

下面这个是提供了两个钩子函数,可以在线程池执行某一个线程的前后进行相关的操作

在给定线程中执行给定Runnable之前调用的方法。此方法由将执行任务r的线程t调用,可用于重新初始化
ThreadLocals或执行日志记录。
这个实现什么都不做,但可以在子类中定制。注意:为了正确嵌套多个重写,子类通常应该在这个方法的
末尾调用super.beforeExecute。
参数:
t–将运行任务的线程r–将执行的任务

protected void beforeExecute(Thread t, Runnable r) { }

protected void afterExecute(Runnable r, Throwable t) { }

这两个方法会在Worker 类的run方法中被调用

private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable

public void run() {
            runWorker(this);
        }

我们可以看一下这个方法,我们可以发现这个线程里面不断地调用阻塞队列里面的线程,然后调用它的run方法执行任务,所以实际上没有创建新的线程,你可能比较好奇为这个执行run方法的线程是哪里来的。其实就是addWorker方法创建出来的核心线程来执行的,那为什么核心线程不会start结束呢?为什么核心线程能够复用呢?

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        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);
        }
    }

我们看一下addWorker这个方法,这个方法你会发现会调用了Thread 类的start方法,这里你可能还不明白为什么么,我们看一下前一句的构造方法,w = new Worker(firstTask);

  • Worker(Runnable firstTask) { setState(-1); // inhibit interrupts until runWorker this.firstTask = firstTask; this.thread = getThreadFactory().newThread(this); }

可以看到这个构造方法赋值给Worker类的thread的成员变量就是Worker类自己,因为Worker类实现了Runnable接口,所以这里下面调用的start方法就是调用了上run方法里面的runWorker方法。

private boolean addWorker(Runnable firstTask, boolean core) {
       /******//
       /* 此处省略创建核心线程之前的判断 */
       /*****/

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && 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;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

这里又回到了上面的runWorker方法,为什么不会执行完呢,如果while (task != null || (task = getTask()) != null)这个判断为false了核心线程不就没了吗?所以复用的原理就出来了,我们看一下这个getTask方法,从阻塞队列里面获取任务。这个getTask会不断循环获取,除非当前线程大于核心线程数才会返回null不然就会循环判断。

private Runnable getTask() {
        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.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

测试代码:

package com.dongmu.threadpool;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class CustomThreadPoolExecutor {
    public static void main(String[] args) {


        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 2,
                10, TimeUnit.SECONDS
                , new LinkedBlockingDeque<>(), new CustomThreadFactory(),
                new CustomRejectedExecutionHandler()
        );

        for (int i = 0; i < 10; i++) {
            final int a = i;
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(a);
                }
            });
        }


    }

   static class CustomThreadFactory implements ThreadFactory {
       private AtomicInteger count = new AtomicInteger(0);

       @Override
       public Thread newThread(Runnable r) {
           Thread t = new Thread(r);
           String threadName = CustomThreadPoolExecutor.class.getSimpleName() + count.addAndGet(1);
           System.out.println(threadName);
           t.setName(threadName);
           return t;
       }
   }
    static class CustomRejectedExecutionHandler implements RejectedExecutionHandler {

        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            // 记录异常
            // 报警处理等
            System.out.println("error.............");
        }
}


}


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

线程池ThreadPoolExecutor源码解析 的相关文章

  • 如何在 Spring Data 中选择不同的结果

    我在使用简单的 Spring Data 查询或 Query 或 QueryDSL 在 Spring Data 中构建查询时遇到问题 如何选择三列 研究 国家 登录 不同的行 并且查询结果将是用户对象类型的列表 Table User Id S
  • JDK 文档是语言规范的一部分吗?

    只有一名官员Java语言规范 https docs oracle com javase specs jls se8 html index html所有 Java 实现都必须遵守它 API文档怎么样 所有Java实现都需要遵守吗这个版本 ht
  • 如何将 javax.persistence.Column 定义为 Unsigned TINYINT?

    我正在基于 MySQL 数据库中的现有表创建 Java 持久性实体 Bean 使用 NetBeans IDE 8 0 1 我在这个表中遇到了一个字段 其类型为 无符号 TINYINT 3 我发现可以执行以下操作将列的类型定义为 unsign
  • 通过SOCKS代理连接Kafka

    我有一个在 AWS 上运行的 Kafka 集群 我想用标准连接到集群卡夫卡控制台消费者从我的应用程序服务器 应用程序服务器可以通过 SOCKS 代理访问互联网 无需身份验证 如何告诉 Kafka 客户端通过代理进行连接 我尝试了很多事情 包
  • 如何在 Firebase 远程配置中从 JSON 获取值

    我是 Android 应用开发和 Firebase 的新手 我想知道如何获取存储在 Firebase 远程配置中的 JSONArray 文件中的值 String 和 Int 我使用 Firebase Remote Config 的最终目标是
  • JVisualVM/JConsole 中的 System.gc() 与 GC 按钮

    我目前正在测试处理 XML 模式的概念验证原型 并围绕一个非常消耗内存的树自动机外部库 我已经获得了源代码 构建 我想绘制 真实峰值 堆 随着模式大小的增加 不同运行的内存消耗 使用的指标符合我的目的并且不会影响问题 或者至少是它的合理近似
  • “java.net.MalformedURLException:未找到协议”读取到 html 文件

    我收到一个错误 java net MalformedURLException Protocol not found 我想读取网络上的 HTML 文件 mainfest uses permission android name android
  • 打印星号的 ASCII 菱形

    我的程序打印出这样的钻石 但只有当参数或菱形的每一面为4 例如如果我输入6 底部三角形的间距是错误的 我一直在试图找出答案 当参数改变时 底部的三角形不会改变 只有顶部的三角形会改变 它只适用于输入4 public static void
  • 如何将 Mat (opencv) 转换为 INDArray (DL4J)?

    我希望任何人都可以帮助我解决这个任务 我正在处理一些图像分类并尝试将 OpenCv 3 2 0 和 DL4J 结合起来 我知道DL4J也包含Opencv 但我认为它没什么用 谁能帮我 如何转换成 INDArray 我尝试阅读一些问题here
  • 来自十六进制代码的 Apache POI XSSFColor

    我想将单元格的前景色设置为十六进制代码中的给定颜色 例如 当我尝试将其设置为红色时 style setFillForegroundColor new XSSFColor Color decode FF0000 getIndexed 无论我在
  • 自动生成Flyway的迁移SQL

    当通过 Java 代码添加新模型 字段等时 JPA Hibernate 的自动模式生成是否可以生成新的 Flyway 迁移 捕获自动生成的 SQL 并将其直接保存到新的 Flyway 迁移中 以供审查 编辑 提交到项目存储库 这将很有用 预
  • 如何使用 Hibernate (EntityManager) 或 JPA 调用 Oracle 函数或过程

    我有一个返回 sys refcursor 的 Oracle 函数 当我使用 Hibernate 调用该函数时 出现以下异常 Hibernate call my function org hibernate exception Generic
  • 使用架构注册表对 avro 消息进行 Spring 云合约测试

    我正在查看 spring 文档和 spring github 我可以看到一些非常基本的内容examples https github com spring cloud samples spring cloud contract sample
  • 如何在 Java 中创建接受多个值的单个注释

    我有一个名为 Retention RetentionPolicy SOURCE Target ElementType METHOD public interface JIRA The Key Bug number JIRA referenc
  • 返回 Java 8 中的通用函数接口

    我想写一种函数工厂 它应该是一个函数 以不同的策略作为参数调用一次 它应该返回一个函数 该函数根据参数选择其中一种策略 该参数将由谓词实现 嗯 最好看看condition3为了更好的理解 问题是 它没有编译 我认为因为编译器无法弄清楚函数式
  • Java Swing:需要一个高质量的带有复选框的开发 JTree

    我一直在寻找一个 Tree 实现 其中包含复选框 其中 当您选择一个节点时 树中的所有后继节点都会被自动选择 当您取消选择一个节点时 树中其所有后继节点都会自动取消选择 当已经选择了父节点 并且从其后继之一中删除了选择时 节点颜色将发生变化
  • Android:无法发送http post

    我一直在绞尽脑汁试图弄清楚如何在 Android 中发送 post 方法 这就是我的代码的样子 public class HomeActivity extends Activity implements OnClickListener pr
  • 如何使用play框架上传多个文件?

    我在用play framework 2 1 2 使用java我正在创建视图来上传多个文件 我的代码在这里 form action routes upload up enctype gt multipart form data
  • 将 Apache Camel 执行器指标发送到 Prometheus

    我正在尝试转发 添加 Actuator Camel 指标 actuator camelroutes 将交换 交易数量等指标 发送到 Prometheus Actuator 端点 有没有办法让我配置 Camel 将这些指标添加到 Promet
  • 配置“DataSource”以使用 SSL/TLS 加密连接到 Digital Ocean 上的托管 Postgres 服务器

    我正在尝试托管数据库服务 https www digitalocean com products managed databases on 数字海洋网 https en wikipedia org wiki DigitalOcean 创建了

随机推荐

  • 1019 数字黑洞

    给定任一个各位数字不完全相同的 4 位正整数 如果我们先把 4 个数字按非递增排序 再按非递减排序 然后用第 1 个数字减第 2 个数字 将得到一个新的数字 一直重复这样做 我们很快会停在有 数字黑洞 之称的 6174 这个神奇的数字也叫
  • 回文质数Prime Palindromes

    题目描述 因为 151 既是一个质数又是一个回文数 从左到右和从右到左是看一样的 所以 151 是回文质数 写一个程序来找出范围 a b 一亿 间的所有回文质数 输入输出格式 输入格式 第 1 行 二个整数 a 和 b 输出格式 输出一个回
  • Locust压力测试使用总结

    上次做接口压力测试前一直研究使用jmeter 本以为可以拿来使用了 但是真正进行并发接口时 发现jmeter在单机下并发1000个时 台式电脑单机资源早就被使用完 整个jmeter卡得死死的 结果那晚使用jmeter并发失败 幸好之前也准备
  • FFmpeg学习笔记--视频传输的基本概念

    目录 1 容器 container 和文件 file 2 媒体流 stream 3 数据帧 frame 和数据包 packet 4 编解码器 Codec 5 复用 mux 6 解复用 demux 7 码率 bps 和帧率 fps 8 ffm
  • 【Android -- 写作工具】Markdown 代码块

    1 前言 关于代码块 Markdown 作者给出的定义如下 预格式化代码块主要用于在 Markdown 文档中显示源代码风格的内容 相比普通的文本段落 代码块可以保留文字内容的多行换行 缩进等格式 在 Markdown 文档中生成代码块 需
  • Numpy中的(一维)数组和(行列)向量

    Numpy中的 一维 数组和 行列 向量 随笔记录 Numpy的数组和行列向量的区别 随笔记录 Numpy的数组和行列向量的区别 今天做sklearn的datasets diabetes 的实验 做了个操作 diabetes是一个442 1
  • 【FPGA的基础快速入门17------频率计】

    FPGA的基础学习 频率计 频率计简介 等精度频率计 频率计简介 频率计又称为频率计数器 是一种专门对被测信号频率进行测量的电子测量仪器 计数法 直接计数单位时间内被测信号的脉冲数 这种方法测量精度高 速度快 适合不同频率 不同精确度测频的
  • 输入一个四位整数,分别输出组成该四位数的各位数字

    一 代码实现 1 include
  • Spring框架支持哪几种Bean作用域?自动装配Bean有哪些方式?

    Spring框架支持哪几种Bean作用域 spring支持五种Bean作用域 singleton 单例 就是每个spring容器只有一个 实例对象 prototype 多例 一个bean可以定义多个实例 另外三个是在web的Spring A
  • dell服务器启动顺序如何设置_戴尔品牌机怎么设置启动顺序(按F12进bios的)?

    展开全部 这主板非常麻烦 可关了保护 并切换 Legacy启动模式 U盘PE 装完系统 要改回uefi模式 DELL bios操作一32313133353236313431303231363533e59b9ee7ad943133343137
  • 传输线的物理基础(二):信号在传输线中的速度

    铜中电子的速度 信号在传输线上传输的速度有多快 如果人们经常错误地认为信号在传输线上的速度取决于导线中电子的速度 凭着这种错误的直觉 我们可能会想象降低互连的电阻会提高信号的速度 事实上 典型铜线中电子的速度实际上比信号速度慢约 100 亿
  • NLP中的数据增强方法!

    作者简介 大家好我是 uu 人工智能硕博在读 精通python 某大厂nlp算法经历 机器学习 深度学习 自然语言处理 计算机视觉 个人主页 uu主页 觉得uu写的不错的话 麻烦动动小手 点赞 收藏 评论 今天给大家带来的刷题系列是 NLP
  • BUS creator & selector、Mux&Demux

    2 3 总线BUS creator selector Bus Creator 由几路输入信号合成为一条总线信号 Bus Selector 由总线信号中选取需要的一路或几路信号输出 Mux 信号合成 Demux 信号分解 区别 Bus的可选择
  • vue web在线聊天功能实现

    上一篇介绍了vue怎么实现无限滚动窗体 这一篇就具体怎么使用vue实现web在线聊天功能展开深入讨论 对尚且不清楚怎么实现无限滚动窗体的 可前往这里查看 vue和iview实现无限滚动的正确解法 先看看最终实现的效果 实现过程 无限滚动窗体
  • 【ChatGPT进阶】如何使用ChatGPT做知乎好物?

    如果你想通过知乎赚钱 知乎好物是一个不错的选择 门槛很低 而且是一个可以长期 躺赚 的项目 如果你会ChatGPT的话 可以去卷同行 知乎好物是什么 知乎好物是一种在知乎平台上创作内容或回答问题时 使用 好物推荐 功能在内容中插入商品卡片
  • AI绘画StableDiffusion美女实操教程:斗破苍穹-小医仙-天毒女(附高清图下载)

    小医仙 是天蚕土豆所著玄幻小说 斗破苍穹 1 及其衍生作品中的角色 身负厄难毒体 食毒修炼 万毒不侵 通体毒气 这种会无意识地杀死别人的体质让天性善良的小医仙成为人憎鬼厌的天毒女 在萧炎多次帮助下得以控制 出图效果展示 资源整合 今天我们就
  • springboot集成RabbitMQ-超级详细步骤

    本文对应的代码地址 https github com zhangshilin9527 rabbitmq study 前置工作 1 安装rabbitmq 2 登录 地址 http localhost 15672 账号密码 guest gues
  • mybatis学习(31):修改部分字段(有外键,先查询,再修改)

    目录结构 com geyao mybatis mapper BlogMapper类 package com geyao mybatis mapper import java util List import java util Map im
  • vue利用路由控制实现登录功能

    未使用服务器接口 登录信息保存在cookie中 可以实现登录功能 vue交流群203849104 vue使用cookie首先需要安装cookie npm install js cookie 然后在router下面的index js文件中引入
  • 线程池ThreadPoolExecutor源码解析

    参考视频 首先回顾一下创建线程等的三种方式 第一个是直接继承Thread类 重写run方法 这个其实内部也是继承了Runnable接口重写run方法 比如 public class MyThread extends Thread Overr