4种常用线程池

一. 线程池简介

  1. 线程池的概念 ​ 线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

  2. 线程池的工作机制

    • 在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。
    • 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
  3. 使用线程池的原因 ​ 多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,过渡切换线程的危险可能导致系统资源的崩溃。这时,线程池就是最好的选择了。

  4. 线程池的优势

    1. 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
    2. 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
    3. 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
    4. 提供更强大的功能,延时定时线程池。

二. 四种常见的线程池详解

  1. 线程池的返回值ExecutorService简介 ​ ExecutorService是Java提供的用于管理线程池的接口。该接口的两个作用:控制线程数量和重用线程

  2. 具体的4种常用的线程池实现如下:(返回值都是ExecutorService)

newCacheThreadPool()

Executors.newCacheThreadPool():可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务

/**
 * 可缓存线程池
 */
public class NewCachedThreadPoolTest {

    public static void main(String[] args) {
        // 创建一个可缓存线程池
        System.out.println("创建一个可缓存线程池");
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            try {
                // sleep可明显看到使用的是线程池里面以前的线程,没有创建新的线程
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            cachedThreadPool.execute(new Runnable() {
                public void run() {
                    // 打印正在执行的缓存线程信息
                    System.out.println(Thread.currentThread().getName()
                        + "正在被执行");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

}

输出结果:

创建一个可缓存线程池
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-2正在被执行
pool-1-thread-2正在被执行
pool-1-thread-2正在被执行
pool-1-thread-2正在被执行
pool-1-thread-2正在被执行
pool-1-thread-1正在被执行
pool-1-thread-1正在被执行

线程池为无限大,当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程

newFixedThreadPool(int n)

Executors.newFixedThreadPool(int n):创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。

/**
 * 可重用固定个数的线程池
 */
public class NewFixedThreadPoolTest {
    public static void main(String[] args) {
        // 创建一个可重用固定个数的线程池
        System.out.println("创建一个可重用固定个数的线程池");
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            fixedThreadPool.execute(new Runnable() {
                public void run() {
                    try {
                        // 打印正在执行的缓存线程信息
                        System.out.println(Thread.currentThread().getName() + "正在被执行");
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

输出结果:

创建一个可重用固定个数的线程池
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-3正在被执行
pool-1-thread-1正在被执行
pool-1-thread-3正在被执行
pool-1-thread-2正在被执行

因为线程池大小为3,每个任务输出打印结果后sleep 2秒,所以每两秒打印3个结果。 定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()

newScheduledThreadPool(int n)

Executors.newScheduledThreadPool(int n):创建一个定长线程池,支持定时及周期性任务执行

/**
 * 定长线程池,支持定时及周期性任务执行——延迟执行
 */
public class NewScheduledThreadPoolTest {
    public static void main(String[] args) {
        // 创建一个定长线程池,支持定时及周期性任务执行——延迟执行
        System.out.println("创建一个定长线程池,支持定时及周期性任务执行——延迟执行");
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        // 延迟1秒执行
        /*
         * scheduledThreadPool.schedule(new Runnable() {
         * public void run() {
         * System.out.println("延迟1秒执行");
         * }
         * }, 1, TimeUnit.SECONDS);
         */

        // 延迟1秒后每3秒执行一次
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            public void run() {
                System.out.println("延迟1秒后每3秒执行一次:" + Thread.currentThread().getName() + "正在被执行");
            }
        }, 1, 3, TimeUnit.SECONDS);

    }

}

输出结果

创建一个定长线程池,支持定时及周期性任务执行——延迟执行
延迟1秒后每3秒执行一次:pool-1-thread-1正在被执行
延迟1秒后每3秒执行一次:pool-1-thread-1正在被执行
延迟1秒后每3秒执行一次:pool-1-thread-2正在被执行
延迟1秒后每3秒执行一次:pool-1-thread-1正在被执行

newSingleThreadExecutor()

Executors.newSingleThreadExecutor():创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

/**
 * 单线程化的线程池
 */
public class NewSingleThreadExecutorTest {
    public static void main(String[] args) {
        //创建一个单线程化的线程池
        System.out.println("创建一个单线程化的线程池");
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            singleThreadExecutor.execute(new Runnable() {
                public void run() {
                    try {
                        //结果依次输出,相当于顺序执行各个任务
                        System.out.println(Thread.currentThread().getName()+"正在被执行,打印的值是:"+index);
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

}

输出结果:

创建一个单线程化的线程池
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

自定义线程池ThreadPoolExecutor

缓冲队列BlockingQueue

BlockingQueue是双缓冲队列。BlockingQueue内部使用两条队列,允许两个线程同时向队列一个存储,一个取出操作。在保证并发安全的同时,提高了队列的存取效率。

常用的几种BlockingQueue:

  • ArrayBlockingQueue(int i):规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是FIFO顺序排序的。
  • LinkedBlockingQueue()或者(int i):大小不固定的BlockingQueue,若其构造时指定大小,生成的BlockingQueue有大小限制,不指定大小,其大小有Integer.MAX_VALUE来决定。其所含的对象是FIFO顺序排序的。
  • PriorityBlockingQueue()或者(int i):类似于LinkedBlockingQueue,但是其所含对象的排序不是FIFO,而是依据对象的自然顺序或者构造函数的Comparator决定。
  • SynchronizedQueue():特殊的BlockingQueue,对其的操作必须是放和取交替完成。

自定义线程池 ThreadPoolExecutor

自定义线程池,用ThreadPoolExecutor类创建,它有多个构造方法来创建线程池。常见的构造函数

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue)

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
  1. corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)

  2. maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。

  3. keepAliveTime(线程存活保持时间)当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。

  4. unit, 单位是和参数 3 存活时间配合使用的,合在一起用于设定线程的存活时间 ,参数 keepAliveTime 的时间单位有以下 7 种可选:

    • TimeUnit.DAYS:天
    • TimeUnit.HOURS:小时
    • TimeUnit.MINUTES:分
    • TimeUnit.SECONDS:秒
    • TimeUnit.MILLISECONDS:毫秒
    • TimeUnit.MICROSECONDS:微妙
    • TimeUnit.NANOSECONDS:纳秒
  5. workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。用来存储线程池等待执行的任务,均为线程安全,它包含以下 7 种类型。较常用的是 LinkedBlockingQueue 和 Synchronous,线程池的排队策略与 BlockingQueue 有关。

    • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
    • LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
    • SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
    • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
    • DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
    • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
    • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
  6. threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。

  7. handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略。

线程池流程

  1. 判断核心线程池是否已满,即线程池中当前线程数是否大于核心线程数,没满则创建一个新的工作线程来执行任务。已满则:
  2. 判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
  3. 判断整个线程池是否已满,即线程池中当前线程数是否大于最大线程数,没满则创建一个新的工作线程来执行任务,已满则执行饱和策略。

线程池为什么需要使用(阻塞)队列?

  1. 为什么要使用队列?

    1. 因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。
    2. 创建线程池的消耗较高。实际是:线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲。
  2. 为什么要使用阻塞队列而不使用非阻塞队列?

    阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。

    当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。使得在线程不至于一直占用cpu资源。

    线程执行完任务后通过循环再次从任务队列中取出任务进行执行,代码片段如下

    while (task != null || (task = getTask()) != null) {})。
    

    不用阻塞队列也是可以的,不过实现起来比较麻烦。

代码示例

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 自定义线程池ThreadPoolExecutor
 */
public class MyCustormerThreadPoolExecutor {

    public static void main(String[] args) {
        // 创建数组型缓冲等待队列
        BlockingQueue<Runnable> bq = new ArrayBlockingQueue<Runnable>(10);
        // ThreadPoolExecutor:创建自定义线程池,池中保存的线程数为3,允许最大的线程数为6
        System.out.println("ThreadPoolExecutor:创建自定义线程池,池中保存的线程数为3,允许最大的线程数为6");
        ThreadPoolExecutor tpe = new ThreadPoolExecutor(3, 6, 50, TimeUnit.MILLISECONDS, bq);

        // 创建3个任务
        Runnable t1 = new TempThread();
        Runnable t2 = new TempThread();
        Runnable t3 = new TempThread();
        Runnable t4 = new TempThread();
        Runnable t5 = new TempThread();
        Runnable t6 = new TempThread();

        // 3个任务在分别在3个线程上执行
        tpe.execute(t1);
        tpe.execute(t2);
        tpe.execute(t3);
        tpe.execute(t4);
        tpe.execute(t5);
        tpe.execute(t6);

        // 关闭自定义线程池
        tpe.shutdown();
    }

    public static class TempThread implements Runnable {

        @Override
        public void run() {
            // 打印正在执行的缓存线程信息
            System.out.println(Thread.currentThread().getName() + "正在被执行");
            try {
                // sleep一秒保证3个任务在分别在3个线程上执行
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

}

输出结果:

ThreadPoolExecutor:创建自定义线程池,池中保存的线程数为3,允许最大的线程数为6
pool-1-thread-1正在被执行
pool-1-thread-3正在被执行
pool-1-thread-2正在被执行
pool-1-thread-3正在被执行
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行

如何配置线程池

  1. CPU密集型任务

    用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

  2. IO密集型任务

    可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

  3. 混合型任务

    可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。 因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。 因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

java提供的线程池

Executors类提供了4种不同的线程池:newCachedThreadPool, newFixedThreadPool, newScheduledThreadPool, newSingleThreadExecutor

线程池 corePoolSize maximumPoolSize keepAliveTime workQueue
CachedThreadPool 0 Integer.MAX_VALUE 60s SynchronousQueue
FixedThreadPool n(固定大小) n(固定大小) 0 LinkedBlockingDeque
SingleThreadExecutor 1 1 0 LinkedBlockingDeque
ScheduledThreadPool corePoolSize Integer.MAX_VALUE 0 DelayQueue
  1. newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)

  2. newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)

  3. newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。

  4. newScheduledThreadPool:适用于执行延时或者周期性任务。

execute()和submit()方法

  1. execute(),执行一个任务,没有返回值。
  2. submit(),提交一个线程任务,有返回值。
submit(Callable<T> task)
能获取到它的返回值,通过future.get()获取(阻塞直到任务执行完)。一般使用FutureTask+Callable配合使用(IntentService中有体现)。

submit(Runnable task, T result)
能通过传入的载体result间接获得线程的返回值。

submit(Runnable task)
则是没有返回值的,就算获取它的返回值也是null

Future.get方法会使取结果的线程进入阻塞状态,知道线程执行完成之后,唤醒取结果的线程,然后返回结果。

参考

  1. 4种常用线程池介绍
  2. Java线程池详解
  3. Java并发编程:Callable、Future和FutureTask

results matching ""

    No results matching ""