线程的高效利用——线程池

线程的开销

线程作为一种昂贵的资源,开销包括如下几点:
1、线程的创建与启动的开销。
2、线程的销毁的开销。
3、线程调度的开销。线程的调度会产生上下文切换,从而增加处理器资源的消耗。
4、一个系统可以创建的线程数受限于该系统的处理器数目。线程数量的临界值总是处理器的数目。

线程池的工作方式

鉴于以上线程资源的消耗,我们需要一种有效的使用线程的方式,线程池就是高效利用线程的一种常见方式。它的工作原理如下:
线程池内部可以预先创建好一定数量的工作者线程,客户端将其需要执行的任务作为一个对象提交给线程池,线程池可以将这些任务缓存到工作队列中,而线程池内部则不断的从队列中取出任务并执行。因此,线程池可以看做是生产者消费者模式,其内部维护的工作者线程相当于消费者线程,客户端的线程相当于生产者线程,客户端提交的代码相当于产品,线程池内部的工作队列相当于信道。

ThreadPoolExecutor基础

ThreadPoolExecutor就是一个常用的线程池,它参数最多的构造器声明如下

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

corePoolSize: 它是线程池的核心线程大小,在线程池初期,来一个任务就会创建一个线程,当线程池中的线程数达到核心线程时,新来的任务就会存入到工作队列中。所以corePoolSize相当于一个临界值。
maximumPoolSize:它用来指定最大线程池大小。当线程池中工作队列满了之后,线程池就会继续创先线程,但是创建的线程数量不能超过maximumPoolSize。
keepAliveTime–unit:这两个参数一起用来指定线程中空闲线程的最大存活时间。空闲线程数即
maximumPoolSize-corePoolSize。
workQueue:工作队列,相当于生产者消费者模式中的传输通道。
threadFactory:指定用于创建工作者线程的线程工厂。
handler:线程池的拒绝策略。该方法如下:
在这里插入图片描述
r代表的是执行的任务,executor代表的是当前的线程池实例。
其中RejectedExecutionHandler 有几个默认的实现类:
1、ThreadPoolExecutor.AbortPolicy
ThreadPoolExecutor.AbortPolicy是默认的执行器,当线程池的线程数量超过了最大线程数,则会直接抛出异常。
2、ThreadPoolExecutor.DiscardPolicy
ThreadPoolExecutor.DiscardPolicy表示丢弃当前执行的任务,但是不抛出任何异常。
3、ThreadPoolExecutor.DiscardOldestPolicy
ThreadPoolExecutor.DiscardOldestPolicy将工作中最老的任务丢掉,然后从新尝试接纳被拒绝的任务。
4、ThreadPoolExecutor.CallerRunsPolicy
ThreadPoolExecutor.CallerRunsPolicy表示在客户端执行被拒绝的任务。

各个参数的整体配合流程如下:
客户端不断的给线程池提交任务,每提交一个新的任务,线程池就创建一个线程来处理该任务,当任务数量超过核心线程数大小时,新来的任务就会存入到工作队列中,然后这些核心线程会不断的从工作队列中取出任务来执行。线程池将任务存入到工作队列中使用的BlockingQueue的非阻塞方法offer,因此工作队列满并不会使提交任务的客户端暂停。当工作队列满了之后,线程池会继续创建新的线程,直到当前线程数达到最大线程数大小,如果此时再有新的任务,客户端执行的任务就会被拒绝。那些超出线程池核心大小的线程如果没有任务的时间达到了keepAliveTime,就会被清理掉,如果keepAliveTime设置的值太小,可能导致线程池持续的创建销毁线程,反而增加了开销。

ThreadPoolExecutor.shutdown()表示用来关闭线程池,已提交到队列中的任务会继续执行,但新提交的任务则会被拒绝。
ThreadPoolExecutor.shutdownNow()关闭线程池时,正在执行的任务也会停止,新的任务也会不会执行。

线程池结果的处理

public Future<?> submit(Runnable task) 方法的参数是Runnable 接口,其抽象方法run() 没有返回值,其作用是向线程池中提交任务,并不关心返回值,且无法抛出异常。
public Future submit(Callable task)可以处理任务的返回结果,其参数是一个Callable接口,其泛型是任务返回的类型,该方法可以抛出异常。结果可以通过Future.get()来获取,当处理的任务发生异常时,Future.get()也会将异常抛出:
在这里插入图片描述

当get方法被调用时,如果该任务还未完成,则会阻塞代码。所以应该尽可能的向线程池提交任务,并且get的调用应该放到需要数据的时刻,这期间先执行其他任务。
举一个使用Fucture接口的例子,我们从网上买完东西之后不会一直等待着收货,这期间肯定会去做别的事情,以此举例写一个小demo

public class ExecutorDemo {
    final static int cpuNum = Runtime.getRuntime().availableProcessors();
    static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0,
            cpuNum * 2,
            4,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100),
            new ThreadPoolExecutor.CallerRunsPolicy());


    public static void main(String[] args) {
        List<String> list = new ArrayList<String>() {{
            add("护肤品");
            add("水果");
        }};

        List<Future<String>> futures = new CopyOnWriteArrayList<>();
        //从网上买东西,异步处理
        list.forEach(item -> {
            Future<String> stringFuture = buySomething(item);

            futures.add(stringFuture);
        });

        //dosomething else


        //东西到了,进行验货
        futures.forEach(item -> {
            //最多等待3秒
            try {
                String something = item.get(3, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }

        });

    }

    public static Future<String> buySomething(String something) {
        return threadPoolExecutor.submit(new Callable<String>() {
            @Override
            public String call() {
                return something.concat("is coming");
            }
        });
    }
}

Executors工具类

Executor 是我们使用线程池中最抽象的接口,我们只需要把任务扔给它即可,并不需要关注它是如何执行的,它只有如下一个抽象的方法:

public interface Executor {
    void execute(Runnable command);
}

ExecutorService是实现它的一个接口,它就是我们常用的线程池,而Executors这个工具类中的一些方法可以快速的创建线程池。
Executors.newCachedThreadPool()
该方法的返回值相当于

new ThreadPoolExecutor(0,
                Integer.MAX_VALUE,
                60L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>());

即一个核心线程数大小为0,最大线程数不受限制,且线程存活时间为60s,队列是SynchronousQueue阻塞队列的线程池。因为它没有核心线程数、线程数量没有上线,则来一个任务就会创建线程,当极端情况下,任务数量太多,有1w个任务都在执行,则就会创建1w个线程,从而导致频繁的上线文切换使性能下降。并且因为是阻塞队列,但生产者线程执行的是非阻塞方法offer,当没有消费者线程从队列中take任务时,就会入队失败,offer返回false。
因此该线程池适合执行大量耗时较短且提交频率较高的任务。耗时太长且提交频率较高的任务则不适合。
Executors.newFixedThreadPool(int nThreads)
该方法的返回值相当于

 new ThreadPoolExecutor(nThreads,
                nThreads,
                0L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>())

即没有空闲线程,核心线程数和最大线程数一样。线程数的数量由用户自己指定。队列是无界队列。因为它不会自动清理线程,所以需要我们手动关闭线程池。
Executors.newSingleThreadExecutor()
该方法相当于Executors.newFixedThreadPool(1),该线程池确保在任意时刻只有一个线程在执行一个任务,因此,它是一个串行的方法,可以用于访问非线程安全对象,但是又不想借助锁的任务。

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值