Android的线程和线程池

说到线程,我想大家都不陌生,因为在开发时候或多或少都会用到线程,而通常创建线程有两种方式:

1.继承Thread类;

2.实现Runnable接口;

虽说这两种方式都可以创建出一个线程,不过它们之间还是有一点区别的,主要区别在于在多线程访问同一资源的情况下,用Runnable接口创建的线程可以处理同一资源,而用Thread类创建的线程则各自独立处理,各自拥有自己的资源。详情可见android创建线程的两种方式及比较

所以,在Java中大多数多线程程序都是通过实现Runnable来完成的,而对于Android来说也不例外,当涉及到需要开启线程去完成某件事时,我们都会这样写:

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

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


使用new Thread()创建线程存在的问题:

1.频繁的创建线程,线程执行完之后又被回收,又会导致频繁的GC;

2.子线程多了之后缺乏统一管理,各线程之间互相竞争,降低程序的运行效率,手机页面卡顿,甚至会导致程序崩溃;

3.无法实现线程停止执行;

而针对上述所描述的问题,解决的办法归根到底就是:重用已有的线程,从而减少线程的创建。


使用线程池的好处

1.重用线程池中的线程,避免因为线程的创建和销毁所带来的性能开销;

2.能有效控制线程池的最大并发数,避免大量的线程之间因为相互抢占系统资源而导致的阻塞现象;

3.能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行等功能;


ExecutorService

通过上述分析,我们知道了通过new Thread().start()方式创建线程去处理任务的弊端,而为了解决这些问题,Java为我们提供了ExecutorService线程池来优化和管理线程的使用。

ExecutorService简介

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


线程池:ThreadPoolExecutor

ThreadPoolExecutor有四个重载的构造方法,我们这里来说说参数最多的那一个重载的构造方法,这样大家就知道其他方法参数的含义了,如下:

public ThreadPoolExecutor(int corePoolSize,  
                              int maximumPoolSize,  
                              long keepAliveTime,  
                              TimeUnit unit,  
                              BlockingQueue<Runnable> workQueue,  
                              ThreadFactory threadFactory,  
                              RejectedExecutionHandler handler) 
这里是7个参数(我们在开发中用的更多的是5个参数的构造方法),OK,那我们来看看这里七个参数的含义:

corePoolSize  线程池中核心线程的数量;

maximumPoolSize  线程池中最大线程数量;

keepAliveTime 非核心线程的超时时长,当系统中非核心线程闲置时间超过keepAliveTime之后,则会被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长;

unit 第三个参数的单位,有纳秒、微秒、毫秒、秒、分、时、天等;

workQueue 线程池中的任务队列,该队列主要用来存储已经被提交但是尚未执行的任务。存储在这里的任务是由ThreadPoolExecutor的execute方法提交来的;

threadFactory  为线程池提供创建新线程的功能,这个我们一般使用默认即可;

handler 拒绝策略,当线程无法执行新任务时(一般是由于线程池中的线程数量已经达到最大数或者线程池关闭导致的),默认情况下,当线程池无法处理新线程时,会抛出一个RejectedExecutionException;

针对于workQueue参数我多说几点:workQueue是一个BlockingQueue类型,那么这个BlockingQueue又是什么呢?它是一个特殊的队列,当我们从BlockingQueue中取数据时,如果BlockingQueue是空的,则取数据的操作会进入到阻塞状态,当BlockingQueue中有了新数据时,这个取数据的操作又会被重新唤醒。同理,如果BlockingQueue中的数据已经满了,往BlockingQueue中存数据的操作又会进入阻塞状态,直到BlockingQueue中又有新的空间,存数据的操作又会被冲洗唤醒。BlockingQueue有多种不同的实现类,下面我举几个例子来说一下:

1.ArrayBlockingQueue:这个表示一个规定了大小的BlockingQueue,ArrayBlockingQueue的构造函数接受一个int类型的数据,该数据表示BlockingQueue的大小,存储在ArrayBlockingQueue中的元素按照FIFO(先进先出)的方式来进行存取。

2.LinkedBlockingQueue:这个表示一个大小不确定的BlockingQueue,在LinkedBlockingQueue的构造方法中可以传一个int类型的数据,这样创建出来的LinkedBlockingQueue是有大小的,也可以不传,不传的话,LinkedBlockingQueue的大小就为Integer.MAX_VALUE,源码如下:

/** 
 * Creates a {@code LinkedBlockingQueue} with a capacity of 
 * {@link Integer#MAX_VALUE}. 
 */  
public LinkedBlockingQueue() {  
    this(Integer.MAX_VALUE);  
}  
  
/** 
 * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity. 
 * 
 * @param capacity the capacity of this queue 
 * @throws IllegalArgumentException if {@code capacity} is not greater 
 *         than zero 
 */  
public LinkedBlockingQueue(int capacity) {  
    if (capacity <= 0) throw new IllegalArgumentException();  
    this.capacity = capacity;  
    last = head = new Node<E>(null);  
}
3.PriorityBlockingQueue: 这个队列和LinkedBlockingQueue类似,不同的是PriorityBlockingQueue中的元素不是按照FIFO来排序的,而是按照元素的Comparator来决定存取顺序的(这个功能也反映了存入PriorityBlockingQueue中的数据必须实现了Comparator接口)。

4.SynchronousQueue:这个是同步Queue,属于线程安全的BlockingQueue的一种,在SynchronousQueue中,生产者线程的插入操作必须要等待消费者线程的移除操作,Synchronous内部没有数据缓存空间,因此我们无法对SynchronousQueue进行读取或者遍历其中的数据,元素只有在你试图取走的时候才有可能存在。我们可以理解为生产者和消费者互相等待,等到对方之后然后再一起离开。

OK,这是ThreadPoolExecutor的构造方法参数的解释,我们的线程提交到线程池之后又是按照什么样的规则去运行呢?OK,它们遵循如下规则:
1.execute一个线程之后,如果线程池中的线程数未达到核心线程数,则会立马启用一个核心线程去执行

2.execute一个线程之后,如果线程池中的线程数已经达到核心线程数,且workQueue未满,则将新线程放入workQueue中等待执行

3.execute一个线程之后,如果线程池中的线程数已经达到核心线程数但未超过非核心线程数,且workQueue已满,则开启一个非核心线程来执行任务

4.execute一个线程之后,如果线程池中的线程数已经超过非核心线程数,则拒绝执行该任务

OK,基于以上讲解,我们来看一个Demo:

@Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        poolExecutor = new ThreadPoolExecutor(3, 5,  
                1, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(128));  
}  
  
    public void btnClick(View view) {  
        for (int i = 0; i < 30; i++) {  
            final int finalI = i;  
            Runnable runnable = new Runnable() {  
                @Override  
                public void run() {  
                    SystemClock.sleep(2000);  
                    Log.d("google_lenve_fb", "run: " + finalI);  
                }  
            };  
            poolExecutor.execute(runnable);  
        }  
    }  

执行结果如下:

OK,由于核心线程数为3,workQueue的大小为128,所以我们的线程的执行应该是先启动三个核心线程来执行任务,剩余的27个任务全部存在workQueue中,等待核心线程空余出来之后执行。OK,那我把构造ThreadPoolExecutor的参数修改一下,来验证一下我们上面的结论正确与否:

poolExecutor = new ThreadPoolExecutor(3, 30,  
                1, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(6));  

OK,首先打印出来0,1,2说明往核心线程添加了三个任务,然后将3,4,5,6,7,8六个任务添加到了任务队列中,接下来要添加的任务因为核心线程已满,队列已满所以就直接开一个非核心线程来执行,因此后添加的任务反而会先执行(3,4,5,6,7,8都在队列中等着),所以我们看到的打印结果是先是0~2,然后9~29,然后3~8,当然,我们在实际开发中不会这样来配置最大线程数和线程队列。那如果我们需要自己来配置这些参数,该如何配置呢?参考一下AsyncTask,AsyncTask部分源码如下:

public abstract class AsyncTask<Params, Progress, Result> {  
    private static final String LOG_TAG = "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);  
        ....  
        ....  
}  

我们看到,核心线程数为手机CPU数量+1(cpu数量获取方式Runtime.getRuntime().availableProcessors()),最大线程数为手机CPU数量×2+1,线程队列的大小为128,OK,那么小伙伴们在以后使用线程池的过程中可以参考这个再结合自己的实际情况来配置参数。


线程池的分类

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

1.FixedThreadPool

FixedThreadPool是一个核心线程数量固定的线程池,创建方式如下:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); 

源码如下:

public static ExecutorService newFixedThreadPool(int nThreads) {  
        return new ThreadPoolExecutor(nThreads, nThreads,  
                                      0L, TimeUnit.MILLISECONDS,  
                                      new LinkedBlockingQueue<Runnable>());  
    }
我们看到核心线程数和最大线程数一样,说明在FixedThreadPool中没有非核心线程,所有的线程都是核心线程,且线程的超时时间为0,说明核心线程即使在没有任务可执行的时候也不会被销毁(这样可让FixedThreadPool更快速的响应请求),最后的线程队列是一个LinkedBlockingQueue,但是LinkedBlockingQueue却没有参数,这说明线程队列的大小为Integer.MAX_VALUE(2的31次方减1),OK,看完参数,我们大概也就知道了FixedThreadPool的工作特点了,当所有的核心线程都在执行任务的时候,新的任务只能进入线程队列中进行等待,直到有线程被空闲出来。OK,我们来看一个Demo:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);  
        for (int i = 0; i < 30; i++) {  
            final int finalI = i;  
            Runnable runnable = new Runnable(){  
                @Override  
                public void run() {  
                    SystemClock.sleep(3000);  
                    Log.d("google_lenve_fb", "run: "+ finalI);  
                }  
            };  
            fixedThreadPool.execute(runnable);  
        }  

这执行结果也和我们想的一致,先往核心线程中添加三个任务,剩余任务进入到workQueue中等待,当有空闲的核心线程时就执行任务队列中的任务

2.SingleThreadExecutor

singleThreadExecutor和FixedThreadPool很像,不同的就是SingleThreadExecutor的核心线程数只有1,如下:
public static ExecutorService newSingleThreadExecutor() {  
    return new FinalizableDelegatedExecutorService  
        (new ThreadPoolExecutor(1, 1,  
                                0L, TimeUnit.MILLISECONDS,  
                                new LinkedBlockingQueue<Runnable>()));  
} 
使用SingleThreadExecutor的一个最大好处就是可以避免我们去处理线程同步问题,其实如果我们把FixedThreadPool的参数传个1,效果不就和SingleThreadExecutor一致了,来看个Demo:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();  
        for (int i = 0; i < 30; i++) {  
            final int finalI = i;  
            Runnable runnable = new Runnable() {  
                @Override  
                public void run() {  
                    Log.d("google_lenve_fb", "run: " + Thread.currentThread().getName() + "-----" + finalI);  
                    SystemClock.sleep(1000);  
                }  
            };  
            singleThreadExecutor.execute(runnable);  
        } 

3.CachedThreadPool

CachedTreadPool一个最大的优势是它可以根据程序的运行情况自动来调整线程池中的线程数量,CachedThreadPool源码如下:
public static ExecutorService newCachedThreadPool() {  
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,  
                                  60L, TimeUnit.SECONDS,  
                                  new SynchronousQueue<Runnable>());  
}
我们看到,CachedThreadPool中是没有核心线程的,但是它的最大线程数却为Integer.MAX_VALUE,另外,它是有线程超时机制的,超时时间为60秒,这里它使用了SynchronousQueue作为线程队列,SynchronousQueue的特点上文已经说过了,这里不再赘述。那么我们提交到CachedThreadPool消息队列中的任务在执行的过程中有什么特点呢?由于最大线程数为无限大,所以每当我们添加一个新任务进来的时候,如果线程池中有空闲的线程,则由该空闲的线程执行新任务,如果没有空闲线程,则创建新线程来执行任务。根据CachedThreadPool的特点,我们可以在有大量任务请求的时候使用CachedThreadPool,因为当CachedThreadPool中没有新任务的时候,它里边所有的线程都会因为超时而被终止。OK,我们来看一个Demo:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();  
        for (int i = 0; i < 30; i++) {  
            final int finalI = i;  
            Runnable runnable = new Runnable(){  
                @Override  
                public void run() {  
                    Log.d("google_lenve_fb", "run: " + Thread.currentThread().getName() + "----" + finalI);  
                }  
            };  
            cachedThreadPool.execute(runnable);  
            SystemClock.sleep(2000);  
        } 

每次添加完任务之后我都停两秒在添加新任务,由于这里的任务执行不费时,我们可以猜测这里所有的任务都使用同一个线程来执行(因为每次添加新任务的时候都有空闲的线程),运行结果如下:


和我们的想法基本一致。OK,那如果我把代码稍微改一下:

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();  
        for (int i = 0; i < 30; i++) {  
            final int finalI = i;  
            Runnable runnable = new Runnable(){  
                @Override  
                public void run() {  
                    SystemClock.sleep(2000);  
                    Log.d("google_lenve_fb", "run: " + Thread.currentThread().getName() + "----" + finalI);  
                }  
            };  
            cachedThreadPool.execute(runnable);  
            SystemClock.sleep(1000);  
        } 

每个任务在执行的过程中都先休眠两秒,但是我向线程池添加任务则是每隔1s添加一个任务,这样的话,添加第一个任务时先开新线程,添加第二个任务时,由于第一个新线程尚未执行完,所以又开一个新线程,添加第三个任务时,第一个线程已经空闲下来了,直接第一个线程来执行第三个任务,依此类推。我们来看看运行结果:


4.ScheduledThreadPool

ScheduledThreadPool是一个具有定时定期执行任务功能的线程池,源码如下:

public ScheduledThreadPoolExecutor(int corePoolSize) {  
    super(corePoolSize, Integer.MAX_VALUE,  
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,  
          new DelayedWorkQueue());  
} 

我们可以看到,它的核心线程数量是固定的(我们在构造的时候传入的),但是非核心线程是无穷大,当非核心线程闲置时,则会被立即回收。

使用ScheduledThreadPool时,我们可以通过如下几个方法来添加任务:

1.延迟启动任务:

public ScheduledFuture<?> schedule(Runnable command,  
                                   long delay, TimeUnit unit); 

示例代码:

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);  
            Runnable runnable = new Runnable(){  
                @Override  
                public void run() {  
                    Log.d("google_lenve_fb", "run: ----");  
                }  
            };  
        scheduledExecutorService.schedule(runnable, 1, TimeUnit.SECONDS);

2.延迟定时执行任务:

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,  
                                              long initialDelay,  
                                              long period,  
                                              TimeUnit unit); 

延迟initialDelay秒后每个period秒执行一次任务。示例代码:

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);  
            Runnable runnable = new Runnable(){  
                @Override  
                public void run() {  
                    Log.d("google_lenve_fb", "run: ----");  
                }  
            };  
        scheduledExecutorService.scheduleAtFixedRate(runnable, 1, 1, TimeUnit.SECONDS); 
延迟1秒之后每隔1秒执行一次新任务。

3.延迟执行任务

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,  
                                                 long initialDelay,  
                                                 long delay,  
                                                 TimeUnit unit); 
第一次延迟initialDelay秒,以后每次延迟delay秒执行一个任务。示例代码:
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);  
            Runnable runnable = new Runnable(){  
                @Override  
                public void run() {  
                    Log.d("google_lenve_fb", "run: ----");  
                }  
            };  
        scheduledExecutorService.scheduleWithFixedDelay(runnable, 1, 1, TimeUnit.SECONDS); 

第一次延迟1秒之后,以后每次也延迟1秒执行。


线程池其他常用功能

1.shutDown()  关闭线程池,不影响已经提交的任务

2.shutDownNow() 关闭线程池,并尝试去终止正在执行的线程

3.allowCoreThreadTimeOut(boolean value) 允许核心线程闲置超时时被回收

4.submit  一般情况下我们使用execute来提交任务,但是有时候可能也会用到submit,使用submit的好处是submit有返回值,举个栗子:
public void submit(View view) {  
    List<Future<String>> futures = new ArrayList<>();  
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 5, 1,  
            TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());  
    for (int i = 0; i < 10; i++) {  
        Future<String> taskFuture = threadPoolExecutor.submit(new MyTask(i));  
        //将每一个任务的执行结果保存起来  
        futures.add(taskFuture);  
    }  
    try {  
        //遍历所有任务的执行结果  
        for (Future<String> future : futures) {  
            Log.d("google_lenve_fb", "submit: " + future.get());  
        }  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    } catch (ExecutionException e) {  
        e.printStackTrace();  
    }  
}  
  
class MyTask implements Callable<String> {  
  
    private int taskId;  
  
    public MyTask(int taskId) {  
        this.taskId = taskId;  
    }  
  
    @Override  
    public String call() throws Exception {  
        SystemClock.sleep(1000);  
        //返回每一个任务的执行结果  
        return "call()方法被调用----" + Thread.currentThread().getName() + "-------" + taskId;  
    }  
} 

使用submit时我们可以通过实现Callable接口来实现异步任务。在call方法中执行异步任务,返回值即为该任务的返回值。Future是返回结果,返回它的isDone属性表示异步任务执行成功!

扩展线程池ThreadPoolExecutor

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

1.beforeExecute():任务执行前执行的方法;

2.afterExecute():任务执行后执行的方法;

3.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()则是在关闭线程池的方法中调用,而关闭线程池有两个方法,我贴其中一个:

所以,我们要扩展线程池,只需要重写这三个方法,并实现我们自己的功能即可,这三个方法分别都会在任务执行前调用、任务执行完成后调用、线程池关闭后调用。 
这里我验证一下,继承自ThreadPoolExecutor 并实现那三个方法:
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", "线程池结束!");
    }
}

而运行后的结果则是,这正符合刚刚说的:

11-17 05:47:51.184 1602-1619/? V/zxy: 线程:pool-6-thread-1准备执行任务!
11-17 05:47:51.184 1602-1619/? V/zxy: 线程:pool-6-thread-1正在执行任务!
11-17 05:47:53.184 1602-1619/? V/zxy: 线程:pool-6-thread-1任务执行结束!
11-17 05:47:58.896 1602-1619/? V/zxy: 线程池结束!

参考资料

1.http://blog.csdn.net/u010687392/article/details/49850803

2.《Android开发艺术探索》 


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值