JUC-你要是能把“线程池”讲清楚,我就做你女朋友

234 阅读4分钟

这是我参与更文挑战的第2天,活动详情查看: 更文挑战

笑话,你觉得我没女朋友,是因为不懂线程池吗

概念及作用

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用写线程池。在开发过程中,合理使用线程池能够带来如下3个好处。

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行
  • 提高线程的客观理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌 ——《Java并发编程的艺术》

实现原理

先复习下,线程状态

  • new——新建(线程被创建且未启动的状态)
    • 继承Thread类(调用start方法)
    • 实现Runnable接口(调用run方法)
    • 实现Callable接口(调用call方法)
    • 走线程池()
  • runnable——就绪(调用start()之后运行之前的状态)
  • running——运行(run()正在执行时线程的状态,可能由于某些因素而退出running,如时间、异常、锁、调度等)
  • blocked——阻塞(即阻塞状态)
    • 同步阻塞——锁被其他线程占用
    • 主动阻塞——调用Thread的某些方法,主动让出cpu执行权,比如sleep()、join()等
    • 等待阻塞——执行了wait()
  • dead——终止(即终止状态,是run执行结束,或因异常退出后的状态,此状态不可逆装

再探探,线程池参数

  • corePoolSize - 常驻核心线程数,即使空闲时仍保留在池中的线程数,除非设置 allowCoreThreadTimeOut
  • maximumPoolSize - 池中允许的最大线程数 。必须大于或等于1,如果等于corePoolSize那就是固定大小线程池。队列缓存达到上限后,如果还有新任务需要处理,线程池就会创建新的线程。
  • keepAliveTime - 表示线程池中线程的空闲时间,当空闲时间达到这个值,线程会被销毁,知道只剩下corePoolSize个线程为止,避免浪费内存和句柄资源
  • unit - keepAliveTime参数的时间单位
  • workQueue - 用于在执行任务之前使用的队列。 这个队列将仅保存execute方法提交的Runnable任务。当请求线程数大于核心线程数时,线程进入BlockingQueue阻塞队列。建议使用有界队列,可以增加系统的稳定性和预警能力
  • threadFactory - 用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字
  • handler - 饱和策略,执行被阻止时使用的处理程序,因为达到线程限制和队列容量,也是一种简单的限流保护
    • AbortPolicy:直接抛出异常(默认情况)
    • CallerRunsPolicy:只用调用者所在线程来运行任务
    • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
    • DiscardPolicy:不处理,丢弃掉
    • 自定义策略:友好的拒绝策略有三种:保存到数据库进行削峰填谷,在空闲时再提取出来执行;转向某个提示页面;打印日志

后瞅瞅,处理流程机制

俗话说一图胜前言,可能没啥 image.png 这是一个十分经典的线程池执行流程图,但可能初次看有些抽象,那就让小学弟来加工一下吧

最后聊聊,弱鸡码农的理解

image.png 我就不信结合这两张图,你还看不明白,如果还看不明白,那你就不配做个LPL忠实观众,不对不对,那你就不配做个踏实的程序员

示例代码

折腾这么长时间,不摆弄点代码,对不起大家,来,上菜

主任务

public class Task implements Runnable{

    private final static AtomicLong a = new AtomicLong(1L);

    @Override
    public void run() {
        //睡三秒,假装执行“换衣服”任务
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("我队服换好了,我是召唤师num_"+a.getAndIncrement());
    }
}

线程工厂

public class UserThreadFactory implements ThreadFactory {

    private final String namePrefix;
    private final AtomicInteger nextId = new AtomicInteger();

    public UserThreadFactory(String whatFeatureOfGroup) {
        namePrefix = "UserThreadFactory's-" + whatFeatureOfGroup + "-Worker-";
    }

    @Override
    public Thread newThread(Runnable task) {
        String name = namePrefix + nextId.getAndIncrement();
        Thread thread = new Thread(null,task,name,0);
        System.out.println(thread.getName());
        return thread;
    }
}

自定义拒绝策略

public class UserThreadHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println("我被拒绝了:"+executor.getActiveCount()+executor.getCompletedTaskCount()
            +executor.getCorePoolSize()+executor.getQueue());
    }
}

线程池主方法

public class ThreadPoolTest {
    public static void main(String[] args) {
        //初始化队列
        BlockingQueue blockingDeque = new LinkedBlockingQueue(3);
        //创建工厂对象
        UserThreadFactory userThreadFactory = new UserThreadFactory("FPX战队更衣室");
        //创建拒绝策略
        UserThreadHandler userThreadHandler = new UserThreadHandler();
        //构建线程池
        ThreadPoolExecutor tpe1 = new ThreadPoolExecutor(
                1,2,20,TimeUnit.SECONDS,blockingDeque,userThreadFactory,userThreadHandler);
        //申明任务对象
        Task task = new Task();
        //线程池发力时刻
        for (int i = 1; i <= 6; i++) {
            tpe1.execute(task);
        }
        //用完了记得关闭线程池,要不然他会一直占着资源
        tpe1.shutdown();
    }
}

主方法执行效果

整体流程正好对应上面小学弟的流程执行过程,懂的都懂 image.png

书上说使用时需注意

  • 合理设置各类参数,应根据实际业务场景来设置合理的工作线程数
  • 线程资源必须通过线程池提供,不允许应用中自行显式创建线程
  • 创建线程或线程池时请指定有意义的线程名称,方便出错是回溯

最后,到底怎样才能合理的配置线程池参数

书上也说了,根据任务特性来分角度分析

  • 性质:
    • CPU密集型:尽可能小的配置线程,如“N+1”个线程的线程池
    • IO密集型:尽可能多的配置线程,如“2*N”个线程
    • 混合型任务:如果两个任务执行时间差别不大,可进行拆分为两个任务,如果差别太大就不用分解
  • 优先级:高、中、低 (可考虑PriorityBlockingQueue优先级队列,可以让优先级高的任务先执行)
  • 执行时间:长、中、短
  • 依赖性:是否依赖其他系统资源,如数据库连接 还有人说进一步的优化理论:最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目 还有人说就定死了,当前服务器的cpu核心数就行了,别管那么多
Runtime.getRuntime().availableProcessors();//获取当前cpu的核心数量

总而言之,言而总之,还是要根据业务情况,结合实际,来做出相应的配置。

参考资料

  • 《Java并发编程的艺术》
  • 《码出高效Java开发手册》

小伙子快醒醒,你女盆友跟别人跑了!!!

image.png