线程池的实现原理

池化技术

一说到线程池自然就会想到池化技术

其实所谓池化技术,就是把一些能够复用的东西放到池中,避免重复创建、销毁的开销,从而极大提高性能。

常见池化技术的例如:

  • 线程池

  • 内存池

  • 连接池

1 线程池的实现原理及基本类结构

合理利用线程池能够带来四个好处。

  1. 降低资源消耗。线程频繁的创建=>销毁=>创建对系统对开销很大,使用线程池可以避免重复的开销
  2. 方便复用,提高相应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  4. 线程的创建于执行完全分开,方便维护,降低耦合

Executor线程池框架的最大优点是把任务的提交和执行解耦。客户端将要执行的任务封装成Task,然后提交即可。而Task如何执行客户端则是透明的。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果。
  下图是线程池所涉及到的所有类的结构图(右键查看大图),先从整体把握下:

              图1 线程池实现原理类结构图

从图一可知,实际的线程池类是实现ExecutorService接口的类,有ThreadPoolExecutor、ForkJoinPool和ScheduledThreadPoolExecutor。下面以常用的ThreadPoolExecutor为例讲解。

2 线程池实现步骤

2.1 Java中的实现

官方接口

JDK 1.5 推出了三大API用来创建线程:

  • Executors.newCachedThreadPool():无限线程池(最大21亿)
  • Executors.newFixedThreadPool(nThreads):固定大小的线程池
  • Executors.newSingleThreadExecutor():单个线程的线程池

这三个API的底层其实都是由同一个类实现的:ThreadPoolExecutor

public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}

2.2 线程池的创建

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

参数说明:

  • corePoolSize(线程池的基本线程数): the number of threads to keep in the pool, even if they are idle, unless allowCoreThreadTimeOut is set.

    • 当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
  • maximumPoolSize(线程池最大线程数): the maximum number of threads to allow in the pool.
    • 线程池允许创建的最大线程数。如果任务队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
  • keepAliveTime(线程活动保持时间):when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.
    • 线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
  • TimeUnit(线程活动保持时间的单位):the time unit for the keepAliveTime argument.
    • 可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
  • workQueue(任务队列):the queue to use for holding tasks before they are executed. This queue will hold only the Runnable tasks submitted by the execute method.
    • 用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。

      • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
      • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
      • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
      • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  • ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
  • RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。
    • AbortPolicy:直接抛出异常。

      • 使用场景:ThreadPoolExecutor中默认的策略就是AbortPolicy,由于ExecutorService接口的系列ThreadPoolExecutor都没有显示的设置拒绝策略,所以默认的都是这个。
    • CallerRunsPolicy:只用调用者所在线程来运行任务。
      • 使用场景:一般在不允许失败、对性能要求不高、并发量较小的场景下使用。
    • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
      • **使用场景:**提交的任务无关紧要,一般用的少。
    • DiscardPolicy:不处理,丢弃掉。
        当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。

      • **使用场景:**发布消息、修改消息类似场景。当老消息还未执行,此时新的消息又来了,这时未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。

由此可见,创建一个线程所需的参数很多,线程池为我们提供了类Executors的静态工厂方法以创建不同类型的线程池。
- newFixedThreadPool可以生成固定大小的线程池;
- newCachedThreadPool可以生成一个无界、可以自动回收的线程池;
- newSingleThreadScheduledExecutor可以生成一个单个线程的线程池;
- newScheduledThreadPool还可以生成支持周期任务的线程池。

2.3 线程池中的状态

  • RUNNING 自然是运行状态,指可以接受任务执行队列里的任务
  • SHUTDOWN 指调用了 shutdown() 方法,不再接受新任务了,但是队列里的任务得执行完毕。
  • STOP 指调用了 shutdownNow() 方法,不再接受新任务,同时抛弃阻塞队列里的所有任务并中断所有正在执行任务。
  • TIDYING 所有任务都执行完毕,在调用 shutdown()/shutdownNow() 中都会尝试更新为这个状态。
  • TERMINATED 终止状态,当执行 terminated() 后会更新为这个状态。

2.4 向线程池提交任务

有两种方式提交任务:
1.使用void execute(Runnable command)方法提交任务
execute方法返回类型为void,所以没有办法判断任务是否被线程池执行成功。

Runnable task = new Runnable() {@Overridepublic void run() {System.out.println("Task is running by " + Thread.currentThread().getName());System.out.println("线程池正在执行的线程数:" + threadPoolExecutor.getActiveCount());}
};
threadPool.execute(task);

2.使用submit方法提交任务
Future<?> submit(Runnable task);<T> Future<T> submit(Runnable task, T result);Future<T> submit(Callable<T> task);会返回一个Future,可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。

Future<?> future = threadPool.submit(task);
try {Object result = future.get();System.out.println("任务是否完成:" + future.isDone());System.out.println("返回的结果为:" + result);
} catch (InterruptedException | ExecutionException e) {e.printStackTrace();
} finally {// 关闭线程池threadPool.shutdown();
}

2.5 线程池关闭

  1. shutdown()方法:这个方法会平滑地关闭ExecutorService,当我们调用这个方法时,ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。
  2. awaitTermination(long timeout, TimeUnit unit)方法:这个方法有两个参数,一个是timeout即超时时间,另一个是unit即时间单位。这个方法会使当前关闭线程池的线程等待timeout时长,当超过timeout时间后,则去监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。一般情况下会和shutdown方法组合使用。
  3. shutdownNow()方法:这个方法会强制关闭ExecutorService,它将取消所有运行中的任务和在工作队列中等待的任务,这个方法返回一个List列表,列表中返回的是等待在工作队列中的任务。
// 4. 关闭线程池
threadPool.shutdown();
// hreadPool.shutdownNow();
System.out.println("线程池是否关闭:" + threadPool.isShutdown());
try {//当前线程阻塞10ms后,去检测线程池是否终止,终止则返回truewhile(!threadPool.awaitTermination(10, TimeUnit.MILLISECONDS)) {System.out.println("检测线程池是否终止:" + threadPool.isTerminated());}
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println("线程池是否终止:" + threadPool.isTerminated());12345678910111213

完整案例:

package com.markliu.concurrent.threadpool;import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class ThreadPoolDemo1 {public static void main(String[] args) {/** 1. 创建线程池* * 创建一个固定线程数目的线程池。corePoolSize = maximumPoolSize = 5* 即线程池的基本线程数和最大线程数相等。* 相当于:* new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());*/ExecutorService threadPool = Executors.newFixedThreadPool(5);/**  2. 封装任务并提交给线程池*/ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) threadPool;Runnable task = new Runnable() {@Overridepublic void run() {System.out.println("Task is running by " + Thread.currentThread().getName());System.out.println("线程池正在执行的线程数:" + threadPoolExecutor.getActiveCount());try {TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}};/** Starts all core threads, causing them to idly wait for work. * This overrides the default policy of starting core threads * only when new tasks are executed.*/int count = threadPoolExecutor.prestartAllCoreThreads();System.out.println("开启的所有core线程数:" + count);System.out.println("线程池当前线程数:" + threadPoolExecutor.getPoolSize());System.out.println("线程池的core number of threads:" + threadPoolExecutor.getCorePoolSize());System.out.println("线程池中的最大线程数:" + threadPoolExecutor.getLargestPoolSize());// 3. 执行,获取返回结果/*** execute方式提交任务*/// threadPool.execute(task);/*** submit方式提交任务*/Future<?> future = threadPool.submit(task);try {// 阻塞,等待线程执行完成,并获得结果Object result = future.get();System.out.println("任务是否完成:" + future.isDone());System.out.println("返回的结果为:" + result);} catch (InterruptedException | ExecutionException e) {e.printStackTrace();} finally {System.out.println("线程池中已经执行完的任务数:" + threadPoolExecutor.getCompletedTaskCount());// 4. 关闭线程池/** shutdown方法平滑地关闭线程池,将线程池的状态设为:SHUTDOWN* 停止接受任何新的任务且等待已经提交的任务执行完成,当所有已经* 提交的任务执行完毕后将会关闭线程池*/threadPool.shutdown();/**  shutdownNow方法强制关闭线程池,将线程池状态设置为:STOP*  取消所有运行中的任务和在工作队列中等待的任务,并返回所有未执行的任务List*/// hreadPool.shutdownNow();System.out.println("线程池是否关闭:" + threadPool.isShutdown());try {//当前线程阻塞10ms后,去检测线程池是否终止,终止则返回truewhile(!threadPool.awaitTermination(10, TimeUnit.MILLISECONDS)) {System.out.println("检测线程池是否终止:" + threadPool.isTerminated());}} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程池是否终止:" + threadPool.isTerminated());}}
}

3 线程池的执行流程分析

线程池的主要工作流程如下图:
    
  当提交一个新任务到线程池时,线程池的处理流程如下:

  • 首先线程池判断“基本线程池”(corePoolSize)是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。

  • 其次线程池判断工作队列(workQueue)是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。

  • 最后线程池判断整个线程池的线程数是否已超过maximumPoolSize?没满,则创建一个新的工作线程来执行任务,满了,则交给拒绝策略来处理这个任务。

    (我的理解:提交任务—>如果线程数未达到corePoolSize,则创建线程执行任务—>如果达到corePoolSize,仍让提交了任务,则会有任务等待,所以将任务保存在任务队列中,直到任务队列workQueue已满—>如果workQueue已满,仍然有任务提交,但未达到最大线程数,则继续创建线程执行任务,直到线程数达到maximumPoolSize,如果达到了maximumPoolSize,则根据饱和策略拒绝该任务。这也就解释了为什么有了corePoolSize还有maximumPoolSize的原因。

    关于线程池的工作原理后期从源代码分析。

4 线程池的监控

通过线程池提供的参数进行监控:

  • taskCount:线程池需要执行的任务数量。
  • completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount。
  • largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
  • getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减
  • getActiveCount:获取活动的线程数。

通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。

    /*** 给定的Thread执行Runnable之前调用此方法** @param t the thread that will run task {@code r}* @param r the task that will be executed*/protected void beforeExecute(Thread t, Runnable r) { }/*** 给定的Runnable完成后执行此方法* This method is invoked by the thread that executed the task. * @param r the runnable that has completed* @param t the exception that caused termination, or null if* execution completed normally*/protected void afterExecute(Runnable r, Throwable t) { }/*** 当Executor终止时调用此方法.  * 注意:方法中子类应调用super.terminated()*/protected void terminated() { }1234567891011121314151617181920

例如:

class ExtendedExecutor extends ThreadPoolExecutor {// ...protected void afterExecute(Runnable r, Throwable t) {super.afterExecute(r, t);if (t == null && r instanceof Future<?>) {try {Object result = ((Future<?>) r).get();} catch (CancellationException ce) {t = ce;} catch (ExecutionException ee) {t = ee.getCause();} catch (InterruptedException ie) {Thread.currentThread().interrupt(); // ignore/reset}}if (t != null)System.out.println(t);}
}

5.推荐使用 ThreadPoolExecutor 构造函数创建线程池

在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。

为什么呢?

使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

另外《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors 返回线程池对象的弊端如下:

  • FixedThreadPoolSingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

方式一:通过ThreadPoolExecutor构造函数实现(推荐)

方式二:通过 Executor 框架的工具类 Executors 来实现 我们可以创建三种类型的 ThreadPoolExecutor:

  • FixedThreadPool
  • SingleThreadExecutor
  • CachedThreadPool

对应 Executors 工具类中的方法如图所示:

6. (重要)ThreadPoolExecutor 使用示例

我们上面讲解了 Executor框架以及 ThreadPoolExecutor 类,下面让我们实战一下,来通过写一个 ThreadPoolExecutor 的小 Demo 来回顾上面的内容。

6.1 示例代码:Runnable+ThreadPoolExecutor

首先创建一个 Runnable 接口的实现类(当然也可以是 Callable 接口,我们上面也说了两者的区别。)

MyRunnable.java
import java.util.Date;/*** 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。* @author shuang.kou*/
public class MyRunnable implements Runnable {private String command;public MyRunnable(String s) {this.command = s;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());processCommand();System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());}private void processCommand() {try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}}@Overridepublic String toString() {return this.command;}
}

编写测试程序,我们这里以阿里巴巴推荐的使用 ThreadPoolExecutor 构造函数自定义参数的方式来创建线程池。

ThreadPoolExecutorDemo.java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class ThreadPoolExecutorDemo {private static final int CORE_POOL_SIZE = 5;private static final int MAX_POOL_SIZE = 10;private static final int QUEUE_CAPACITY = 100;private static final Long KEEP_ALIVE_TIME = 1L;public static void main(String[] args) {//使用阿里巴巴推荐的创建线程池的方式//通过ThreadPoolExecutor构造函数自定义参数创建ThreadPoolExecutor executor = new ThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE_TIME,TimeUnit.SECONDS,new ArrayBlockingQueue<>(QUEUE_CAPACITY),new ThreadPoolExecutor.CallerRunsPolicy());for (int i = 0; i < 10; i++) {//创建WorkerThread对象(WorkerThread类实现了Runnable 接口)Runnable worker = new MyRunnable("" + i);//执行Runnableexecutor.execute(worker);}//终止线程池executor.shutdown();while (!executor.isTerminated()) {}System.out.println("Finished all threads");}
}

可以看到我们上面的代码指定了:

  1. corePoolSize: 核心线程数为 5。
  2. maximumPoolSize :最大线程数 10
  3. keepAliveTime : 等待时间为 1L。
  4. unit: 等待时间的单位为 TimeUnit.SECONDS。
  5. workQueue:任务队列为 ArrayBlockingQueue,并且容量为 100;
  6. handler:饱和策略为 CallerRunsPolicy

Output:

pool-1-thread-3 Start. Time = Sun Apr 12 11:14:37 CST 2020
pool-1-thread-5 Start. Time = Sun Apr 12 11:14:37 CST 2020
pool-1-thread-2 Start. Time = Sun Apr 12 11:14:37 CST 2020
pool-1-thread-1 Start. Time = Sun Apr 12 11:14:37 CST 2020
pool-1-thread-4 Start. Time = Sun Apr 12 11:14:37 CST 2020
pool-1-thread-3 End. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-4 End. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-1 End. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-5 End. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-1 Start. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-2 End. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-5 Start. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-4 Start. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-3 Start. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-2 Start. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-1 End. Time = Sun Apr 12 11:14:47 CST 2020
pool-1-thread-4 End. Time = Sun Apr 12 11:14:47 CST 2020
pool-1-thread-5 End. Time = Sun Apr 12 11:14:47 CST 2020
pool-1-thread-3 End. Time = Sun Apr 12 11:14:47 CST 2020
pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020

Java多线程- 线程池的基本使用和执行流程分析 - ThreadPoolExecutor相关推荐

  1. 并发编程五:java并发线程池底层原理详解和源码分析

    文章目录 java并发线程池底层原理详解和源码分析 线程和线程池性能对比 Executors创建的三种线程池分析 自定义线程池分析 线程池源码分析 继承关系 ThreadPoolExecutor源码分 ...

  2. Java多线程 线程池Executor框架

    目录 一.说明 二.理解 Executor ExecutorService Executors 三.实现 1. newSingleThreadExecutor 2. newFixedThreadPoo ...

  3. java多线程线程池_Java多线程——线程池(ThreadPool)

    我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁 ...

  4. Java多线程——线程池的饥饿现象

    概述 定长线程池的使用过程中会存在饥饿现象,也就是当多线程情况下,当池中所有线程都被占用后,被占用的线程又需要空闲线程去进行下一步的操作,此时又获取不到池中空闲的线程,此时就出现了饥饿现象. 示例 p ...

  5. Java多线程——线程池使用示例

    示例代码: import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;public clas ...

  6. Java并发—线程池ThreadPoolExecutor基本总结

    原文作者:Matrix海子 原文地址:Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线 ...

  7. [转]深入理解Java之线程池

    原文链接 原文出处: 海 子 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这 ...

  8. 深入理解Java之线程池

    深入理解Java之线程池 原作者:海子 出处:http://www.cnblogs.com/dolphin0520/ 本文归作者海子和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面 ...

  9. 多线程线程池的实现java_如何在Java中实现线程池

    多线程线程池的实现java 线程是独立程序的执行路径. 在java中,每个线程都扩展java.lang.Thread类或实现java.lang.Runnable. 多线程是指在一个任务中同时执行两个或 ...

最新文章

  1. 字节跳动喜欢招聘这样的机器学习工程师
  2. 树结构(三)----线索二叉树
  3. PyTorch随笔-1
  4. 全国地铁城市数据分析(python实现)
  5. 解决“DNS_PROBE_FINISHED_NXDOMAIN”问题
  6. WDLINUX (Centos5.8) 安装 bcmath
  7. c语言 选择结构,C语言学习:选择结构
  8. 人工智能火灾报警器_使用AI进行准确的火灾预测
  9. 数据结构与算法 c语言6,C语言程序设计题库之6.doc
  10. python win32api教程_winapi-如何在python中使用win32 API?
  11. 基于python实现网络课程秒刷
  12. virt-manager 键盘错位解决
  13. 网络笔记:路由选路原则
  14. 计算机二级Python考试经验分享(含考试大纲及知识点总结)
  15. CLRS 17.4动态表
  16. 缓冲区溢出攻击指什么?如何防御?
  17. 推送系统从0到1(八):个性化精准推送的实现
  18. 天九共享:企业成功的重要元素是责任感
  19. croc使用 —(两个电脑间传输文件)
  20. spring的后置处理器(未完结版)

热门文章

  1. BBI综述:在微生物组研究中使用宏转录组
  2. 我有个师兄,毕业后投身了学术自媒体行业
  3. WR:中国46个饮用水供水系统评估水源水对龙头水细菌群落的“烙印”
  4. ggplot2笔记2:图层的使用——基础、怎样加标签、注释
  5. R语言ggplot2可视化分面图(facet_wrap)、使用size参数自定义设置分面图标签栏(灰色标签栏)中的标签文本的字体大小
  6. R语言为dataframe添加新的数据列(横向拼接、Appending columns,Unioning columns):使用R原生方法、data.table、dplyr等方案
  7. pandas使用read_csv读取数据使用skiprows参数跳过指定的数据行但保留表头、pandas使用to_csv函数将dataframe保存为gzip压缩文件
  8. R语言ggplot2可视化在可视化图形的X轴标签中添加温度摄氏度符号(add temperature degree symbol on axis label)
  9. R语言vtreat包的mkCrossFrameCExperiment函数交叉验证构建数据处理计划并进行模型训练、通过显著性进行变量筛选(删除相关性较强的变量)、构建多变量模型、转化为分类模型、模型评估
  10. R语言使用dplyr包基于因子变量(factor)将原dataframe拆分为每一个因子对应的单独数据集dataframe实战