当前位置:主页 > 软件编程 > JAVA代码 >

JAVA线程池专题(概念和作用)

时间:2020-10-30 16:33:34 | 栏目:JAVA代码 | 点击:

线程池的作用

我们在用一个东西的时候,首先得搞明白一个问题。这玩意是干嘛的,为啥要用这个,用别的不行吗。那么一个一个解决这些问题

我们之前都用过数据库连接池,线程池的作用和连接池有点类似,频繁的创建,销毁线程会造成大量的不必要的性能开销,所以这个时候就出现了一个东西统一的管理线程,去负责线程啥时候销毁,啥时候创建,以及维持线程的状态,当程序需要使用线程的时候,直接从线程池拿,当程序用完了之后,直接把线程放回线程池,不需要去管线程的生命周期,专心的执行业务代码就行。

当然,如果非要是自己想手动new一个线程来执行,也不是不可以,只是像上面说的那样,第一麻烦,第二开销大,第三不好控制。

控制线程的方法

在说到线程池之前,首先要提到一个创建线程池的工具类,又或者说是工厂类 Executors 通过这个线程可以统一的创建线程,返回的是一个ExecutorService 类这个类中包含了一些对线程执行过程进行管理控制的方法;

void execute(Runnable command); 这个方法是将任务提交到线程池进行执行。这个方法没有返回值。

<T> Future<T> submit(Callable<T> task); 这个方法最特别的地方是线程执行完毕之后是有返回值的,另外方法的参数可以用Callable也可以为Runnable。可以适用于一些后续的代码,需要线程执行结果的程序。

下面的示例中,我们创建了一个 ExecutorService 的实例,提交了一个任务,然后使用返回的 Future 的 get() 方法等待提交的任务完成并返回值。

 ExecutorService executorService = Executors.newFixedThreadPool(10);
 Future<String> future = executorService.submit(() -> "Hello World");
 // 一些其它操作
 String result = future.get();

在实际使用时,我们并不会立即调用 future.get() 方法,可能会等待一些时间,推迟调用它直到我们需要它的值用于计算等目的。

ExecutorService 中的 submit() 方法被重载为支持 RunnableCallable ,它们都是功能接口,可以接收一个 lambdas 作为参数( 从 Java 8 开始 ):

如果想让编译器将参数推断为 Callable 类型,只需要 lambda 返回一个值即可。

这两种方法的使用场景:如果线程中的任务相互之间没有什么关联某个线程的异常对结果影响不大。那么所有线程都能在执行任务结束之后可以正常结束,程序能在所有task都做完之后正常退出,适合用ShutDown。但是,如果一个线程在做某个任务的时候失败,则整个结果就是失败的,其他worker再继续做剩下的任务也是徒劳,这就需要让他们全部停止当前的工作。这里使用ShutDownNow就可以让该pool中的所有线程都停止当前的工作,从而迫使所有线程执行退出。从而让主程序正常退出。

线程池的分类

通过工厂类 Executors 通过这个线程可以根据自己的需要统一的创建各种类型的线程,线程的分类大致分为以下四种:

  1. newSingleThreadExecutor
  2. CachedThreadPool
  3. newFixedThreadPool
  4. newScheduledThreadPool
public static ExecutorService newSingleThreadExecutor() {
  return new FinalizableDelegatedExecutorService
   (new ThreadPoolExecutor(1, 1,
         0L, TimeUnit.MILLISECONDS,
         new LinkedBlockingQueue<Runnable>()));
 }
public class SinglePoolDemo {
 public static void main(String[] args) {
  ExecutorService pool = Executors.newSingleThreadExecutor();
//  ExecutorService pool = Executors.newFixedThreadPool(2);
  for (int i = 0; i < 10; i++) {
   int finalI = i;
   pool.execute(() -> {
    System.out.println(Thread.currentThread().getName()+"----"+ finalI);
   });
  }
 }
}

输出结果:

pool-1-thread-1----0
pool-1-thread-1----1
pool-1-thread-1----2
pool-1-thread-1----3
pool-1-thread-1----4
pool-1-thread-1----5
pool-1-thread-1----6
pool-1-thread-1----7
pool-1-thread-1----8
pool-1-thread-1----9

观察线程编号,可以发现,自始自终都只有一个线程在执行,并且也是按照顺序来执行的,。

public static ExecutorService newCachedThreadPool() {
  return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
          60L, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>());
 }
public class CachePoolDemo {
 public static void main(String[] args) {

  ExecutorService pool = Executors.newCachedThreadPool();
  for (int i = 0; i < 20000; i++) {
   int finalI = i;
   pool.submit(() -> {
    System.out.println(Thread.currentThread().getName()+"-------------"+finalI);
   });
  }
 }
}

运行结果部分:

......
pool-1-thread-1805-------------19760
pool-1-thread-1806-------------19783
pool-1-thread-1809-------------19875
pool-1-thread-1810-------------19951
pool-1-thread-1811-------------19980

以上的代码我们运行了2w次线程任务,如果是按照我们之前的做法的话,我们要new 2w的线程去执行。通过这个不定长的线程池,他可以根据任务数来灵活的分配所创建的线程,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程,所以这里只创建了大概1800多个线程就完成了我们原本需要new 2w个线程才能完成的任务,之所以说他是灵活分配的是因为,可以这样验证看看,把i的值改为20的话,所创建的线程数量大概是10以内,因此是根据任务数量来自行创建线程数的,可以保证效率和性能的最大化。

但是经过实测,这个灵活性虽然最高,但是性能貌似是相对比较差的,在两万任务数的条件下,所以他的缺点就是,可能会创建大量的线程。当然线程池这东西是需要根据自身情况来选择的。如果主线程提交任务的速度远远大于CachedThreadPool的处理速度,则CachedThreadPool会不断地创建新线程来执行任务,这样有可能会导致系统耗尽CPU和内存资源,所以在使用该线程池是,一定要注意控制并发的任务数,否则创建大量的线程可能导致严重的性能问题。

 public static ExecutorService newFixedThreadPool(int nThreads) {
  return new ThreadPoolExecutor(nThreads, nThreads,
          0L, TimeUnit.MILLISECONDS,
          new LinkedBlockingQueue<Runnable>());
 }
public class FixedPoolDemo {
 public static void main(String[] args) {
  ExecutorService pool = Executors.newFixedThreadPool(10);
//  ExecutorService pool = Executors.newFixedThreadPool(2);
  for (int i = 0; i < 1000; i++) {
   int finalI = i;
   pool.execute(() -> {
    System.out.println(Thread.currentThread().getName()+"----"+ finalI);
   });
  }
 }
}

从运行结果可以看出,线程池一直都是维持着十个线程

.....
pool-1-thread-5----882
pool-1-thread-1----881
pool-1-thread-4----865
pool-1-thread-10----989
pool-1-thread-3----931
pool-1-thread-2----934
pool-1-thread-9----910
pool-1-thread-6----896

 public ScheduledThreadPoolExecutor(int corePoolSize) {
  super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
    new DelayedWorkQueue());
 }

以上四种线程池,各有优劣点

newFixedThreadPool、newSingleThreadExecutor:

主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。

newCachedThreadPool、newScheduledThreadPool:

主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

阿里线程池规范

  1. 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
  2. FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
  3. CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

总结

本篇文章首先我们知道了线程池有什么好处,然后了解一些线程的执行方法,submit,execute,shutdown以及他们的区别,用法等等,然后对几种线程池做了一个大概的介绍,以及他们的作用,好处和弊端。如果看的细心的同学可以看代码发现,这些线程池其实本质上都是通过创建一个 ThreadPoolExecutor ,包括阿里的线程池规范也是建议用ThreadPoolExecutor ,但是本篇文章只是对线程池的作用以及分类做一个概述,在下篇文章中,将会详细的讲一下ThreadPoolExecutor

您可能感兴趣的文章:

相关文章