Android中的线程池

194 阅读7分钟

线程池的作用

  • 重用线程池中的线程,避免因为线程的创建和销毁所带来的性能开销
  • 能有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致的阻塞现象。
  • 能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行等功能。

Android中的线程池概念来源于Java中的Executor,Executor是一个接口,真正的线程池的实现为ThreadPoolExecutor。

ThreadPoolExecutor 的类关系

Executor是一个接口,它是Executor框架的基础,它将任务的提交与任务的执行分离开来。 ExecutorService接口继承了Executor,在其上做了一些shutdown()、submit()的扩展,可以说是真正的线程池接口;

AbstractExecutorService抽象类实现了ExecutorService接口中的大部分方法;

ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。

ScheduledExecutorService接口继承了ExecutorService接口,提供了带"周期执行"功能ExecutorService;

ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。ScheduledThreadPoolExecutor比Timer更灵活,功能更强大。

线程池的构造参数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize 线程池的核心线程数,默认情况下,核心线程会在线程池中一直存活,即使他们处于闲置状态。如果将ThreadPoolExecutor的allowCoreThreadTimeout属性设置为true,那么闲置的核心线程在等待新任务到来会有超时策略,这个时间间隔由keepAliveTime所指定,当等待时间超时keepAliveTime所指定的时长后,核心线程会被终止。

  • maximumPoolSize 线程池所能容纳的最大线程数,当活动线程达到这个数值后,后续的新任务将会根据RejectedExecutionHandler(拒绝策略实现不同的操作)。

  • keepAliveTime 非核心线程闲置时的超时时长,超过这个时间,非核心线程就会被收回。当ThreadPoolExecutor的allowCoreThreadTimeout属性设置为true时,keepAliveTime同样会作用于核心线程。

*unit 用于指定keepAliveTime参数的时间单位,这是一个枚举,常用的有TimeUnit.MILLISECONDS;TimeUnit.SECONDS;TimeUnit.MINUTES等

  • workQueue 线程池的任务队列,通过线程池的execute方法提交的Runnable对象会存储在这个参数中。

  • ThreadFactory 线程工厂,为线程池提供创建新线程的功能。ThreadFactory是一个接口,它只有一个方法:Thread newThread(Runnable r);

  • handler(拒绝策略) 当线程池无法执行新的任务时,这可能是由于任务队列已满或者是无法成功执行任务,这个时候ThreadPoolExecutor回调用RejectedExecutionHandler的rejectedExecution(Runnable r, ThreadPoolExecutor executor)方法来通知调用者,默认情况下rejectedExecution会直接抛出一个RejectedExecutionException的运行时异常。

  • AbortPolicy:直接抛出异常,默认策略;

  • CallerRunsPolicy:用调用者所在的线程来执行任务;

  • DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;

  • DiscardPolicy:直接丢弃任务;

线程池的工作机制

  • 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。

  • 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。

  • 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务。

  • 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

image.png

阻塞队列

队列

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。

在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。

阻塞队列

1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。

2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。

在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序整体处理数据的速度。

在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。

为了解决这种生产消费能力不均衡的问题,便有了生产者和消费者模式。生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

image.png

常见阻塞队列

·ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。

·LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。

·PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。

·DelayQueue:一个使用优先级队列实现的无界阻塞队列。

·SynchronousQueue:一个不存储元素的阻塞队列。

·LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

·LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

以上的阻塞队列都实现了BlockingQueue接口,也都是线程安全的。

线程池的关闭

可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程

只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

okhttp中的线程池

public synchronized ExecutorService executorService() {
    if (executorService == null) {
        executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher",
                false));
    }
    return executorService;
}

这种线程池的实现方式核心线程数是0,使用了SynchronousQueue阻塞队列,实现了高并发处理。 为什么这种能实现高并发处理呢?

 @Test
    public void testQueue() {
        // 没有容量的容器
        SynchronousQueue<Runnable> queue = new SynchronousQueue<>();
//        ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1);

        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(0,
                Integer.MAX_VALUE,
                60, TimeUnit.SECONDS, queue);

        poolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1:" + Thread.currentThread());
                while (true) {
                }
            }
        });

        poolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2:" + Thread.currentThread());
            }
        });
    

        while (true) {

        }
    }

假如我们使用的是ArrayBlockingQueue,这时队列的容量只是1,这是线程1一直在处理任务,没有释放。根据线程池的工作机制,此时核心线程数是0,没有空闲线程,线程2只能入队列。这时线程2就一直得不到执行。 运行结果如下:

image.png

如果我们使用的是SynchronousQueue阻塞队列。我们知道SynchronousQueue是不存储任务的,这时候任务数量大于核心线程数且小与非核心线程数时就会创建线程来执行任务。

    @Test
    public void testQueue() {
        // 没有容量的容器
        SynchronousQueue<Runnable> queue = new SynchronousQueue<>();
//        LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
//        ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);

        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(0,
                Integer.MAX_VALUE,
                60, TimeUnit.SECONDS, queue);

        poolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1:" + Thread.currentThread());
                while (true) {
                }
            }
        });

        poolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2:" + Thread.currentThread());
            }
        });
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        poolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务3:" + Thread.currentThread());
            }
        });


        //防止程序执行完
        while (true) {

        }
    }

image.png

这时候打印结果就是创建了其他的线程来执行任务2根3. 为啥任务2跟任务3线程名字相同?因为执行任务3之前Thread.sleep(100);此时任务2执行完毕,现在空闲,那就用任务2的空闲线程执行。

AsyncTask中的线程池

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final int KEEP_ALIVE = 1;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
        }
    };

    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>(128);

    /**
     * An {@link Executor} that can be used to execute tasks in parallel.
     */
    public static final Executor THREAD_POOL_EXECUTOR
            = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
                    TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

AsyncTask线程池配置后的规格如下:

  • 核心线程数等于CPU核心数+1
  • 线程池的最大线程数为CPU的核心数的2倍 + 1
  • 核心线程无超时机制,非核心线程在闲置时的超时时间为1秒
  • 任务队列的容量为128

⚠️:当任务书超过128的时候 此时会报错。