线程池(ThreadPoolExecutor)处理异步任务

久未见 提交于 2019-12-02 19:31:10

转载于: http://blog.csdn.net/u010687392/article/details/49850803

1.前言

我们在开发时候或多或少都会用到线程,而通常创建线程有两种方式:

  1. 继承Thread类
  2. 实现Runnable接口

这两种方式虽然都可以创建线程,但是是有区别的:

  • 主要区别在于在多线程访问同一资源的情况下,用Runnable接口创建的线程可以处理同一资源,而用Thread类创建的线程则各自独立处理,各自拥有自己的资源,这样不利于资源共享。
  • 用Thread类创建是继承的方式,而用Runnable接口创建是实现接口的方式,更灵活多样性。

所以,在Java中大多数多线程程序都是通过实现Runnable来完成的,而对于Android来说也不例外。

具体实现代码如下:

new Thread(new Runnable() {
    @Override
    public void run() {
        //TODO.
    }
}).start();

上述代码创建了一个线程并执行,它在任务结束后GC会自动回收该线程,这种处理在线程并发不多的程序中确实不错,但是当程序有很多地方需要开启大量线程来处理任务,那么如果还是用上述的方式去创建线程处理的话,那么将导致系统的性能表现的非常糟糕,更别说在内存有限的移动设备上,主要的影响如下:

  • 线程的创建和销毁都需要时间,当有大量的线程创建和销毁时,那么这些时间的消耗则比较明显,将导致性能上的缺失
  • 大量的线程创建、执行和销毁是非常耗cpu和内存的,这样将直接影响系统的吞吐量,导致性能急剧下降,如果内存资源占用的比较多,还很可能造成OOM
  • 大量的线程的创建和销毁很容易导致GC频繁的执行,从而发生内存抖动现象,而发生了内存抖动,对于移动端来说,最大的影响就是造成界面卡顿

而针对上述所描述的问题,解决的办法归根到底就是:重用已有的线程,从而减少线程的创建。
如此就需要使用到线程池(ExecutorService),线程池的基本作用就是进行线程的复用

使用线程池管理线程的优点

  1. 线程的创建和销毁由线程池维护,一个线程在完成任务后并不会立即销毁,而是由后续的任务复用这个线程,从而减少线程的创建和销毁,节约系统的开销
  2. 线程池旨在线程的复用,这就可以节约我们用以往的方式创建线程和销毁所消耗的时间,减少线程频繁调度的开销,从而节约系统资源,提高系统吞吐量
  3. 在执行大量异步任务时提高了性能
  4. Java内置的一套ExecutorService线程池相关的api,可以更方便的控制线程的最大并发数、线程的定时任务、单线程的顺序执行等

2.ExecutorService简介

ExecutorService:它是一个接口,其实如果要从真正意义上来说,它可以叫做线程池的服务,因为它提供了众多接口api来控制线程池中的线程,而真正意义上的线程池就是:ThreadPoolExecutor,它实现了ExecutorService接口,并封装了一系列的api使得它具有线程池的特性,其中包括工作队列、核心线程数、最大线程数等。
ThreadPoolExecutor:我们要创建一个线程池只需要new ThreadPoolExecutor(…);就可以创建一个线程池,而如果这样创建线程池的话,我们需要配置一堆东西,非常麻烦:

public ThreadPoolExecutor(int corePoolSize,                             //核心线程池数量
                              int maximumPoolSize,                      //最大线程池大小
                              long keepAliveTime,                       //线程池中超过                                         corePoolSize数目的空闲线程最大存活时间;可以allowCoreThreadTimeOut(true)使得核心线程有效时间
                              TimeUnit unit,                            //keepAliveTime时间单位
                              BlockingQueue<Runnable> workQueue,        //阻塞任务队列
                              ThreadFactory threadFactory,              //新建线程工厂
                              RejectedExecutionHandler handler) {...}   //当提交任务数超过maxmumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理

官方一般不推荐此种使用方法,而是推荐使用Executors的工厂方法来创建线程池,Executors类是官方提供的一个工厂类,它里面封装好了众多功能不一样的线程池,从而使得我们创建线程池非常的简便,主要提供了如下五种功能不一样的线程池:

  • newFixedThreadPool(int nThreads) :该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也不会销毁已经创建好的线程,自始自终都是那几个固定的线程在工作,所以该线程池可以控制线程的最大并发数。
    eg:假如有一个新任务提交时,线程池中如果有空闲的线程则立即使用空闲线程来处理任务,如果没有,则会把这个新任务存在一个任务队列中,一旦有线程空闲了,则按FIFO方式处理任务队列中的任务。
    *FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
  • newCachedThreadPool() :该方法返回一个可以根据实际情况调整线程池中线程的数量的线程池。即该线程池中的线程数量不确定,是根据实际情况动态调整的。
    eg:线程池中的线程都有一个“保持活动时间”的参数,通过配置它,如果线程池中的空闲线程的空闲时间超过该“保存活动时间(keepAliveTime)”则立刻停止该线程,而该线程池默认的“保持活动时间”为60s。因此线程池中线程不会越集越多
    *在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
  • newSingleThreadExecutor() :该方法返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,当这个线程空闲了再按FIFO方式顺序执行任务队列中的任务。
    *单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
 ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
  • newScheduledThreadPool(int corePoolSize):该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。
 ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
  • newSingleThreadScheduledExecutor() : 该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。只不过和上面的区别是该线程池大小为1,而上面的可以指定线程池的大小。
ScheduledExecutorService singleThreadScheduledPool = Executors.newSingleThreadScheduledExecutor();

ThreadPoolExecutor的构造方法中,workQueue这个任务队列却要再次说明一下,它是一个BlockingQueue对象,而泛型则限定它是用来存放Runnable对象的,不同的线程池它的任务队列实现肯定是不一样的,所以,保证不同线程池有着不同的功能的核心就是这个workQueue的实现了,细心的会发现在刚刚的用来创建线程池的工厂方法中,针对不同的线程池传入的workQueue也不一样:

1、newFixedThreadPool()—>LinkedBlockingQueue        //无界的队列
2、newSingleThreadExecutor()—>LinkedBlockingQueue 
3、newCachedThreadPool()—>SynchronousQueue          //直接提交的队列 
4、newScheduledThreadPool()—>DelayedWorkQueue       //等待队列
5、newSingleThreadScheduledExecutor()—>DelayedWorkQueue

**几种线程池使用场合:

  • 对于需要保证所有提交的任务都要被执行的情况,使用FixedThreadPool
  • 如果限定只能使用一个线程进行任务处理,使用SingleThreadExecutor
  • 如果希望提交的任务尽快分配线程执行,使用CachedThreadPool
  • 如果业务上允许任务执行失败,或者任务执行过程可能出现执行时间过长进而影响其他业务的应用场景,可以通过使用限定线程数量的线程池以及限定长度的队列进行容错处理。

3.线程池ThreadPoolExecutor的使用

(1).Java内置提供的五种常用的线程池

使用线程池,其中涉及到一个极其重要的方法,即:

execute(Runnable command); //执行给定的任务,该任务处理可能在新的线程、已入池的线程或者正调用的线程,这由ThreadPoolExecutor的实现决定。

创建一个固定线程数量的线程池,示例为:

    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
    for (int i = 1; i <= 10; i++) {
        final int index = i;
        fixedThreadPool.execute(new Runnable() {
             @Override
             public void run() {
                 String threadName = Thread.currentThread().getName();
                 Log.v("zy", "线程:"+threadName+",正在执行第" + index + "个任务");
                 try {
                        Thread.sleep(2000);
                 } catch (InterruptedException e) {
                        e.printStackTrace();
                 }
             }
         });
     }

创建一个可以定时或者周期性执行任务的线程池,示例为:

        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
        //延迟2秒后执行该任务
        scheduledThreadPool.schedule(new Runnable() {
            @Override
            public void run() {

            }
        }, 2, TimeUnit.SECONDS);
        //延迟1秒后,每隔2秒执行一次该任务
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {

            }
        }, 1, 2, TimeUnit.SECONDS);

(2).自定义线程池ThreadPoolExecutor

实现一个功能是按任务的优先级来处理的线程池

  • 首先我们创建一个基于PriorityBlockingQueue实现的线程池,为了测试方便,我这里把核心线程数量设置为3,如下:
ExecutorService priorityThreadPool = new ThreadPoolExecutor(3,3,0L,TimeUnit.SECONDS,new PriorityBlockingQueue<Runnable>());
  • 然后创建一个实现Runnable接口的类,并向外提供一个抽象方法供我们实现自定义功能,并实现Comparable接口,实现这个接口主要就是进行优先级的比较,代码如下:
public abstract class PriorityRunnable implements Runnable, Comparable<PriorityRunnable> {
    private int priority;

    public PriorityRunnable(int priority) {
        if (priority < 0)
            throw new IllegalArgumentException();
        this.priority = priority;
    }

    @Override
    public int compareTo(PriorityRunnable another) {
        int my = this.getPriority();
        int other = another.getPriority();
        return my < other ? 1 : my > other ? -1 : 0;
    }

    @Override
    public void run() {
        doSth();
    }

    public abstract void doSth();

    public int getPriority() {
        return priority;
    }
}
  • 使用我们自己的PriorityRunnable提交任务,整体代码如下:
        ExecutorService priorityThreadPool = new ThreadPoolExecutor(3, 3, 0L, TimeUnit.SECONDS, new PriorityBlockingQueue<Runnable>());
        for (int i = 1; i <= 10; i++) {
            final int priority = i;
            priorityThreadPool.execute(new PriorityRunnable(priority) {
                @Override
                public void doSth() {
                    String threadName = Thread.currentThread().getName();
                    Log.v("zxy", "线程:" + threadName + ",正在执行优先级为:" + priority + "的任务");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

可以推断执行结果,由于核心线程数设置为3,刚开始时,系统有3个空闲线程,所以无须使用任务队列,而是直接运行前三个任务,而后面再提交任务时由于当前没有空闲线程所以加入任务队列中进行等待,此时,由于我们的任务队列实现是由PriorityBlockingQueue实现的,所以进行等待的任务会经过优先级判断,优先级高的放在队列前面先处理。

优先级线程池的优点:创建一个优先级线程池非常有用,它可以在线程池中线程数量不足或系统资源紧张时,优先处理我们想要先处理的任务,而优先级低的则放到后面再处理,这极大改善了系统默认线程池以FIFO方式处理任务的不灵活

(3).扩展线程池ThreadPoolExecutor

除了内置的功能外,ThreadPoolExecutor也向外提供了三个接口供我们自己扩展满足我们需求的线程池,这三个接口分别是:

beforeExecute() - 任务执行前执行的方法 
afterExecute() -任务执行结束后执行的方法 
terminated() -线程池关闭后执行的方法

这三个方法在ThreadPoolExecutor内部都没有实现.

前面两个方法我们可以在ThreadPoolExecutor内部的runWorker()方法中找到,而runWorker()是ThreadPoolExecutor的内部类Worker实现的方法,Worker它实现了Runnable接口,也正是线程池内处理任务的工作线程,而Worker.runWorker()方法则是处理我们所提交的任务的方法,它会同时被多个线程访问,所以我们看runWorker()方法的实现,由于涉及到多个线程的异步调用,必然是需要使用锁来处理,而这里使用的是Lock来实现的.

因此,我们要扩展线程池,只需要重写这三个方法,并实现我们自己的功能即可,这三个方法分别都会在任务执行前调用、任务执行完成后调用、线程池关闭后调用。

public class MyThreadPoolExecutor extends ThreadPoolExecutor {
    public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        String threadName = t.getName();
        Log.v("zxy", "线程:" + threadName + "准备执行任务!");
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        String threadName = Thread.currentThread().getName();
        Log.v("zxy", "线程:" + threadName + "任务执行结束!");
    }

    @Override
    protected void terminated() {
        super.terminated();
        Log.v("zxy", "线程池结束!");
    }
}

具有暂时功能的线程池:

public class PausableThreadPoolExecutor extends ThreadPoolExecutor {
    private boolean isPaused;
    private ReentrantLock pauseLock = new ReentrantLock();
    private Condition unpaused = pauseLock.newCondition();

    public PausableThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        pauseLock.lock();
        try {
            while (isPaused) unpaused.await();
        } catch (InterruptedException ie) {
            t.interrupt();
        } finally {
            pauseLock.unlock();
        }

    }

    public void pause() {
        pauseLock.lock();
        try {
            isPaused = true;
        } finally {
            pauseLock.unlock();
        }
    }

    public void resume() {
        pauseLock.lock();
        try {
            isPaused = false;
            unpaused.signalAll();
        } finally {
            pauseLock.unlock();
        }
    }
}

然后结合上面的优先级线程池的实现,创建具有暂停功能的优先级线程池:

        PausableThreadPoolExecutor pausableThreadPoolExecutor = new PausableThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new PriorityBlockingQueue<Runnable>());
        for (int i = 1; i <= 100; i++) {
            final int priority = i;
            pausableThreadPoolExecutor.execute(new PriorityRunnable(priority) {
                @Override
                public void doSth() {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            textView.setText(priority + "");
                        }
                    });
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

(4).优化线程池ThreadPoolExecutor

虽说线程池极大改善了系统的性能,不过创建线程池也是需要资源的,所以线程池内线程数量的大小也会影响系统的性能,大了反而浪费资源,小了反而影响系统的吞吐量,所以我们创建线程池需要把握一个度才能合理的发挥它的优点,通常来说我们要考虑的因素有CPU的数量、内存的大小、并发请求的数量等因素,按需调整。

通常核心线程数可以设为CPU数量+1,而最大线程数可以设为CPU的数量*2+1。

获取CPU数量的方法为:

Runtime.getRuntime().availableProcessors();

shutdown()和shutdownNow()的区别:
1、shutdown()方法在终止前允许执行以前提交的任务。
2、shutdownNow()方法则是阻止正在任务队列中等待任务的启动并试图停止当前正在执行的任务。

(5).优化线程池ThreadPoolExecutor

AsyncTask内部实现其实就是Thread+Handler。其中Handler是为了处理线程之间的通信,而这个Thread是线程池,AsyncTask内部实现了两个线程池,分别是:串行线程池和固定线程数量的线程池。而这个固定线程数量则是通过CPU的数量决定的。

在默认情况下,我们大都通过AsyncTask::execute()来执行任务的,而execute()内部则是调用executeOnExecutor(sDefaultExecutor, params)方法执行的,第一个参数就是指定处理该任务的线程池,而默认情况下AsyncTask是传入串行线程池(在这里不讲版本的变化),也就是任务只能单个的按顺序执行。

而我们要是想让AsyncTask并行的处理任务,大家都知道调AsyncTask::executeOnExecutor(sDefaultExecutor, params)方法传入这个参数即可:AsyncTask.THREAD_POOL_EXECUTOR。
而这个参数的意义在于为任务指定了一个固定线程数量的线程池去处理,从而达到了并行处理的功能,我们可以在源码中看到AsyncTask.THREAD_POOL_EXECUTOR这个参数就是一个固定线程数量的线程池:

  public static final Executor THREAD_POOL_EXECUTOR
            = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
                    TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!