线程池配置参数详解案例

  • 自定义一个线程池
  • newFixedThreadPool
  • newCachedThreadPool
  • 设置单任务线程池

1.自定义一个线程池

  • API

    ThreadPoolExecutor(int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue workQueue)

    • corePoolSize核心线程数:任务数小于核心线程数,会新建线程处理
    • maximumPoolSize 最大线程数
    • keepAliveTime 线程允许的空闲时间(核心线程默认不会超时,使用allowCoreThreadTimeout:允许核心线程超时)
    • workQueue 阻塞队列大小(spring参数queueCapacity),当任务数大于corePoolSize,多余的任务存放在队列等待,当队列满了时,若当前的总线程数小于maximumPoolSize则创建线程,若大于maximumPoolSize,则调用拒绝策略(默认抛异常)
    • 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭。如果为false,那么只有当线程超过corePoolSize,并且时间达到keepAliveTime才会销毁

1.1 Excutor.execute执行过程

  1. 首先判断当前线程数是否小于核心线程数,则创建线程执行
  2. 如果当前线程数大于核心线程数,队列没有满,则将当前任务runnable加入到任务队列中。
  3. 如果队列也满了,就判断当前线程数是否大于最大线程数,如果不大于,就创建线程执行;如果大于最大线程数,就执行拒绝策略。默认策略是抛出异常。
  4. 创建线程里面逻辑里面,会把当前线程数+1,同时会新建一个work线程,将客户端的任务runnable传递进来,然后执行runwork里面方法,核心线程会不断的从队列的取任务,取的方法是take,拿不到任务就阻塞,除非设置allowCoreThreadTimeout。非核心线程,取任务是通过poll方法,里面设置有超时时间,超过时间拿不到就返回null,然后runwork里面跳出while循环销毁线程
  • 测试:

    比如,这里定义了15个任务,调度时,先有5个任务根据corepoolsize创建,然后放入3个到blocking(这3个任务排队等待执行),剩余7个任务,但是当前最大值时11,所以,因为之前已经创建了5个,这里最多会再创建6个,剩余的会拒绝掉。最终调度的任务只有前面的5+3+6=14个任务


    public static void definedExecutor() {
        // 参数 corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue
        // corePoolSize = 5 maximumPoolSize = 11  workQueue = 3
        Executor executor = new ThreadPoolExecutor(5, 11, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(3));
        executorsTest(executor);
    }
    public static void main(String a[]) {
        definedExecutor();
    }
    // 模拟15个任务并发执行
    private static void executorsTest(Executor executor) {
        for (int i = 0; i < 15; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "正在执行");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

运行结果:
在这里插入图片描述

2.newFixedThreadPool

  • 固定大小线程池,底层实现LinkedBlockingQueue
  • 实现原理最小值和最大值一致,使用无界阻塞队列,多余的任务全部都会进入队列排队
    public static void fixedThreadPoolTest() {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        executorsTest(executorService);
    }
        public static void main(String a[]) {
        fixedThreadPoolTest();
    }
    // 模拟15个任务并发执行
    private static void executorsTest(Executor executor) {
        for (int i = 0; i < 15; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "正在执行");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
  • 测试结果:可以看到线程数字始终只有5个
    在这里插入图片描述

3.newCachedThreadPool

  • 设置无界线程池
  • 底层实现SynchronousQueue, 使用一个不存元素的阻塞队列,队列的插入操作put,必须另一个线程的调用移除操作take, 这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,所以当此时大于核心线程的任务,maxsize又非常大时会新建线程
  • SynchronousQueue:生产者消息执行put操作时候,必须要有一条消费者线程执行take,不然生产者现在put操作将会阻塞,直到消费者执行take。take操作将会唤醒该生产线程
  • 条件:maxsize无限大
    public static void cacheThreadPoolTest() {
        Executor executor = Executors.newCachedThreadPool();
        executorsTest(executor);
    }
    public static void main(String a[]) {
        cacheThreadPoolTest();
    }
    // 模拟15个任务并发执行
    private static void executorsTest(Executor executor) {
        for (int i = 0; i < 15; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "正在执行");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
  • 测试
    在这里插入图片描述

4.设置单任务线程池

  • 等价于创建固定大小为1的线程池Executors.newFixedThreadPool(5);
  • 源码
    在这里插入图片描述
    //单线程,始终只有一个
    public static void sigleThreadPool() {
        Executor executor = Executors.newSingleThreadExecutor();
        executorsTest(executor);

    }
    public static void main(String a[]) {
        sigleThreadPool();
    }
    // 模拟15个任务并发执行
    private static void executorsTest(Executor executor) {
        for (int i = 0; i < 15; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "正在执行");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
  • 测试
    在这里插入图片描述

keepaliveTime是如何监控回收

参考另一篇链接

LinkedBlockingQueue和ArrayBlockingQueue的异同

1、队列大小有所不同。ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.Max_Value),对于后者,如果添加速度大于移除速度,在无界情况下,可能会造成内存溢出。
2、数据存储结构不同,arrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的是链表
3、由于ArrayBlockingQueue采用的是数组容器,因此在插入和删除元素时不会产生或者销毁任何额外的对象实例,而LinkedBlocking则会产生一个额外的Node对象,这在长时间高并发处理大量数据时候,对于GC可能会产生较大影响。
4、两者实现队列添加和移除的锁不一样,ArrayBlockingQueue的实现中锁是没有分离的,添加和删除元素用的同一个锁,意味着两者无法真正并行运行。而LinkedBlockingQueue的实现中锁是分离的,其添加采用的是putLock锁,删除采用的是takeLock锁,这样能大大提高队列的吞吐量,生产者和消费者可以并行地操作队列中的数据。

  • ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。之所以没这样去做,猜测是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。

SynchronousQueue

它的同步原理是,当生产者线程准备将元素放入这个队列时候,如果这时没有消费者线程过来,生产者就一直wait,一直等到有消费者过来取走元素,SynchronousQueue就可以返回true了。
同样,当消费者准备从这队列取走元素时候,如果这时没有生产者过来,那么就一直wait,一直等到有生产者过来,消费者就把元素取走,返回true.

『不允许』使用Executors创建线程池

在这里插入图片描述

Executors为什么存在缺陷

如newFixedThreadPool

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

LinkedBlockingQueue如果不指定大小的情况下,默认是一个链表实现的无界阻塞队列,最大值是Integer.Max_value,是可以不断的向其中添加任务,任务过多会导致OOM.
newFixedThreadPool和newSingleThreadExecutor都用了无界的LinkedBlockingQueue

再看newCachedThreadPool

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

虽然这里用的SynchronousQueue,SynchronousQueue是一种无缓冲的等待队列,不会存任何线程,但是这里默认将最大线程数目设置了 Integer.MAX_VALUE,那么大量请求下,必然会导致OOM.
newCachedThreadPool和newScheduledThreadPool都讲最大线程数设置为了Integer.MAX_VALUE

创建线程池正确方式

  ThreadPoolExecutor threadPoolExecutor =  new ThreadPoolExecutor(0,10,60L,TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(10));
 // threadPoolExecutor.allowCoreThreadTimeOut(true);

当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭。
allowCoreThreadTimeOut这个方法就像其字面的意思一样,允许Core Thread超时后可以关闭。
书上说了,要想使线程池没有任务时销毁所有的进程,需要启用allowCoreThreadTimeOut(true)同时将core size设置为0,而实际上,core size设置成任意一个正数值就可以,设置成0时,加不加allowCoreThreadTimeOut(true)都没有影响,因为这个方法是对core thread产生影响,但此时core thread为0,而且当新任务进来时,必须等到workQueue满时才会创建新线程,这也不是我们想要的结果。

线程池大小设置

取决于硬件环境和软件环境
CPU的核心数-》
软件环境:线程的执行情况,
IO密集型(CPU时间片的切换) CPU核心的2倍
CPU密集型(CPU利用率非常高) 以CPU核心数为准,最大同时能够执行的线程数,设置最大线程数为CPU核心数+1
(线程等待的时间+线程CPU的时间)/线程CPU时间*CPU核心数

sevice.shutdown();
sevice.shutdownNow();

excute和submit的区别

submit();实现一个带返回值的线程。可以接受参数runnable 和callable。出现异常不会抛出,通过future.get抛出

比如:要依赖此线程的执行结果时,可以用submit

excuter(); 只能接受一个runnable接口的类。有异常可以抛出异常

  1. submit:
try {
            Future<String> future = service.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    System.out.println("111");
                    System.out.println(1/0);
                    System.out.println("2222");
                    return "返回";
                }
            });
        }catch (Exception e){
            System.out.println("xxxxxxxx");
            e.printStackTrace();
        }

执行结果:未抛出异常
在这里插入图片描述
获取返回值:

future.get()//  阻塞获取结果 ,线程run的时候park,run完了会unpark,future才会执行

2.execute

try {
            service.execute(new Runnable() {
                @Override
                public void run()   {
                    System.out.println("aaa");
                    System.out.println(1/0);
                    System.out.println("bbb");

                }
            });
        }catch (Exception e){
            System.out.println("xxxxxxxx");
            e.printStackTrace();
        }

执行结果:
在这里插入图片描述

线程池用的是独占锁

拒绝策略

默认策略AbortPolicy:直接抛出异常
discardpolicy :任务直接丢弃
discardOldestPolicy:直接丢弃最早的任务,并且执行当前任务
callerRunsPolicy :直接由调用者的线程直接处理,例如直接用主线程运行run方法

合理设置线程池大小

  1. 考虑任务是CPU密集型的,还是IO密集型的

    cpu密集型任务指进程绝大部份任务依靠cpu的计算能力完成,典型的如同科学计算,数值模拟等程序

    io密集型任务指绝大部分任务就是在读入,输出数据,典型的例如web后端程序,主要就是在根据url请求找到对应的资源并输出。mysql的大量读写属于io密集

  2. 考虑线程的执行时间,

  • 如果是CPU密集型的, CPU密集型可配置线程数为CPU个数+1

  • IO密集型的任务,IO不占用CPU,所以可以加大线程数量,可以设置为两倍CPU个数+1

  • 如果任务对其他资源依赖,比如远程调用一个服务等,这里面产生一个等待时间。线程数量可以设置为((等待时间/CPU时间)+1) * CPU数
    公式可以看出,等待时间越长,可以设置更多线程。CPU占用时间高,就减少线程

来自并发编程网的题

  1. 高并发、执行时间短的任务怎么使用线程池?
    高并发、执行时间短,说明会产生大量的上下文切换,可以使用CPU+1的线程数
  2. 并发不高、执行时间长的怎么使用线程池?
    这种要分两种情况:如果执行时间长是在IO操作上,IO不占CPU,可以设置大点,两倍CPU+1
    如果是时间大部分耗在cpu上,则减少线程数,cpu+1
  3. 并发高、执行时间长怎么使用线程池?
    并发高、执行时间长,大量线程执行过慢,会导致服务器压力增加,此时应该考虑如何减少执行时间,或者增加服务器。对于线程池而言,应该根据执行时间,具体对待。参考(2)

线程池容量的动态调整

ThreadPoolExecutor 提 供 了 动 态 调 整 线 程 池 容 量 大 小 的 方 法 : setCorePoolSize() 和
setMaximumPoolSize(),setCorePoolSize:设置核心池大小 setMaximumPoolSize:设置线
程池最大能创建的线程数目大小

线程池的监控

如果在项目中大规模的使用了线程池,那么必须要有一套监控体系,来指导当前线程池的状
态,当出现问题的时候可以快速定位到问题。而线程池提供了相应的扩展方法,我们通过
线程池的 beforeExecute、afterExecute 和 shutdown 等方式就可以实现对线程的监控

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Java线程池是一种用于管理和复用线程的机制,它可以提高多线程程序的性能和效率。在Java中,线程池由ThreadPoolExecutor类实现,通过设置不同的参数可以对线程池的行为进行调整。 以下是Java线程池的一些常用参数及其解释: 1. corePoolSize(核心线程数):线程池中始终保持的活动线程数,即使它们处于空闲状态。当有新任务提交时,如果活动线程数小于corePoolSize,则会创建新线程来处理任务。 2. maximumPoolSize(最大线程数):线程池中允许存在的最大线程数。当活动线程数达到maximumPoolSize并且工作队列已满时,新任务将会被拒绝。 3. keepAliveTime(线程空闲时间):当线程池中的线程数量超过corePoolSize时,多余的空闲线程在等待新任务到来时的最长等待时间。超过这个时间,空闲线程将被终止。 4. unit(时间单位):keepAliveTime的时间单位,可以是秒、毫秒、微秒等。 5. workQueue(工作队列):用于存储等待执行的任务的阻塞队列。常见的工作队列有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等。 6. threadFactory(线程工厂):用于创建新线程的工厂类。可以自定义线程的名称、优先级等属性。 7. handler(拒绝策略):当线程池无法接受新任务时的处理策略。常见的拒绝策略有AbortPolicy(默认,抛出RejectedExecutionException异常)、CallerRunsPolicy(由调用线程执行任务)、DiscardPolicy(直接丢弃任务)和DiscardOldestPolicy(丢弃最旧的任务)。 这些参数可以根据实际需求进行调整,以达到最佳的线程池性能和资源利用率。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EmineWang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值