Android中线程池的使用与分析

为什么要使用线程池?

通常创建线程的方式有两种:
1.继承Thread类
2.实现Runnable接口

两种方式都可以创建一个线程,但它们之间还是有一点区别的,主要的区别在于多线程访问同一资源的情况下,用Runnable接口创建的线程可以处理同一资源,而使用Thread类创建的线程则是各自独立处理,各拥有自己的资源。所以,在java中大多数线程是使用Runnable来创建的,对于Android也是不例外。一般我们都会这样写:

new Thread(new Runnable({
public void run(){
//处理具体逻辑
}
}).start();

这段代码创建了一个线程并执行,它在任务结束后GC会自动回收该线程,它在线程并发不多的时候确实不错,但如果有程序需要大量线程来处理任务,若用上述方法开启大量线程会导致系统性能表现糟糕,主要影响如下:

1.线程的创建和销毁都需要时间,当有大量线程创建和销毁,这些时间消耗则比较明显。将导致性能的缺失。

2.大量线程的创建和销毁非常耗CPU和内存,这样将直接影响系统的吞吐量,导致性能急剧下降,如果内存资源占用比较多,还有可能造成OOM。

3.大量线程的创建和销毁导致GC的频繁执行,从而发生内存抖动现象,内存抖动对移动端来说,最大的影响就是造成界面卡顿。

针对上述问题,解决办法归根到底是:重用已有线程,从而减少线程的创建。所以这就涉及到线程池(ExecutorService)的概念,线程池的基本作用就是进行线程复用。、

线程池的介绍和使用

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

1.线程的创建和销毁由线程池维护,一个线程在完成任务后不会立即销毁,而是由后续的任务复用,从而减少线程的创建和销毁,节约系统开销。

2.线程池旨在线程的复用,减少线程频繁调度的开销,节约系统资源,提高系统吞吐量。

3.在执行大量异步任务时提高了性能。

4.java内置的一套ExecutorService线程池相关的api,可以方便的控制线程的最大并发数、线程的定时完成任务、单线程的顺序执行等。

ExcutorService和ThreadPoolExecutor

ExecutorService ,它是一个接口,其实要从真正意义上说,它可以还叫做线程池的服务,因为它是提供了众多的接口api来控制线程池中的线程,真正意义上的线程池就是:ThreadPoolExecutor,它实现了ExecutorService接口,并封装了一系列的api使得它具有线程池的特性,其中包括线程队列、核心线程数、最大线程数等。

创建线程池只需要new ThreadPoolExecutor(…) ,但是采用直接new 一个线程池我们需要配置一大堆的东西,我们可以看看它的构造方法:

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandle handler){...}

官方也不推荐使用直接new的方法来创建一个线程池,而推荐使用Executor的工厂方法来创建线程池,Executor类是官方提供的一个工厂类,它里面封装了功能不一样的线程池,从而使我们创建线程池非常的简便,主要提供了以下5种功能不一样的线程池:

newFixedThreadPool()
作用:该方法返回一个固定数量的线程池,改线程池中的数量始终不变,即不回创建新的线程,也不会销毁线程,自始至终都是固定数量的线程在工作,所以该线程池可以控制线程的最大并发数。
列子:加入一个新任务提交时,如果线程池中有空闲的线程则立即交于该线程来处理任务,若没有空闲线程则把改任务存在一个任务队列中,一旦有线程空闲了,则按FIFO方式处理任务队列中的任务。

newCachedThread()
作用:该方法返回一个可以根据实际情况调整线程池中线程数量的线程池。即线程池中的数量不确定,根据实际动态调整。
列子:加入线程池中所有的线程都在工作,而此时又有新的任务提交,那么将会创建新的线程去处理,而此时假如之前有一些线程完成了任务,现在又有新的任务提交,那么将不会创建新的线程去处理,而是复用空闲的线程处理新的任务。那这样会导致线程越集越多?其实并不会,因为线程池中的线程有一个”保持活动时间”的参数,通过配置它,如果线程池的空闲线程超过该”保持活动时间” 则立刻停止该线程,一般线程池默认的”保持活动时间”为60s。

newSingleThreadExecutor()
作用:该方法返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,若任务队列中还有任务则按FIFO方式顺序执行任务队列中的任务。

newScheduledThreadPool()
作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。只不过和上面的区别是该线程池大小为1,而上面的可以指定线程的大小。

我们可以通过Executors的工厂方法来获取上述五种线程池:

  • ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
  • ExecutorService singleThreadPool = Executors.newSingleThreadPool();
  • ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
  • ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
  • ScheduledExecutorService singleThreadScheduledPool =
    Executors.newSingleThreadScheduledExecutor();
    我们可以看到通过Exectors的工厂方法创建线程池及其简便,其实它的内部还是通过new的方式创建线程池,我们看一下这些工厂方法的内部实现:
public  static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}

public static ExecutorService newSingleThreadExecutor(){
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}

public static ExecutorService newCachedThreadPool(){
return new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUint.SECONDS,new SynchronosQueue<Runnable>());
}

我们可以清楚的看到这些方法的内部实现都是通过创建一个ThreadPoolExecutor对象,正所谓万变不离其宗,我们想要了解线程池必须要了解ThreadPoolExecutor这个线程池类。其中由于定时任务相关的线程池比较特殊(newScheduledThreadPool()\newSingleThreadScheduledExecutor()),他们创建的线程池内部是由ScheduleThreadPoolExecutor这个类实现的,而ScheduleThreadPoolExecutor是继承于ThreadPoolExecutor扩展而成,所以本质是一样的,只不过多封装了一些定时任务相关的api,所以我们主要了解ThreadPoolExecutor。
构造方法:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {//...}

参数介绍:

  • corePoolSize:线程池中的核心线程数量
  • maximumPoolSize:线程池中最大线程数量
  • keepAliveTime:“保持活动时间”,当线程池中的线程数量超过corePoolSize时,它表示多余的空闲线程的存活时间,即:多余的空闲线程在超过keepAliveTime时间内没有任务活动的话被销毁,而这个主要应用在缓存线程池中。
  • unit:它是一个枚举类型,表示keepAliveTime的单位,常用的如:TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)
  • workQueue:任务队列,主要用来存储已经提交但未被执行的任务,不同的线程池采用的排队策略是不一样的
  • threadFactory:线程工厂,用来创建线程池中的线程,通常用默认的即可
  • handler:通常叫做拒绝策略,在线程已经关闭的情况下,任务太多导致最大线程数和任务队列已经饱和无法再接受新的任务,在上面两种情况下,只要满足其中一种时,在使用execute()来提交新的任务时将会拒绝,默认的一种拒绝策略是抛RejectedExecutionException异常。

上面的参数理解起来比较简单,下面我们再说一下workQueue,它是一个BlockingQueue<Runnable>
对象,泛型则限定它存放Runnable对象,不同的线程池它的任务队列实现是不一样的,所以,保证不同线程池有着不同的功能的核心就是workQueue的实现,针对不同的线程池传入的workQueue也是不一样的,下面我们总结一下五种线程池分别用的什么BlockingQueue:

  • newFixedThreadPool()——LinkedBlockingQueue
  • newSingleThreadExecutor()——LinkedBlockingQueue
  • newCachedThreadPool()——SynchronousQueue
  • newScheduledThreadPool()——DelayedWorkQueue
  • newSingleThreadScheduledExecutor()——DelayedWorkQueue

这些队列标识

  • LinkedBlockingQueue:无界队列
  • SynchronousQueue:直接提交队列
  • DelayedWorkQueue:等待队列

线程池ThreadPoolExecutor的研究和具体实现

1.newFixedThreadPool示例

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

上述代码 ,我们创建了一个线程数为3的固定线程数量的线程池,同理该线程池支持的线程最大并发数也是3。上述程序模拟了10个任务,执行情况首先执行前三个任务,后面7个则依次进入任务队列进行等待,执行完前三个任务后,再通过FIFO的方式从任务队列中取任务执行,知道最后任务都执行完毕。

2.newSingleThreadExecutor示例

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

代码还是差不多,只不过改了线程池的实现方式,效果我想大家都知道,即依次一个一个的处理任务,而且都是复用一个线程。

3.newCachedThreadPool示例

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

为了体现该线程池可以自动根据实现情况进行线程的重用,而不是一味的创建新的线程去处理任务,我设置了每隔1s去提交一个新任务,这个新任务执行的时间也是动态变化的

4.newScheduledThreadPool示例

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);

5.newSingleThreadScheduledExecutor示例

 ScheduledExecutorService singleThreadScheduledPool = Executors.newSingleThreadScheduledExecutor();
    //延迟1秒后,每隔2秒执行一次该任务
    singleThreadScheduledPool.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            Log.v("zxy", "线程:" + threadName + ",正在执行");
        }
    },1,2,TimeUnit.SECONDS);

自定义线程池的研究和具体实现

java内置只为我们提供了五种常用的线程池,一般来足够用了,不过有时候我们也可以根据需求来自定义我们自己的线程池,而是要自定义不同功能的线程池,上面我们也说了线程池功能的不同归根到底还是内部BlockingQueue的实现做手脚,而上面也说了BlockingQueue的实现类有多个,那么我们这次选用PriorityBlockingQueue来实现一个功能是按任务的优先级来处理的线程池。
1.首先我们创建一个基于PriorityBlockingQueue实现的线程池,为了测试方便,我们把核心线程数设置为3,如下:

ExecutorService priorityThreadPool = new ThreadPoolExecutor(3,3,0L,TimeUnit.SECONDS,new PriorityBlockingQueue<Runnable>());

2.然后创建一个实现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;
    }
    public int compareTo(PriorityRunnable  another){
        int my = this.getPriority();
        int other = another.getPriority();
        return my<other?1:my>other?-1:0;
    }
    public void run(){
        doSth();
    }
    public abstract void doSth();

    public int getPriority(){
        return priority;
    }
}

3.使用自己的PriorityRunnable 提交任务,代码如下:

ExecutorService prioirtyThreadPool = new ThreadPoolExecutor(3,3,0L,TimeUnit.SECONDS,new PriorityBlockingQueue<Runnable>());
for(int i=1;i<=10;i++){
    final int priority = i;
    prioirtyThreadPool.execute(new PriorityRunnable  (priority ){
        public void doSth(){
            String threadName = Thread.currentThread().getName();
            Log.v("hello","线程"+threadName +",正在执行优先级为:"+priority+"的任务");
            try{
                Thread.sleep(2000);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
    });
}

扩展线程池的研究和具体实现

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

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

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

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

try{
    beforeExecute(wt,task);
    Throwable thrown = null;
    try{
        task.run();
    }catch(RuntimeException x){
        thrown = x;
        throw x;
    }catch(Error x){
        thrown = x;
        throw x;
    }catch(Throwable x){
        thrown = x;
        throw new Error(x);
    }finally{
        afterExecute(task,thrown);
    }
}

可以看到在task.run()之前和之后分别调用了beforeExecute()和afterExecute()方法,并传入我们的任务Runnable对象

而terminated()则是在关闭线程池方法中调用,关闭线程池有两个方法,下面是其中之一

public void shutdown(){
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try{
        checkShutdownAccess();
        advanceRunState(SHUTDOWN);
        interruptIdleWorkers();
        onShutdown();
    }finally{
        mainLock.unLock();
    }
    tryTerminate();
}

所以我们要扩展线程池,只需要重写这三个方法,并实现我们的功能即可,这三个方法分别都在任务执行前,任务执行后,线程池关闭嗲用。

线程池的优化

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

线程池的停止

关于线程池的停止,ExecutorService为我们提供了两个方法:shutdown和shutdownNow,这两个方法各有不同,如下:
shutdown()此方法在终止前允许执行以前提交的任务
shutdownNow()方法则是阻止正在任务队列中等待的任务的启动并试图停止当前正在执行的任务。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值