分析Java线程池执行原理

2023-11-06

Java并发编程源码分析系列:

上一篇已经对线程池的创建进行了分析,了解线程池既有预设的模板,也提供多种参数支撑灵活的定制。

本文将会围绕线程池的生命周期,分析线程池执行任务的过程。

线程池状态

首先认识两个贯穿线程池代码的参数:

  • runState:线程池运行状态
  • workerCount:工作线程的数量

线程池用一个32位的int来同时保存runState和workerCount,其中高3位是runState,其余29位是workerCount。代码中会反复使用runStateOf和workerCountOf来获取runState和workerCount。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// 线程池状态
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;

// ctl操作
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
  • RUNNING:可接收新任务,可执行等待队列里的任务
  • SHUTDOWN:不可接收新任务,可执行等待队列里的任务
  • STOP:不可接收新任务,不可执行等待队列里的任务,并且尝试终止所有在运行任务
  • TIDYING:所有任务已经终止,执行terminated()
  • TERMINATED:terminated()执行完成

线程池状态默认从RUNNING开始流转,到状态TERMINATED结束,中间不需要经过每一种状态,但不能让状态回退。下面是状态变化可能的路径和变化条件:

图1 线程池状态变化路径

Worker的创建

线程池是由Worker类负责执行任务,Worker继承了AbstractQueuedSynchronizer,引出了Java并发框架的核心AQS。

AbstractQueuedSynchronizer,简称AQS,是Java并发包里一系列同步工具的基础实现,原理是根据状态位来控制线程的入队阻塞、出队唤醒来处理同步。

Worker利用AQS的功能实现对独占线程变量的设置,这是一个需要同步的过程。AQS不会在这里展开讨论,有兴趣的同学可以去看分析CountDownLatch的实现原理,里面详细介绍了AQS的实现原理。

调用execute将会根据线程池的情况创建Worker,可以归纳出下图四种情况:

图2 worker在线程池里的四种可能

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();

    int c = ctl.get();
    //1
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //2
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            //3
            reject(command);
        else if (workerCountOf(recheck) == 0)
            //4
            addWorker(null, false);
    }
    //5
    else if (!addWorker(command, false))
        //6
        reject(command);
}

标记1对应第一种情况,要留意addWorker传入了core,core=true为corePoolSize,core=false为maximumPoolSize,新增时需要检查workerCount是否超过允许的最大值。

标记2对应第二种情况,检查线程池是否在运行,并且将任务加入等待队列。标记3再检查一次线程池状态,如果线程池忽然处于非运行状态,那就将等待队列刚加的任务删掉,再交给RejectedExecutionHandler处理。标记4发现没有worker,就先补充一个空任务的worker。

标记5对应第三种情况,等待队列不能再添加任务了,调用addWorker添加一个去处理。

标记6对应第四种情况,addWorker的core传入false,返回调用失败,代表workerCount已经超出maximumPoolSize,那就交给RejectedExecutionHandler处理。


private boolean addWorker(Runnable firstTask, boolean core) {
        //1
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
        //2
        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;
    }

标记1的第一段代码,目的很简单,是为workerCount加一。至于为什么代码写了这么长,是因为线程池的状态在不断变化,并发环境下需要保证变量的同步性。外循环判断线程池状态、任务非空和队列非空,内循环使用CAS机制保证workerCount正确地递增。不了解CAS可以看认识非阻塞的同步机制CAS,后续增减workerCount都会使用CAS。

标记2的第二段代码,就比较简单。创建一个新Worker对象,将Worker添加进workers里(Set集合)。成功添加后,启动worker里的线程。在finally里判断线程是否启动成功,不成功直接调用addWorkerFailed。

private void addWorkerFailed(Worker w) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            if (w != null)
                workers.remove(w);
            decrementWorkerCount();
            tryTerminate();
        } finally {
            mainLock.unlock();
        }
    }

addWorkerFailed将减少已经递增的workerCount,并且调用tryTerminate结束线程池。

Worker的执行

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

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

Worker在构造函数里采用ThreadFactory创建Thread,在run方法里调用了runWorker,看来是真正执行任务的地方。

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
       //1
        while (task != null || (task = getTask()) != null) {
            w.lock();
           //2
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
               //3
                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;
                 //4
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;       //5
    } finally {
        //6
        processWorkerExit(w, completedAbruptly);
    }
}

标记1进入循环,从getTask获取要执行的任务,直到返回null。这里达到了线程复用的效果,让线程处理多个任务。

标记2是一个比较复杂的判断,保证了线程池在STOP状态下线程是中断的,非STOP状态下线程没有被中断。如果你不了解Java的中断机制,看如何正确结束Java线程这篇。

标记3调用了run方法,真正执行了任务。执行前后提供了beforeExecute和afterExecute两个方法,由子类实现。

标记4里的completedTasks统计worker执行了多少任务,最后累加进completedTaskCount变量,可以调用相应方法返回一些统计信息。

标记5的变量completedAbruptly表示worker是否异常终止,执行到这里代表执行正常,后续的方法需要这个变量。

标记6调用processWorkerExit结束,后面会分析。


接着来看worker从等待队列获取任务的getTask方法:

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        //1
        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);
        //2
        // 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;
        }
       //3
        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

标记1检查线程池的状态,这里就体现出SHUTDOWN和STOP的区别。如果线程池是SHUTDOWN状态,还会先处理完等待队列的任务;如果是STOP状态,就不再处理等待队列里的任务了。

标记2先看allowCoreThreadTimeOut这个变量,false时worker空闲,也不会结束;true时,如果worker空闲超过keepAliveTime,就会结束。接着是一个很复杂的判断,好难转成文字描述,自己看吧。注意一下wc>maximumPoolSize,出现这种可能是在运行中调用setMaximumPoolSize,还有wc>1,在等待队列非空时,至少保留一个worker。

标记3是从等待队列取任务的逻辑,根据timed分为等待keepAliveTime或者阻塞直到有任务。


最后来看结束worker需要执行的操作:

private void processWorkerExit(Worker w, boolean completedAbruptly) {
   //1
    if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
        decrementWorkerCount();

  //2
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        completedTaskCount += w.completedTasks;
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }

   //3
    tryTerminate();

    int c = ctl.get();
    //4
    if (runStateLessThan(c, STOP)) {
        if (!completedAbruptly) {
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        addWorker(null, false);
    }
}

正常情况下,在getTask里就会将workerCount减一。标记1处用变量completedAbruptly判断worker是否异常退出,如果是,需要补充对workerCount的减一。

标记2将worker处理任务的数量累加到总数,并且在集合workers中去除。

标记3尝试终止线程池,后续会研究。

标记4处理线程池还是RUNNING或SHUTDOWN状态时,如果worker是异常结束,那么会直接addWorker。如果allowCoreThreadTimeOut=true,并且等待队列有任务,至少保留一个worker;如果allowCoreThreadTimeOut=false,workerCount不少于corePoolSize。


总结一下worker:线程池启动后,worker在池内创建,包装了提交的Runnable任务并执行,执行完就等待下一个任务,不再需要时就结束。

线程池的关闭

线程池的关闭不是一关了事,worker在池里处于不同状态,必须安排好worker的"后事",才能真正释放线程池。ThreadPoolExecutor提供两种方法关闭线程池:

  • shutdown:不能再提交任务,已经提交的任务可继续运行;
  • shutdownNow:不能再提交任务,已经提交但未执行的任务不能运行,在运行的任务可继续运行,但会被中断,返回已经提交但未执行的任务。
public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();   //1 安全策略机制
        advanceRunState(SHUTDOWN);   //2
        interruptIdleWorkers();   //3
        onShutdown(); //4 空方法,子类实现
    } finally {
        mainLock.unlock();
    }
    tryTerminate();   //5
}

shutdown将线程池切换到SHUTDOWN状态,并调用interruptIdleWorkers请求中断所有空闲的worker,最后调用tryTerminate尝试结束线程池。

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);
        interruptWorkers();
        tasks = drainQueue();  //1
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

shutdownNow和shutdown类似,将线程池切换为STOP状态,中断目标是所有worker。drainQueue会将等待队列里未执行的任务返回。

interruptIdleWorkers和interruptWorkers实现原理都是遍历workers集合,中断条件符合的worker。


上面的代码多次出现调用tryTerminate,这是一个尝试将线程池切换到TERMINATED状态的方法。

final void tryTerminate() {
    for (;;) {
        int c = ctl.get();
        //1
        if (isRunning(c) ||
            runStateAtLeast(c, TIDYING) ||
            (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
            return;
        //2
        if (workerCountOf(c) != 0) { // Eligible to terminate
            interruptIdleWorkers(ONLY_ONE);
            return;
        }
       //3
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                try {
                    terminated();
                } finally {
                    ctl.set(ctlOf(TERMINATED, 0));
                    termination.signalAll();
                }
                return;
            }
        } finally {
            mainLock.unlock();
        }
        // else retry on failed CAS
    }
}

标记1检查线程池状态,下面几种情况,后续操作都没有必要,直接return。

  • RUNNING(还在运行,不能停)
  • TIDYING或TERMINATED(已经没有在运行的worker)
  • SHUTDOWN并且等待队列非空(执行完才能停)

标记2在worker非空的情况下又调用了interruptIdleWorkers,你可能疑惑在shutdown时已经调用过了,为什么又调用,而且每次只中断一个空闲worker?你需要知道,shutdown时worker可能在执行中,执行完阻塞在队列的take,不知道要结束,所有要补充调用interruptIdleWorkers。每次只中断一个是因为processWorkerExit时,还会执行tryTerminate,自动中断下一个空闲的worker。

标记3是最终的状态切换。线程池会先进入TIDYING状态,再进入TERMINATED状态,中间提供了terminated这个空方法供子类实现。


调用关闭线程池方法后,需要等待线程池切换到TERMINATED状态。awaitTermination检查限定时间内线程池是否进入TERMINATED状态,代码如下:

public boolean awaitTermination(long timeout, TimeUnit unit)
    throws InterruptedException {
    long nanos = unit.toNanos(timeout);
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (;;) {
            if (runStateAtLeast(ctl.get(), TERMINATED))
                return true;
            if (nanos <= 0)
                return false;
            nanos = termination.awaitNanos(nanos);
        }
    } finally {
        mainLock.unlock();
    }
}

后言

以上过了一遍线程池主要的逻辑,总体来看线程池的设计是很清晰的。如有错误或不足,欢迎指出,也欢迎留言交流。今次介绍了线程池运行的生命周期,下篇会研究更细粒度地控制任务的生命周期,也就是submit和Future。



作者:展翅而飞
链接:https://www.jianshu.com/p/f62a3f452869
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

 

 

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

分析Java线程池执行原理 的相关文章

  • 线程池

    1 线程池的概念 线程池 其实就是一个容纳多个线程的容器 其中的线程可以反复使用 省去了频繁创建线程对象的操作 无需反复创建线程而消耗过多资源 最初是程序员自己开发线程池 用ArrayList
  • Win32环境下两种用于C++的线程同步类

    线程同步是多线程程序设计的核心内容 它的目的是正确处理多线程并发时的各种问题 例如线程的等待 多个线程访问同一数据时的互斥 防死锁等 Win32提供多种内核对象和手段用于线程同步 如互斥量 信号量 事件 临界区等 所不同的是 互斥量 信号量
  • 用Java Socket开发小型服务器,支持上千个并发

    Java Socket 套接字 socket 为两台计算机之间的通信提供了一种机制 在James Gosling注意到Java 语言之前 套接字就早已赫赫有名 该语言只是让您不必了解底层操作系统的细节就能有效地使用套接字 1 客户机 服务器
  • ORA-00322, ORA-00312 问题解决

    昨天发现无法登录Oracle数据库 通过sqlplus工具open数据库时报如下错误 alter database open ERROR at line 1 ORA 00322 log 2 of thread 1 is not curren
  • 03C++11多线程编程之测试join,detach传各种实参时形参的拷贝次数

    03C 11多线程编程之测试join detach传各种实参时形参的拷贝次数 首先我们看下面的总结测试图 然后一步步的测试 1 这里我们先测试join传实参的类型 当实参为普通对象时 1 当形参为普通对象时 拷贝了两次 2 当形参为引用时
  • java中的锁池和等待池

    在java中 每个对象都有两个池 锁 monitor 池和等待池 wait notifyAll notify 三个方法都是Object类中的方法 锁池 假设线程A已经拥有了某个对象 注意 不是类 的锁 而其它的线程想要调用这个对象的某个sy
  • Python使用threading.Timer实现执行可循环的定时任务

    前言 Python中使用threading Timer执行定时任务时 执行任务是一次性的 类似于JS中的setTimeout方法 我们对其在封装 改造成可循环的定时器 类似于JS中setInterval方法的效果 值得注意的是 thread
  • 主线程中捕获子线程异常

    需求 主线程独立执行 无需等待子线程执行完毕 子线程如有异常抛出可自行catch 网上介绍的方法一般是 1 在线程内部进行try catch捕获异常 2 通过线程池的submit方法 获取Future对象 然后try catch Futur
  • yield和join方法的使用。

    join方法用线程对象调用 如果在一个线程A中调用另一个线程B的join方法 线程A将会等待线程B执行完毕后再执行 yield可以直接用Thread类调用 yield让出CPU执行权给同等级的线程 如果没有相同级别的线程在等待CPU的执行权
  • AFX_MANAGE_STATE(AfxGetStaticModuleState())讲解

    以前写MFC的DLL的时候 总会在自动生成的代码框架里看到提示 需要在每一个输出的函数开始添加上AFX MANAGE STATE AfxGetStaticModuleState 一直不明白这样做的含义 也一直没有这样做 而且代码也工作得好好
  • 0.net-跨线程使用CSocket

    CSocket断言错误 ASSERT pState gt m hSocketWindow NULL 起因 在套接字处于连接或者发送状态时 试图关闭套接字 于是在这个断言语句处发生中断 原因分析 微软官方解释如下 http support m
  • java多线程实战( 多个线程 修改同一个变量)

    java多线程实战 多个线程 修改同一个变量 synchronized 同步 介绍 java多线程实战 需求 创建两个线程 分别输出 a b 要求输出总和为30个 线程介绍 一 定义线程 1 扩展java lang Thread类 此类中有
  • Java 线程的生命周期(对应七大状态)

    博主前些天发现了一个巨牛的人工智能学习网站 通俗易懂 风趣幽默 忍不住也分享一下给大家 点击跳转到网站 线程的生命周期 线程状态的转换 通过代码输出线程的状态 代码如下 public class ThreadState public sta
  • qt中常用lambda表达式

    qt中lambda表达式 什么是lambda 个人理解 没有函数名的函数 qt中使用基础 备注 都是在qt5中做的使用 我的qt版本是qt5 11 3 pro文件中 config c 11 常见的lambda表达式使用 延时执行操作 举例
  • 第十三章:QT多线程(QThread)

    回顾 第一章 Qt的概述 第二章 在Ubuntu编写第一个Qt程序 第三章 Qt的字符串和字符编码 第四章 Qt的信号和槽 第五章 Qt容器窗口 父窗口 第六章 面向对象的Qt编程 第七章 Qt设计师使用 designer 第八章 Qt创造
  • c++11std::thread扩展

    最近 整理一下学习c 的文章 看到一篇文章 其中提到了thread local和std future 觉得这两东西很有趣 于是网上搜了一些资料 觉得很有帮助 希望可以对大家学习c 线程有所帮助 http www cnblogs com ha
  • python_os.walk(dir)

    for root dirs files in os walk dir os walk返回一个三元组 path 对当前路径以及其下所有的子目录进行递归 dirs 当前路径下的子目录 files 当前路径下的文件 gt gt gt for r
  • winCE中实现虚拟串口的方法

    转载请标明是引用于 http blog csdn net chenyujing1234 欢迎大家拍砖 环境 wince6 0 ARM Freescell 一 目的 设计一个读GPS串口数据的驱动 并注册为COM口 二 实现过程 1 COM
  • 一、使用interrupt()中断线程

    当一个线程运行时 另一个线程可以调用对应的Thread对象的interrupt 方法来中断它 该方法只是在目标线程中设置一个标志 表示它已经被中断 并立即返回 这里需要注意的是 如果只是单纯的调用interrupt 方法 线程并没有实际被中
  • Java并发编程:CountDownLatch、CyclicBarrier和 Semaphore

    Java并发编程 CountDownLatch CyclicBarrier和 Semaphore 2016 10 07 分类 基础技术 7 条评论 标签 并发 分享到 0 原文出处 海子 在java 1 5中 提供了一些非常有用的辅助类来帮

随机推荐

  • xml模式文档(xml:Schema)详解

    XML Schema 是基于 XML 的 DTD 替代者 XML Schema 描述 XML 文档的结构 XML Schema 语言也称作 XML Schema 定义 XML Schema Definition XSD 下面的例子是一个XM
  • EA12 显示用例图中use关系线的箭头

    默认情况下在Enterprise Architect EA 的用例中use关系线是没有箭头指示的 这样有时候看着挺别扭的 不过这个是可以设置的 步骤如下 点击菜单栏TOOLS 点击Options 选择Links 勾选Show Uses ar
  • win10编译 Fast R-CNN 所需的setup.py(rotate) tensorflow版

    问题描述 Fast R CNN rotate 原版提供的 setup py 是在linux中使用的 在linux里可以直接编译 而在windows下需要修改 setup py 解决方案 先提供思路 最后附上代码 1 修改 setup py
  • 5款类蝉妈妈抖音数据工具推荐

    担心蝉妈妈数据不准怎么办 作为一个在电商培训行业摸爬滚打10多年的老兵 大头今天给大家推荐其它五个类似蝉妈妈的抖音数据分析网站 一 飞瓜数据 飞瓜比蝉妈妈做的更早 抖快 B站 小红书 淘宝 微博等主流平台均有产品线 规模很大 飞瓜比较注重产
  • 二叉树的路径之和

    题目 给定一个二叉树与正数sum 找出所有从根节点到叶节点的路径 这些路径上的节点值累加和为sum include
  • 图机器学习课程笔记4

    维生素C吃多了会上火 个人CSDN博文目录 cs224w 图机器学习 2021冬季课程学习笔记集合 目录 1 思维大纲 2 中文笔记 1 思维大纲 2 中文笔记 笔记4 提取码 8888
  • 论文检索号查询

    大家好 在我们学术研究过程中 当你取得一份好的学术成果 成功发表一篇期刊论文后 往往会在不同的场合用到 论文检索号 今天我给大家分享一下什么是论文检索号以及如何查询论文检索号 什么是论文检索号 论文检索号是在论文数据库中检索论文的具有唯一性
  • 查找相似网站的方法和地址

    描述 查找相似网站的方法和地址 网址 https www similarsites com
  • macbook 15 自动重启,一天十几次

    重启后提示消息中包含 GPUPanic cpp 127 google了一下 可能是显卡的问题 macbook 15 2010年版本 机器里面有两个显卡 一个是integrated Intel显卡 一个是NVIDA显卡 mac os在切换显卡
  • python 列表排序方法

    本文将讨论的是 如何将一个字符串组成的列表 比如 abc cba bac 按照特定的条件 比如首字母 尾字母 或者长度 灵活的排序 目录 直接排序 由一些字符串组成的 list sort 方法可以直接用来对字符串排序 a John Smit
  • 函数指针和函数指针数组

    函数指针 指向函数的指针 函数指针数组 一个数组里面存的类型是函数指针 一 函数指针的声明和调用 1 指针声明 函数指针顾名思义就是一个指向函数的指针 int Add int x int y return x y char Sub char
  • Error:Unable to start the daemon process.解决

    导入一个项目出现了以下错误 Error Unable to start the daemonprocess This problem might be caused by incorrect configuration of the dae
  • 显示等待和隐式等待

    学习显示等待和隐式等待使用 参考博客原址 https blog csdn net sinat 41774836 article details 88965281 文章目录 强制等待 sleep 隐式等待 implicitly wait 显示
  • 【Linux命令详解

    文章目录 简介 一 参数列表 二 使用介绍 1 修改用户权限 2 修改用户组权限 3 修改其他用户权限 4 同时修改多个权限 5 使用数字模式设置权限 6 递归修改目录权限 总结 简介 在Ubuntu系统中 chmod命令是一个强大的工具
  • 疯壳Android嵌入式Linux平板开发教程3-9G-sensor

    详情地址 https fengke club GeekMart views offline android 购买链接 https fengke club GeekMart su fHnaDyD1o jsp 视频地址 https fengke
  • c字符串分割成数组_leetcode第31双周赛第三题leetcode1525. 字符串的好分割数目

    leetcode1525 字符串的好分割数目 给你一个字符串 s 一个分割被称为 好分割 当它满足 将 s 分割成 2 个字符串 p 和 q 它们连接起来等于 s 且 p 和 q 中不同字符的数目相同 请你返回 s 中好分割的数目 示例 1
  • HCIP考试考哪三门你知道么?

    HCIP考试考哪三门 HCIP是华为认证中资深级别的认证 与hcia相比 它可能需要考多门考试 只有都通过了 才能获得HCIP认证 那么HCIP考试要考哪三门呢 其实 不同的方向需要通过的考试门数各不相同 有的只要通过一门 有的则是两门 最
  • kettle转换中文数据出现乱码

    在使用kettle转换数据时 有时会出现中文乱码问题 下面介绍解决办法 首先先保证你自己创建或连接的数据库是utf 8编码 1 设置DB连接 打开kettle中连接的数据库 在高级中输入set names utf8 2 再到选项中命名参数
  • Flink服务器无响应,apache-flink

    在这种情况下 我们有3个kafka主题 每个主题有50个分区 它们具有不同的消息 而所有这些消息都具有 用户名 字段 topic 1 gt Message01 String username about 50 000 messages pe
  • 分析Java线程池执行原理

    Java并发编程源码分析系列 分析Java线程池的创建 上一篇已经对线程池的创建进行了分析 了解线程池既有预设的模板 也提供多种参数支撑灵活的定制 本文将会围绕线程池的生命周期 分析线程池执行任务的过程 线程池状态 首先认识两个贯穿线程池代