线程并发-1. 线程池

标注:本文为个人整理,仅做自己学习参考使用,请勿转载和转发
2018-05-29: 初稿
2018-06-03: 第一次整理

1. 前言

  • 首先介绍进程和线程之间的关系,其中进程是最小的程序的单位,而线程为CPU执行的最小单位,也就是在该进程的基础上,可以执行相应功能的单位
  • 一个进程不可能只有一个线程,实现异步任务,或者耗时操作还是需要开启其他的线程来实现其他功能的。
  • 创建新的线程的方法可以通过new Thread方法来实现,记得在最后添加.start()方法哦!要不线程是起不起来的哈!
    new Thread(new Runnable() {
    
    @Override
    public void run() {
    // TODO Auto-generated method stub
    }
    }).start();
    
    • 该种方法的弊端:
      1)每次new Thread新建对象性能差。
      2)线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
      3)缺乏更多功能,如定时执行、定期执行、线程中断。
  • 创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率,记创建线程消耗时间T1,执行任务消耗时间T2,销毁线程消耗时间T3,如果T1+T3>T2,那么是不是说开启一个线程来执行这个任务太不划算了!正好,线程池缓存线程,可用已有的闲置线程来执行新任务,避免了T1+T3带来的系统开销。

2. 线程池简介

  • 线程池优点,线程池会很好的解决上述问题,其中本文主要介绍的为Java中的线程池的使用,可以使用在Android中,该方法的主要使用有以下优点
    1)重用存在的线程,减少对象创建、消亡的开销,性能佳。
    2)可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
    3)提供定时执行、定期执行、单线程、并发数控制等功能。
  • 线程池分类,在Java中Executors一共提供了4中线程池,主要分为:
    1)newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
    2)newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
    3)newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
    4)newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  • 线程池参数,在Java中,线程池的概念是Executor这个接口,具体实现为ThreadPoolExecutor类,对线程池的配置就是对ThreadPoolExecutor的构造参数的配置
2.1. Executors构造方法参数简介
  • 构造方法,ThreadPoolExecutor提供了四个构造函数
//五个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue)

//六个参数的构造函数-1
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory)

//六个参数的构造函数-2
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler)

//七个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • 上面的构造方法中的参数比较多,但是对于参数来数主要有几个核心的参数需要了解
2.1.1 int corePoolSize 该线程池中的核心线程数的最大值
  • 但是什么是核心线程呢,线程池在新建线程的时候如果当前的线程的总数小于corePoolSize,表示该建立的线程是核心线程,如果超出了这个大小,表示建立的线程不是核心线程。
  • 核心线程有什么用呢,核心线程可以一直存在当前的线程池中,即使当前的线程处于闲置状态。
  • 怎么改变这种情况呢,可以通过设定ThreadPoolExecutor的allowCoreThreadTimeOut的值为true,即对核心线程设置了超时时间,若核心线程闲置超出时间的话,会被销毁。
2.1.2 int maximumPoolSize 该线程池中线程总数最大值
  • 核心线程 + 非核心线程 = 线程总数,也就是对核心线程以及非核心的线程的总数进行了一定的限制。
2.1.3 long keepAliveTime 该线程池中非核心线程闲置超时时长
  • 针对于非核心线程,若当前的非核心的线程闲置状态超出于当前设定的keepAliveTime值的话,会被销毁。
  • 针对核心线程,需要将allowCoreThreadTimeOut = true,会作用于核心线程。
2.1.4 TimeUnit unit 该线程池中超时时长的单位,为枚举类型
  • NANOSECONDS : 1微毫秒 = 1微秒 / 1000
  • MICROSECONDS : 1微秒 = 1毫秒 / 1000
  • MILLISECONDS : 1毫秒 = 1秒 /1000
  • SECONDS : 秒
  • MINUTES : 分
  • HOURS : 小时
  • DAYS : 天
2.1.5 BlockingQueue<Runnable> workQueue 该线程池中的任务队列
  • 该任务队列维护着等待执行的Runnable对象,当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。
  • 常见的workQueue的类型:
    1)SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大
    2)LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize
    3)ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误
    4)DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务
2.1.6 ThreadFactory threadFactory 该线程池的创建线程的方式
  • 怎么说呢,这个是个接口,实现的话需要实现Thread newThread(Runnable r)的方法,一般情况用不上。
  • 举个例子🌰,AsyncTask源码就是对线程池的封装,然后就是改了个名...
new ThreadFactory() {
    private final AtomicInteger mCount = new AtomicInteger(1);
    
    public Thread new Thread(Runnable r) {
        return new Thread(r,"AsyncTask #" + mCount.getAndIncrement());
    }
}
2.1.7 RejectedExecutionHandler handler 该线程池抛出异常专用
  • 用于上文中遇到的错误中抛出的错误的处理,但是对于错误的单独的处理还不会啊!一般也用不上。
  • // TODO 对错误的单独的处理
2.2 初始化线程池
  • 构造方法就是初始化线程池的嘛,对于如何初始化线程池,一般只用5个参数的构造方法
  • 先初始化一个对象,竟然不是new出来的,不同类型的线程池的构造方法不同,举例说明🌰
    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
  • 使用方法,添加一个任务到线程池,但是这个run方法竟然不用.start()耶
    cachedThreadPool.execute(Runnable command) { ... 实现run方法 ... }
2.3 常见使用的四类线程池
  • Java通过Executors提供了四种线程池,这四种线程池都是直接或间接配置ThreadPoolExecutor的参数实现的,如果当前的这四种线程池没有办法满足需要的话,需要自己实现了....,感觉好难
2.3.1 CachedThreadPool 提供可缓存线程池机制
  • 线程数无限制
  • 有空闲线程则复用空闲线程,若无空闲线程则新建线程
  • 一定程序减少频繁创建/销毁线程,减少系统开销
  • 举个例子🌰
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
    final int index = i;
    try {
        Thread.sleep(index * 1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 这就是执行该可缓存的线程了
    cachedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println(index);
        }
    });
}
  • 具体的实现的源码
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
2.3.2 newFixedThreadPool 提供定长线程池,控制并发数
  • 可控制线程最大并发数(同时执行的线程数)
  • 超出的线程会在队列中等待
  • 举个栗子🌰
// 构建定长线程池,最大线程数为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
    final int index = i;
    // 执行任务线程,但是允许同时执行的线程数为3个
    fixedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(index);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();    
            }
        }
    });
}
  • 具体的实现的源码
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
2.3.3 newScheduledThreadPool 提供定长线程池,定时及周期性任务执行
  • 举个栗子🌰
// 创建定长线程池,定时周期性执行任务,最大线程数为5个
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println("delay 3 seconds");
    }
}, 3, TimeUnit.SECONDS);        // 延迟3秒执行
  • 具体的实现的源码
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

// ScheduledThreadPoolExecutor():
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}
2.3.4 SingleThreadExecutor 提供单线程化的线程池
  • 有且仅有一个工作线程执行任务
  • 所有任务按照指定顺序(FIFO, LIFO, 优先级)执行,即遵循队列的入队出队规则
  • 具体的实现的源码
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

3. 总结

3.1 线程池的作用:
  • 线程池作用就是限制系统中执行线程的数量。
  • 根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。
  • 用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
3.2 为什么要用线程池
  • 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  • 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
  • Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
  • 比较重要的几个类:
    1)ExecutorService:真正的线程池接口。
    2)ScheduledExecutorService:能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
    3)ThreadPoolExecutor:ExecutorService的默认实现。
    4)ScheduledThreadPoolExecutor:继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。
  • 要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。

4. 参考文献

[1] 线程池,这一篇或许就够了-LiuZh_
[2] Java 四种线程池-星辰之力

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容