【JUC系列一】Java线程

 一、操作系统中线程和进程的概念
现在的操作系统是多任务操作系统。多线程是实现多任务的一种方式。
进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。

线程是指进程中的一个执行流程,一般我们说一个进程开始运行,是指该进程的主线程开始运行。一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。

“同时”执行是人的感觉,在线程之间实际上轮换执行。

二、Java中的线程

所以一般我们在java中都频繁使用线程池,实现线程的复用。

一、创建java线程的4种方法

Java 使用 Thread 类代表线程,所有的线程对象必须是 Thread 类或其子类的实例。Java 可以用四种方式来创建线程,如下:
①继承 Thread 类创建线程没有返回值
②实现 Runnable 接口创建线程没有返回值
③实现 Callable 接口,通过 FutureTask 包装器来创建 Thread 线程有返回值
④线程池:使用 ExecutorService、Callable、Future 实现有返回结果的线程有返回值

1️⃣------------------------继承Thread类创建线程---------------------

  1. 定义 Thread 类的子类,并重写该类的 run(),该方法的方法体就是线程需要完成的任务,run() 也称为线程的执行体。
  2. 创建 Thread 子类的实例,也就是创建了线程对象。
  3. 启动线程,即调用线程的 start()。
    代码实例:
public class MyThread extends Thread{//继承Thread类
  public void run(){ 
  //重写run方法
  }
}
public class Main {
  public static void main(String[] args){
    new MyThread().start();//创建并启动线程
  }
}

2️⃣------------------------实现Runnable接口创建线程---------------------

  1. 定义 Runnable 接口的实现类,同样要重写 run()。这个 run() 和 Thread 中的 run() 一样是线程的执行体。
  2. 创建 Runnable 实现类的实例,并用这个实例作为 Thread 的 target 来创建 Thread 对象,这个 Thread 对象才是真正的线程对象。
  3. 依然是通过调用线程对象的 start() 来启动线程。
public class MyThread implements Runnable {//实现Runnable接口
  public void run(){
  //重写run方法
  }
}
public class Main {
  public static void main(String[] args){
    //创建并启动线程
    MyThread myThread=new MyThread();
    Thread thread=new Thread(myThread);
    thread().start();
    //或者new Thread(new MyThread()).start();
  }
}

3️⃣------------------------使用Callable和Future创建线程---------------------

不同于 Runnable 接口,Callable 接口提供了一个 call() 为线程的执行体,call() 比 run() 功能要强大:

  1. call() 可以有返回值;
  2. call() 可以声明抛出异常。

Java5 提供了 Future 接口来代表 Callable 接口里 call() 的返回值,并且为 Future 接口提供了一个实现类 FutureTask,这个实现类既实现了 Future 接口,还实现了 Runnable 接口,因此可以作为 Thread 类的 target。在 Future 接口里定义了几个公共方法来控制它关联的 Callable 任务:

  1. boolean cancel(boolean mayInterruptIfRunning):试图取消该 Future 里面关联的 Callable 任务。
  2. get():返回 Callable 里 call() 的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值。
  3. get(long timeout,TimeUnit unit):返回 Callable 里 call() 的返回值,最多阻塞 timeout 时间,经过指定时间没有返回抛出 TimeoutException。
  4. boolean isDone():若 Callable 任务完成,返回 true。
  5. boolean isCancelled():如果在 Callable 任务正常完成前被取消,返回 true。

创建并启动有返回值的线程的步骤如下:

  1. 创建 Callable 接口的实现类,并实现 call(),然后创建该实现类的实例(从 Java8 开始可以直接使用 Lambda 表达式创建 Callable 对象)。
  2. 使用 FutureTask 类来包装 Callable 对象及 call() 的返回值。
  3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动线程(因为 FutureTask 实现了 Runnable 接口)。
  4. 调用 FutureTask 对象的 get() 来获得子线程执行结束后的返回值。
public class Main {
  public static void main(String[] args){
   //使用Lambda表达式创建Callable对象
    //使用FutureTask类来包装Callable对象
   FutureTask<Integer> task = new FutureTask<Integer>(
    (Callable<Integer>)()->{
      return 5;
    }
    );
   new Thread(task,"有返回值的线程").start();
   //实质上还是以Callable对象来创建并启动线程
    try{
    System.out.println("子线程的返回值:"+task.get());
    //get()方法会阻塞,直到子线程执行结束才返回
    }catch(Exception e){
    ex.printStackTrace();
   }
  }
}

4️⃣----------使用ExecutorService、Callable、Future实现有返回结果的线程--------

ExecutorService、Callable、Future 三个接口实际上都是属于 Executor 框架。返回结果的线程是在 JDK1.5 中引入的新特征,有了这种特征就不需要再为了得到返回值而大费周折了。而且自己实现了也可能漏洞百出。有返回值的任务必须实现 Callable 接口。类似的,无返回值的任务必须实现 Runnable 接口。
执行 Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object
注意:get()是阻塞的,线程无返回结果,get()会一直等待。
再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。如下是一个完整的有返回结果的多线程例子:

import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;

//有返回值的线程
@SuppressWarnings("unchecked")
public class Test {
    public static void main(String[] args) throws ExecutionException,
            InterruptedException {
        System.out.println("----程序开始运行----");
        long start = System.currentTimeMillis();
        int taskSize = 5;
        // 创建一个线程池
        ExecutorService pool = Executors.newFixedThreadPool(taskSize);
        // 创建多个有返回值的任务
        List<Future> list = new ArrayList<Future>();
        for (int i = 0; i < taskSize; i++) {
            Callable c = new MyCallable(i + " ");
            // 执行任务并获取Future对象
            Future f = pool.submit(c);
            list.add(f);
        }
        // 关闭线程池
        pool.shutdown();
        // 获取所有并发任务的运行结果
        for (Future f : list) {
            // 从Future对象上获取任务的返回值,并输出到控制台
            System.out.println(">>>" + f.get().toString());
        }
        long end = System.currentTimeMillis();
        System.out.println("----程序结束运行----,程序运行时间【" + (end - start) + "毫秒】");
    }
}
class MyCallable implements Callable<Object> {
    private String taskNum;
    MyCallable(String taskNum) {
        this.taskNum = taskNum;
    }
    @Override
    public Object call() throws Exception {
        System.out.println(">>>" + taskNum + "任务启动");
        long start = System.currentTimeMillis();
        Thread.sleep(1000);
        long end = System.currentTimeMillis();
        System.out.println(">>>" + taskNum + "任务终止");
        return taskNum + "任务返回运行结果,当前任务时间【" + (end - start) + "毫秒】";
    }
}

二、对比

Runnable 接口中的 run() 的返回值是 void,它做的事情只是纯粹地去执行 run() 中的代码而已;Callable 接口中的 call() 是有返回值的,是一个泛型,和 Future、FutureTask 配合可以用来获取异步执行的结果。

这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了,某条线程执行了多久,某条线程执行时期望的数据是否已经赋值完毕。无法得知,能做的只是等待这条多线程的任务执行完毕而已。而 Callable+Future/FutureTask 却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务。

实现 Runnable 接口和实现 Callable 接口的方式基本相同。可以把这两种方式归为一种,这种方式与继承 Thread 类的方法之间的差别如下:

  1. 线程只是实现 Runnable 接口或实现 Callable 接口,还可以继承其他类。
  2. 这种方式下,多个线程可以共享一个 target 对象,非常适合多线程处理同一份资源的情形。
  3. 编程稍微复杂。如果需要访问当前线程,必须调用 Thread.currentThread()。
  4. 继承 Thread 类的线程类不能再继承其他父类( Java 单继承)。

注:一般推荐采用实现接口的方式来创建多线程

对于线程的生命周期,一tu

三、线程池的使用

基础知识

1、Executors创建线程池

Java中创建线程池很简单,只需要调用Executors中相应的便捷方法即可,比如Executors.newFixedThreadPool(int nThreads),但是便捷不仅隐藏了复杂性,也为我们埋下了潜在的隐患(OOM,线程耗尽)。

Executors创建线程池便捷方法列表:

方法名功能
newFixedThreadPool(int nThreads)创建固定大小的线程池
newSingleThreadExecutor()创建只有一个线程的线程池
newCachedThreadPool()创建一个不限线程数上限的线程池,任何提交的任务都将立即执行
注:固定数量线程池的弊端:内部使用无界队列来存放排队任务
“固定数量的线程池”的适用场景:需要任务长期执行的场景。“固定数量的线程池”的线程
数能够比较稳定保证一个数,能够避免频繁回收线程和创建线程,故适用于处理 CPU 密集型的任
务,在 CPU 被工作线程长时间使用的情况下,能确保尽可能少的分配线程。
“固定数量的线程池”的弊端:内部使用无界队列来存放排队任务,当大量任务超过线程池
最大容量需要处理时,队列无线增大,使服务器资源迅速耗尽。
“可缓存线程池”的适用场景:需要快速处理突发性强、耗时较短的任务场景,如 Netty
NIO 处理场景、 REST API 接口的瞬时削峰场景。“可缓存线程池”的线程数量不固定,只要有空
闲线程就会被回收;接收到的新异步任务执行目标,查看是否有线程处于空闲状态,如果没有就
直接创建新的线程。
“可缓存线程池”的弊端:线程池没有最大线程数量限制,如果大量的异步任务执行目标实
例同时提交,可能导致创线程过多会而导致资源耗尽。
 
大部分企业的开发规范,都会禁止使用快捷线程池(具体原因稍后介绍),要求通过标准构
造器 ThreadPoolExecutor 去构造工作线程池。实质上,Executors 工厂类中创建线程池的快捷工厂 方法,实际上是调用了 ThreadPoolExecutor (定时任务使用 ScheduledThreadPoolExecutor )线程池的构造方法完成的。

小程序使用这些快捷方法没什么问题,对于服务端需要长期运行的程序,创建线程池应该直接使用ThreadPoolExecutor的构造方法。没错,上述Executors方法创建的线程池就是ThreadPoolExecutor

2、ThreadPoolExecutor构造方法

Executors中创建线程池的快捷方法,实际上是调用了ThreadPoolExecutor的构造方法(定时任务使用的是ScheduledThreadPoolExecutor),该类构造方法参数列表如下:

// Java线程池的完整构造函数
public ThreadPoolExecutor(
  int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。
  int maximumPoolSize, // 线程数的上限
  long keepAliveTime, TimeUnit unit, // 超过corePoolSize的线程的idle时长,
                                     // 超过这个时间,多余的线程会被回收。
  BlockingQueue<Runnable> workQueue, // 任务的排队队列
  ThreadFactory threadFactory, // 新线程的产生方式
  RejectedExecutionHandler handler) // 拒绝策略

竟然有7个参数,很无奈,构造一个线程池确实需要这么多参数。这些参数中,比较容易引起问题的有corePoolSizemaximumPoolSizeworkQueue以及handler

  • corePoolSizemaximumPoolSize设置不当会影响效率,甚至耗尽线程;
  • workQueue设置不当容易导致OOM;
  • handler设置不当会导致提交任务时抛出异常。

正确的参数设置方式会在下文给出。

3、线程池的工作顺序

If fewer than corePoolSize threads are running, the Executor always prefers adding a new thread rather than queuing.
If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread.
If a request cannot be queued, a new thread is created unless this would exceed maximumPoolSize, in which case, the task will be rejected.

corePoolSize -> 任务队列 -> maximumPoolSize -> 拒绝策略

Runnable和Callable

可以向线程池提交的任务有两种:RunnableCallable,二者的区别如下:

  1. 方法签名不同,void Runnable.run()V Callable.call() throws Exception
  2. 是否允许有返回值,Callable允许有返回值
  3. 是否允许抛出异常,Callable允许抛出异常。

Callable是JDK1.5时加入的接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。

4、三种提交任务的方式:

提交方式是否关心返回结果
Future<T> submit(Callable<T> task)
void execute(Runnable command)
Future<?> submit(Runnable task)否,虽然返回Future,但是其get()方法总是返回null

四、如何正确使用线程池

避免使用无界队列

不要使用Executors.newXXXThreadPool()快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免OOM,我们应该使用ThreadPoolExecutor的构造方法手动指定队列的最大长度:

ExecutorService executorService = new ThreadPoolExecutor(2, 2, 
				0, TimeUnit.SECONDS, 
				new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOM
				new ThreadPoolExecutor.DiscardPolicy());

明确拒绝任务时的行为

任务队列总有占满的时候,这是再submit()提交新的任务会怎么样呢?RejectedExecutionHandler接口为我们提供了控制方式,接口定义如下:

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

线程池给我们提供了几种常见的拒绝策略:

拒绝策略拒绝行为
AbortPolicy抛出RejectedExecutionException
DiscardPolicy什么也不做,直接忽略
DiscardOldestPolicy丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置
CallerRunsPolicy直接由提交任务者执行这个任务

线程池默认的拒绝行为是AbortPolicy,也就是抛出RejectedExecutionHandler异常,该异常是非受检异常,很容易忘记捕获。如果不关心任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy,这样多余的任务会悄悄的被忽略。

ExecutorService executorService = new ThreadPoolExecutor(2, 2, 
				0, TimeUnit.SECONDS, 
				new ArrayBlockingQueue<>(512), 
				new ThreadPoolExecutor.DiscardPolicy());// 指定拒绝策略

获取处理结果和异常

线程池的处理结果、以及处理过程中的异常都被包装到Future中,并在调用Future.get()方法时获取,执行过程中的异常会被包装成ExecutionExceptionsubmit()方法本身不会传递结果和任务执行过程中的异常。获取执行结果的代码可以这样写:

ExecutorService executorService = Executors.newFixedThreadPool(4);
Future<Object> future = executorService.submit(new Callable<Object>() {
        @Override
        public Object call() throws Exception {
            throw new RuntimeException("exception in call~");// 该异常会在调用Future.get()时传递给调用者
        }
    });
	
try {
  Object result = future.get();
} catch (InterruptedException e) {
  // interrupt
} catch (ExecutionException e) {
  // exception in Callable.call()
  e.printStackTrace();
}

上述代码输出类似如下:

线程池的常用场景

正确构造线程池

int poolSize = Runtime.getRuntime().availableProcessors() * 2;
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(512);
RejectedExecutionHandler policy = new ThreadPoolExecutor.DiscardPolicy();
executorService = new ThreadPoolExecutor(poolSize, poolSize,
    0, TimeUnit.SECONDS,
            queue,
            policy);

获取单个结果

submit()向线程池提交任务后会返回一个Future,调用V Future.get()方法能够阻塞等待执行结果,V get(long timeout, TimeUnit unit)方法可以指定等待的超时时间。

获取多个结果

如果向线程池提交了多个任务,要获取这些任务的执行结果,可以依次调用Future.get()获得。但对于这种场景,我们更应该使用ExecutorCompletionService,该类的take()方法总是阻塞等待某一个任务完成,然后返回该任务的Future对象。向CompletionService批量提交任务后,只需调用相同次数的CompletionService.take()方法,就能获取所有任务的执行结果,获取顺序是任意的,取决于任务的完成顺序:

void solve(Executor executor, Collection<Callable<Result>> solvers)
   throws InterruptedException, ExecutionException {
   
   CompletionService<Result> ecs = new ExecutorCompletionService<Result>(executor);// 构造器
   
   for (Callable<Result> s : solvers)// 提交所有任务
       ecs.submit(s);
	   
   int n = solvers.size();
   for (int i = 0; i < n; ++i) {// 获取每一个完成的任务
       Result r = ecs.take().get();
       if (r != null)
           use(r);
   }
}

单个任务的超时时间

V Future.get(long timeout, TimeUnit unit)方法可以指定等待的超时时间,超时未完成会抛出TimeoutException

多个任务的超时时间

等待多个任务完成,并设置最大等待时间,可以通过CountDownLatch完成:

public void testLatch(ExecutorService executorService, List<Runnable> tasks) 
	throws InterruptedException{
      
    CountDownLatch latch = new CountDownLatch(tasks.size());
      for(Runnable r : tasks){
          executorService.submit(new Runnable() {
              @Override
              public void run() {
                  try{
                      r.run();
                  }finally {
                      latch.countDown();// countDown
                  }
              }
          });
      }
	  latch.await(10, TimeUnit.SECONDS); // 指定超时时间
  }

结合异常和等待的标准写法

public Resp getDatasourceListByOu(String ouId) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        String Path1 = "url1";
        String Path2 = "url2";
        String Path3 = "url3";
        String Path4 = "url4";
        List<String> urlList = new ArrayList<>();
        if (!"".equals(Path1)) {
            urlList.add(Path1);
        }
        if (!"".equals(Path2)) {
            urlList.add(Path2);
        }
        if (!"".equals(Path3)) {
            urlList.add(Path3);
        }
        if (!"".equals(Path4)) {
            urlList.add(Path4);
        }

        List<FutureTask<String>> taskList = new ArrayList<>();

        CountDownLatch latch = new CountDownLatch(urlList.size());

        for (String uri : urlList) {
            FutureTask<String> futureTask = new FutureTask<>(new GetDataSourceListTask(ouId, uri, latch));
            taskList.add(futureTask);
            executorService.submit(futureTask);
        }
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String typeResult = "";
        List<String> resultList = new ArrayList<>();
        Resp resp = new Resp();
        for (FutureTask<String> task : taskList) {
            try {
                typeResult = task.get();
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
            resp = JSON.parseObject(typeResult, resp.class);
            List<String> typeList = resp.getData();
            for (String typeNum : typeList) {
            	resultList.add(typeNum);                    
            }
        }
        if (resp.getCode() == 200) {
            return Resp.success(resultList);
        } else {
            return Resp.error(queryDataSourceTypeResp.getCode());
        }
    }
public class GetDataSourceListTask implements Callable<String> {

    private final String ouId;
    private final String url;
    private final CountDownLatch latch;

    public GetDataSourceListTask(String ouId, String url, CountDownLatch latch) {
        this.ouId = ouId;
        this.url = url;
        this.latch = latch;
    }


    @Override
    public String call() throws Exception {
        Map<String,String> map = new HashMap<>();
        map.put("ouId",ouId);
        String s = HttpClientUtil.doGet(url, map);
        latch.countDown();
        return s;
    }
}

线程池和装修公司

以运营一家装修公司做个比喻。公司在办公地点等待客户来提交装修请求;公司有固定数量的正式工以维持运转;旺季业务较多时,新来的客户请求会被排期,比如接单后告诉用户一个月后才能开始装修;当排期太多时,为避免用户等太久,公司会通过某些渠道(比如人才市场、熟人介绍等)雇佣一些临时工(注意,招聘临时工是在排期排满之后);如果临时工也忙不过来,公司将决定不再接收新的客户,直接拒单。

线程池就是程序中的“装修公司”,代劳各种脏活累活。上面的过程对应到线程池上:

// Java线程池的完整构造函数
public ThreadPoolExecutor(
  int corePoolSize, // 正式工数量
  int maximumPoolSize, // 工人数量上限,包括正式工和临时工
  long keepAliveTime, TimeUnit unit, // 临时工游手好闲的最长时间,超过这个时间将被解雇
  BlockingQueue<Runnable> workQueue, // 排期队列
  ThreadFactory threadFactory, // 招人渠道
  RejectedExecutionHandler handler) // 拒单方式

总结

Executors为我们提供了构造线程池的便捷方法,对于服务器程序我们应该杜绝使用这些便捷方法,而是直接使用线程池ThreadPoolExecutor的构造方法,避免无界队列可能导致的OOM以及线程个数限制不当导致的线程数耗尽等问题。ExecutorCompletionService提供了等待所有任务执行结束的有效方式,如果要设置等待的超时时间,则可以通过CountDownLatch完成。

五、springBoot的线程池管理

springboot的线程池配置

创建一个配置类ExecutorConfig,用来定义如何创建一个ThreadPoolTaskExecutor,要使用@Configuration和@EnableAsync这两个注解,表示这是个配置类,并且是线程池的配置类,如下所示:

@Configuration
@EnableAsync
public class ExecutorConfig {

    private static final Logger logger = LoggerFactory.getLogger(ExecutorConfig.class);

    @Bean
    public Executor asyncServiceExecutor() {
        logger.info("start asyncServiceExecutor");
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //配置核心线程数
        executor.setCorePoolSize(5);
        //配置最大线程数
        executor.setMaxPoolSize(5);
        //配置队列大小
        executor.setQueueCapacity(99999);
        //配置线程池中的线程的名称前缀
        executor.setThreadNamePrefix("async-service-");

        // rejection-policy:当pool已经达到max size的时候,如何处理新任务
        // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //执行初始化
        executor.initialize();
        return executor;
    }
}

一般搭配注解来实现,非常便捷

API性能优化之异步_人工智的博客-CSDN博客_异步api

数据库线程池我们可以使用阿里的druid,非常强大,监控也是非完备

MyBatis集成Druid实现数据库线程池管理(一)_java界的小学生-CSDN博客_mybatis集成druid

线程池关键上线文传递

线程本地变量是实现无锁编程的关键:首先要了解ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal这三个实现。这个不是本文的重点后续会专门梳理

ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal三者之间区别_mztBang的博客-CSDN博客

Java 多线程上下文传递在复杂场景下的实践_vivo互联网技术-CSDN博客_多线程上下文传递 

关键:线程池参数设置

IO密集型

        由于 IO 密集型任务的 CPU 使用率较低,导致线程空余时间很多,所以通常就需要开 CPU

核心数两倍的线程。当 IO 线程空闲时,可以启用其他线程继续使用 CPU ,以提高 CPU 的使用
率;
CPU 密集型
        CPU 密集型任务也叫计算密集型任务,其特点是要进行大量计算而需要消耗 CPU 资源,比
如计算圆周率、对视频进行高清解码等等。 CPU 密集型任务虽然也可以并行完成,但是并行的任
务越多,花在任务切换的时间就越多, CPU 执行任务的效率就越低,所以,要最高效地利用 CPU , CPU 密集型任务的并行执行的数量应当等于 CPU 的核心数。 比如说 4 个核心的 CPU ,通过 4 个线程并行执行 4 CPU 密集型任务,此时的效率是最高的。但是如果线程数远远超出 CPU 核心数量,需要频繁的切换线程,线程上下文切换时需要消耗时间的,反而会使得任务效率下降。因此对于 CPU 密集型的任务来说,线程数等于 CPU 数就行。
混合型
        为混合型任务确定线程数。混合型任务既要执行逻辑计算,又要进行大量非 CPU 耗时操作(如 RPC 调用、数据库访问、 网络通信等),所以,混合型任务 CPU 利用率不是太高,非 CPU 耗时往往是 CPU 耗时的数倍。 比如在 Web 应用处理 HTTP 请求处理时,一次请求处理会包括 DB 操作、 RPC 操作、缓存操作等 多种耗时操作。一般来说,一次 Web 请求的 CPU 计算耗时往往较少,大致在 100ms-500ms 之间, 而其他耗时操作会占用 500ms-1000ms 甚至更多的时间。
在为混合型任务创建线程池时,如何确定线程数呢?业界有一个比较成熟的估算公式,具体
如下:
最佳线程数 = ((线程等待时间 + 线程 CPU 时间) / 线程 CPU 时间 ) * CPU 核数
经过简单的换算,以上公式可进一步转换为:
最佳线程数目 = (线程等待时间与线程 CPU 时间之比 + 1 * CPU 核数
通过公式可以看出:等待时间所占比例越高,需要越多线程; CPU 耗时所占比例越高,需要
越少线程。下面举个例子:比如在 Web 服务器处理 HTTP 请求时,假设平均线程 CPU 运行时间
100ms ,而线程等待时间(比如包括 DB 操作、 RPC 操作、缓存操作等)为 900ms ,如果 CPU
核数为 8 ,那么根据上面这个公式,估算如下:
900ms+100ms /100ms*8= 10*8 = 80
经过计算,以上案例中需要的线程数为 80 。很多的小伙伴认为,线程数越高越好。那么,使
用很多线程是否就一定比单线程高效呢?答案是否定的,比如大名鼎鼎的 Redis 就是单线程的,
但它却非常高效,基本操作都能达到十万量级 / 秒。
为什么redis快?  就是因为他io多,响应快,cpu可以充分利用,一个线程可以节省很多上下文切换时间
-----  觉得多数从其他博文拷贝而来,权作记录。找不到原文链接,如有冒犯,请联系我
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值