Android/Java中线程池详解

参考文章:
http://gityuan.com/2016/01/16/thread-pool/
https://blog.csdn.net/pozmckaoddb/article/details/51478017


前言

在我们平时的工作或者课堂的学习中,对于开一个子线程常见到如下写法:


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

这种写法的弊端有以下几点:
1.每次都要new一个对象,性能较低;
2.线程缺乏统一的管理,相互之间竞争,占用系统资源;
3.缺乏更多的功能,如定时执行,定期执行,线程中断等;

如果是Android开发中遇到子线程UI线程的切换时,一般程序员会这么写:
首先用Handler来处理UI线程;

Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 1:
                    //...
                    break;
            }
        }
};

然后在子线程

Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                Message message = handler.obtainMessage();
                message.what = 1;
                handler.sendEmptyMessage(message.what);
            }
        });
thread.start();

看起来比较low,演示一下看起来比较高级的写法
演示以下写法,是结合线程池和Future的用法进行子线程和UI线程的切换,写法更加简洁;

//定义一个线程池,核心线程给5个
private ExecutorService executorService = Executors.newFixedThreadPool(5);
//模拟我们要处理的数据List
private List<String> strings = new ArrayList<>();

//参数传入一个Callable是为了返回结果(这里返回的是一个List)
Future<List<String>> submit = executorService.submit(new Callable<List<String>>() {
            @Override
            public List<String> call() throws Exception {
                Log.d("TAG", "call: " + "currentThread: " + Thread.currentThread().getName());
                //模拟耗时操作
                Thread.sleep(3000);
                strings.add("new String1");
                strings.add("new String2");
                return strings;
            }
        });
        try {
            //获取到返回的结果,可以开始进行UI的操作
            //之所以前面用到Future,因为通过future可以使用get方法获取结果
            List<String> result = submit.get();
            Log.d("TAG",  " currentThread: " + Thread.currentThread().getName());
            for (String string : result) {
                Log.d("TAG", "onCreate: " + string);
            }
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

根据运行结果的时间和打印出的线程名来看:可以看到主线程是在子线程执行完任务之后才去进入主线程运行的    

2021-10-31 11:18:15.864 24253-24282/com.example.switchx D/TAG: call: currentThread: pool-2-thread-1
2021-10-31 11:18:18.864 24253-24253/com.example.switchx D/TAG:  currentThread: main
2021-10-31 11:18:18.864 24253-24253/com.example.switchx D/TAG: onCreate: new String1
2021-10-31 11:18:18.864 24253-24253/com.example.switchx D/TAG: onCreate: new String2


关于Future的介绍,可以去我另一篇文章:https://blog.csdn.net/qq873044564/article/details/120932035

由上面的例子,可以看出线程池的好处了吧,既方便写法又简单
java提出了一系列线程池的用法,且在不断改进,所以我们为什么不用呢?

正文


线程池优点:
1.通过重用已存在的线程,降低线程创建和销毁造成的消耗;
2.提高系统响应速度,当有任务到达时,无须等待新线程的创建便可以立即执行;
3.方便线程并发数的管控;


在正式介绍用法之前先介绍一下线程池的参数和排队策略:

线程池的参数

java提供的四种线程池:
newCachedThreadPool, newFixedThreadPool, newScheduledThreadPool, newSingleThreadExecutor

自定义线程池的参数说明,其实以上java提供的四中线程池四种方法最终都是调用到以下的方法

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

[1]corePoolSize(线程池基本大小,核心工作线程)必须大于或等于0,当向线程池提交一个任务时,若线程池已创建的线程小于corePoolSize,
即使存在空闲线程,也是通过创建一个新线程来执行该任务;反之,若大于等于,则会判断是否有空闲线程来决定是否
创建新的线程;除了利用提交新任务来创建和启动线程,也可以通过prestartCoreThread和prestartAllCoreThreads
来提前启动线程池里的基本线程;
[2]maximumPoolSize(线程池最大大小)必须大于或等于1,所允许的最大线程个数.对于无界队列,可忽略该参数;
maximumPoolSize必须大于或等于corePoolSize;
[4]keepAliveTime(线程存活保持时间)必须大于或等于0;当线程池的线程个数多余corePoolSize时,线程的空闲
时间多余keepAliveTime就会终止
[5]workQueue(任务队列/等待队列/缓存队列)不能为空;(有界队列)大于CorePoolSize小于maximumPoolSize会进入这里等待;
[6]threadFactory(线程工厂)不能为空,默认为DefaultThreadFactory类,用于创建新线程,由同一个threadFactory
创建的线程,属于同一个ThreadGroup;threadFactory创建线程的方式也是采用new Thread方式;
[7]handler(线程饱和策略,即达到最大线程数时如何处理)不能为空,默认策略为ThreadPoolExecutor.AbortPolicy。
ThreadPoolExecutor里面4种拒绝策略
1].ThreadPoolExecutor.AbortPolicy()
抛出java.util.concurrent.RejectedExecutionException异常
2].ThreadPoolExecutor.CallerRunsPolicy
不进入线程池执行,在调用者线程中执行
3].ThreadPoolExecutor.DiscardOldestPolicy();
丢弃最旧的任务;
4].ThreadPoolExecutor.DiscardPolicy
丢弃将要加入队列的任务;

排队策略

图解:

 解释:
线程池占用的顺序 核心工作线程 -> 等待队列 -> 新的工作线程

如:核心线程为5个,等待队列为10,最大为128个的话
即先占用5个核心线程(1->5)
核心工作满了占用等待队列(6->15)
等待队列满了继续开新的工作线程(16->138)
工作线程满了,如果这时再向线程池中提交任务,会抛出异常RejectedExecutionException
需要对抛出的异常进行catch,否则会ForceClose


排队的三种通用策略:


1].直接排队:默认工作队列SynchronousQueue,将任务直接提交给线程而不进入等待队列;
如果不存在可用于立即运行任务的线程,将会构造一个新的线程,不得超过最大线程数.
2].无界队列,例如LinkedBlockingQueue,使用无界队列将导致在corePoolSize线程忙时,
新任务在队列中等待.这样创建的线程就不会超过corePoolSize(相当于控制了并发的线程数)
当每个任务完全独立互不影响时,适用于无界队列,例如在WEB服务器中,用于处理瞬间突发请求;
3].有界队列.使用有限的maximumPoolsize时,有界队列,如ArrayBlockQueue,超过核心线程会先加入队列
等待,超出队列范围后继续生成线程,创建的线程数不得超过最大线程数;

BlockingQueue接口的实现类:


[1]SynchronousQueue:同步的阻塞队列.其中每个插入操作必须等待另一个线程的对应
移除操作,等待过程一致处于阻塞的状态;SynchronousQueue没有容量;
[2]LinkedBlockingQueue:基于链表的无界阻塞队列,与ArrayBlockingQueue一样采用FIFO原则
对元素进行排序,基于链表的队列吞吐量要高于基于数组的队列;
[3]ArrayBlockingQueue:基于数组的有界阻塞队列;队列按照先进先出对元素进行排序,
固定大小的数组,无法增加其容量.向已满队列中放入元素会导致操作受阻塞,同样从空队列
中提取元素会导致类似阻塞;ArrayBlockingQueue构造方法有fairness参数选择是否采用
公平策略;
[4]DelayedWorkQueue:基于优先级的无界阻塞队列.优先级队列的元素按照其自然顺序
进行排序.
[5]DelayQueue叫做延迟队列,队列中的元素必须是Delayed的实现类,队列中的元素不但会按照延迟时间delay进行排序,
且只有等待元素的延迟时间delay到期后才能出队。

java提供的四种常用线程池:
newCachedThreadPool, newFixedThreadPool, newScheduledThreadPool, newSingleThreadExecutor

各自采用的等待队列
工厂方法                workQueue
newCachedThreadPool        SynchronousQueue
newFixedThreadPool        LinkedBlockingQueue
newSingleThreadExecutor    LinkedBlockingQueue
newScheduledThreadPool    DelayedWorkQueue

代码举例


1.newCachedThreadPool
创建一个可缓存的线程池,线程池的大小上限为MAX_VALUE;
当线程池的线程空闲时间超过60s则会自动回收该线程;

public void cachedThreadPoolDemo(){
    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
        final int index = i;

        cachedThreadPool.execute(new Runnable() {

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+", index="+index);
            }
        });

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

pool-1-thread-1, index=0
pool-1-thread-1, index=1
pool-1-thread-1, index=2
pool-1-thread-1, index=3
pool-1-thread-1, index=4

由结果可知:
都是在同一个线程中运行,后面的线程都是在复用之前的;(即直接提交,无等待队列)

2.newFixedThreadPool
创建一个固定的线程池,可指定核心线程数,对于多出来的线程会在等待队列LinkedBlockingQueue中等待;

public void fixedThreadPoolDemo(){
    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
    for (int i = 0; i < 6; i++) {
        final int index = i;

        fixedThreadPool.execute(new Runnable() {

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+", index="+index);
            }
        });

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

pool-1-thread-1, index=0
pool-1-thread-2, index=1
pool-1-thread-3, index=2
pool-1-thread-1, index=3
pool-1-thread-2, index=4
pool-1-thread-3, index=5

线程池3个,轮番执行,运行完之后释放继续执行;(无界队列,即等待队列是无限的)

3.newSingleThreadExecutor
只有一个线程的线程池,所有任务都保存在等待队列中,等待唯一的线程去执行任务;
所有任务按照顺序执行;

public void singleThreadExecutorDemo(){
    ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    for (int i = 0; i < 3; i++) {
        final int index = i;

        singleThreadExecutor.execute(new Runnable() {

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+", index="+index);
            }
        });

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

pool-1-thread-1, index=0
pool-1-thread-1, index=1
pool-1-thread-1, index=2

可以看成相当于newFixedThreadPool核心线程数为1

4.newScheduledThreadPool
一种可以定时执行或者周期性执行的线程池,可指定线程池的核心线程个数;

public void scheduledThreadPoolDemo(){
    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
    //定时执行一次的任务,延迟1s后执行
    scheduledThreadPool.schedule(new Runnable() {

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+", delay 1s");
        }
    }, 1, TimeUnit.SECONDS);

    //周期性地执行任务,延迟2s后,每3s一次地周期性执行任务
    scheduledThreadPool.scheduleAtFixedRate(new Runnable() {

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+", every 3s");
        }
    }, 2, 3, TimeUnit.SECONDS);
}

pool-1-thread-1, delay 1s
pool-1-thread-1, every 3s
pool-1-thread-2, every 3s
pool-1-thread-2, every 3s
...

四种方法的区别:

 其他参数都相同,其中线程工厂的默认类为DefaultThreadFactory,线程饱和的默认策略为ThreadPoolExecutor.AbortPolicy。


线程池关闭


shutdown或者shutdownNow来关闭线程池;
shutdown:将线程池状态设置为SHUTDOWN状态,然后中断所有没有执行任务的线程;
shutdownNow:将线程池的状态设置为STOP,然后中断所有任务,并返回等待执行任务的
列表;

优化


合理地配置线程池:
任务类别可划分为CPU密集型任务,IO密集型任务和混合型任务;
对于CPU密集型任务:线程池中线程个数应尽量少,不应大于CPU核心数;
对于IO密集型任务:由于IO操作速度远低于CPU速度,在运行这类任务时,CPU绝大多数
时间处于空闲状态,线程池可以配置尽量多的线程,以提高CPU利用率;
对于混合型任务:可以拆分为上面两种,通过拆分再执行的吞吐率高于串行执行的吞吐率;

线程池监控


taskCount:线程池需要执行的任务数量;
completedTaskCount:线程池在运行过程中已完成的任务数量,小于等于taskCount;
largestPoolSize:线程池创建的最大线程数量,通过这个知道线程池是否满了;
getpoolSize:线程池的线程数量;
getActiveCount:获取活动的线程数;
通过重写线程池的beforeExecute,afterExecute和terminated方法,可以在任务执行前
,执行后和线程池关闭前自定义行为;

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值