乐趣区

关于java:JAVA并发编程线程池原理使用和参数设置详解

1. 线程池简介

2. 线程池应用

3. 线程池参数详解

4. 如何正当设置线程池参数

1. 线程池简介
咱们先来介绍一下,什么是线程池?
在以往的博客中,咱们介绍线程的时候,无论是 Runnable,还是 Calllable 这些,最终都是应用 new Thread().start(); 的形式进行创立,其实在实在的生产环境中,咱们是禁止应用这种写法的,因为这样有几个重大的弊病:
a. 每次 new Thread() 新建对象,会造成开销,导致服务器运行性能变差。
b. 在无限度 new Thread()的状况下,咱们一不小心就会创立过多的线程,相互之间竞争资源,可能导致上下文频繁切换,占用资源过多,死锁或者 oom。
c. 不足更多功能,如定时执行、定期执行、线程中断。

于是,JAVA 线程池就产生了!

JAVA 线程池做的次要工作是 管制运行的线程的数量 ,解决过程中将工作放入队列而后在线程创立后启动这些工作,如果线程数量超过了最大执行数量, 超出数量的线程将排队等待,期待其它线程执行结束,再从队列中取出工作来执行

它的次要特点:线程服用,管制最大并发数,治理线程。

第一:升高资源耗费 ,通过反复利用本人创立的线程升高线程创立和销毁造成的耗费。
第二:进步响应速度 ,当工作达到时,工作能够不须要等到线程创立就能立刻执行
第三:进步线程的可管理性,线程是稀缺资源,如果无限度地创立,不仅会耗费系统资源,还会升高零碎的稳定性,应用线程池能够对立地进行调配,调优和监控。

多说无用,咱们来看看如何应用 JAVA 线程池吧!

2. 线程池应用
咱们先介绍一下 JAVA 自带的创立线程池的工具类!

Executors

它提供了三个办法来创立线程池:

        ExecutorService threadPool = Executors.newFixedThreadPool(5);// 一池固定数线程
        ExecutorService threadPoo2 = Executors.newCachedThreadPool();// 一池多线程
        ExecutorService threadPoo3 = Executors.newSingleThreadExecutor();// 一池一线程

顾名思义:
newFixedThreadPool()办法就是创立一个 固定线程数 的线程池,
newCachedThreadPool()办法会依据你计算机和工作的状况,进行主动增减线程
newSingleThreadExecutor() 创立一个单线程的线程池,每次只能执行一个工作

同时创立完线程池之后,应用线程池自带的submit 或者 excute 办法,而后用 lambda 表达式传入函数体后,就能够执行工作了!

        threadPool.submit();
        threadPool.execute();

这时,咱们能够应用一个线程池尝试着应用一下

        ExecutorService threadPool = Executors.newFixedThreadPool(5);// 一池固定数线程
        // 模仿 10000 个用户来办理业务,每一个用户就是来自内部的申请线程
        try {for (int i = 1; i < 10000; i++) {threadPool.submit(() -> {System.out.println(Thread.currentThread().getName() + "正在办理业务");
                });
            }
        } catch (Exception e) {e.printStackTrace();
        } finally {threadPool.shutdown();
        }

咱们创立了一个线程池,固定数量为 5,而后启动 10000 个线程,看看运行状况。

能够看出,线程编号始终没有超过咱们设置的初始值 5。

这时,咱们能够换一个线程池应用一下,咱们应用 cache 线程池:

 
        ExecutorService threadPoo1 = Executors.newCachedThreadPool();// 一池多线程
        // 模仿 10000 个用户来办理业务,每一个用户就是来自内部的申请线程
        try {for (int i = 1; i < 10000; i++) {threadPoo1.submit(() -> {System.out.println(Thread.currentThread().getName() + "正在办理业务");
                });
            }
        } catch (Exception e) {e.printStackTrace();
        } finally {threadPoo1.shutdown();
        }


能够看出,线程启动地越多,该线程池就会启动越多的线程来适应需要。

让咱们来看一下最初一个线程池:

        ExecutorService threadPoo1 = Executors.newSingleThreadExecutor();// 一池一线程
        // 模仿 10000 个用户来办理业务,每一个用户就是来自内部的申请线程
        try {for (int i = 1; i < 10000; i++) {threadPoo1.submit(() -> {System.out.println(Thread.currentThread().getName() + "正在办理业务");
                });
            }
        } catch (Exception e) {e.printStackTrace();
        } finally {threadPoo1.shutdown();
        }

查看后果:

能够看出,永远 只有一个线程 在运行。

是不是十分神奇?!咱们能够看一下创立线程池的办法到底怎么执行地,竟然有这种成果!

首先看一下固定数线程的办法:

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, // 常驻外围线程数
                                      nThreads,// 最大线程数
                                      0L,// 多余的闲暇线程的存活工夫 
                                      TimeUnit.MILLISECONDS,// 多余线程存活单位
                                      new LinkedBlockingQueue<Runnable>()); // 应用的阻塞队列
    }

cache 线程池的办法:

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

单线程池的办法:

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

从我给的源码正文中能够看出,
固定数线程池最大和常驻线程数都是传入的参数
而 cache 线程池的最大线程数是 Integer.MAX_VALUE
单线程池底层应用了 SynchronousQueue : 不存储元素的阻塞队列,即单个元素的队列。
多线程池的底层应用 了 LinkedBlockingQueue : 由链表组成的有界(然而默认值为 Integer.MAX_VALUE 阻塞队列
阻塞队列能够看我之前写的这篇文章:JAVA 并发编程——阻塞队列实践及其 API 介绍
所以就导致了为什么会有以上的运行后果!

所以在生产中,咱们到底如何应用呢??

答案是————一个都不应用 ,因为 LinkedBlockingQueue 它的最大长度是 Integer.MAX_VALUE, 那就相当于无界 可能会让服务器内存爆仓产生 OOM,所以接下来,咱们学习一下自定义创立线程池的参数!

3. 线程池参数详解
咱们先点到 ThreadPoolExecutor 这个类里看一下,它到底有几个参数:

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

咱们能够看到,有七个参数
corePoolSize 线程池中 常驻外围线程数
maximumPoolSize 线程池可能包容的同时执行的 最大线程数 ,此值必须大于等于 1
keppAliveTime 多余 的闲暇线程的 存活工夫
unit 存活工夫 单位
workQueue 工作队列,被提交然而尚未执行的工作
threadFactory 示意生成线程池中工作线程的线程工厂,用于创立线程,个别默认即可
handler 回绝策略,示意 队列满了并且工作线程大于等于线程池的最大线程数,如何解决。

后面几个参数都好了解,重要的是这个办法能够自定义阻塞队列的类型,咱们就能够防止应用 LinkedBlockingQueue 了

接下来,咱们看一下 handler 回绝策略:
当外围线程数满了,阻塞队列也满了的时候,再进来的线程,就会启动回绝策略,个别常见的回绝策略有这几种:

AbortPolicy(默认):间接抛出 RejectExcutionException 异样阻止线程运行。
CallerRunsPolicy: 将多进去的工作 退还给调用者,从而升高流量。

DiscardOldestPolicy:摈弃期待队列中期待最久的工作,而后把当前任务退出期待队列。

DiscardPolicy:间接抛弃工作

接下来,咱们尝试一下本人新建线程池,并尝试着应用该线程池执行一下工作。

     ThreadPoolExecutor threadPoolExecutor
                = new ThreadPoolExecutor(2,// 常驻外围线程数
                5,// 最大外围线程数
                1L,// 多余的闲暇线程的存活工夫
                TimeUnit.SECONDS,// 多余闲暇线程存活工夫单位
                new ArrayBlockingQueue<Runnable>(10),// 阻塞队列
                Executors.defaultThreadFactory(),// 生成线程池中工作线程的线程工厂
                new ThreadPoolExecutor.AbortPolicy());// 回绝策略

        // 模仿多个用户来办理业务,每一个用户就是来自内部的申请线程
        try {for (int i = 1; i < 10000; i++) {threadPoolExecutor.submit(() -> {System.out.println(Thread.currentThread().getName() + "正在办理业务");
                });
            }
        } catch (Exception e) {e.printStackTrace();
        } finally {threadPoolExecutor.shutdown();
        }
    }

在应用以后回绝策略下,很显著能够看出,执行线程超出线程池的数量了,会抛异样,咱们来看一下运行后果:

咱们能够换一个回绝策略试一下:比如说 DiscardPolicy(摈弃工作)

        ThreadPoolExecutor threadPoolExecutor
                = new ThreadPoolExecutor(2,// 常驻外围线程数
                5,// 最大外围线程数
                1L,// 多余的闲暇线程的存活工夫
                TimeUnit.SECONDS,// 多余闲暇线程存活工夫单位
                new ArrayBlockingQueue<Runnable>(10),// 阻塞队列
                Executors.defaultThreadFactory(),// 生成线程池中工作线程的线程工厂
                new ThreadPoolExecutor.DiscardPolicy());// 回绝策略


它就运行实现了,且没有抛出任何异样。

4. 如何正当设置线程池参数
如何配置线程池的参数,其实咱们最须要配置的参数只有一个:
corePoolSize 线程池中常驻外围线程数

当咱们的线程池应用场景是在 CPU 密集型(过程绝大部份工作依附 cpu 的计算能力实现),那么为了避免上下文的切换造成的开销,咱们个别设置 最大线程数等于 CPU 核数(核数获取办法:Runtime.getRuntime().availableProcessors();)

当咱们的线程池应用场景是在 IO 密集型(绝大部分工作就是在读入,输入数据), 那么 cpu 不是十分十分忙碌,大部分在期待的状况下, 最大线程能够是 CPU 核数的两倍。

5. 总结
这次咱们介绍了线程池的原理以及参数详解,最重要的是,创立线程池必须要手动应用构造方法创立,不能应用默认的办法类进行创立!

最初,咱们用两张图来概括一下线程池的执行流程:

好了,明天的分享就到这里!

退出移动版