【JAVA并发】三、JAVA线程池详解

一、为什么要使用线程池

在使用线程池之前,我们得明白,为什么要用线程池?首先我们得了解,线程的调度是由操作系统来控制的,所以他就跟数据库连接池一样,采用了池设计。

线程是一个重量级的对象,同样应该避免频繁创建和销毁。

附赠一段线程启动的源码:

 public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();

上述代码是Thread类的线程启动方法,可以看到,start()方法里面最终调用的start0(),而start0()是一个native 方法,由此可见他调用底层操作系统的方法。也就是说,线程的启动是交给操作系统去完成的。

二、如何合理的创建线程池

既然知道了要使用线程池,那么如何创建合适的线程池呢?

首先创建线程池的核心是ThreadPoolExecutor。但是由于ThreadPoolExecutor的构造函数的参数太多太复杂,JAVA API就提供了一个简单的创建线程池的方法。就是Executors,例子如下:

ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(()->{
	System.out.println("hello world");
});

Executors能快速创建各种各样的线程池,但是并不建议使用Executors来创建线程池。先看下他的源码,

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

可以看到对于maximumPoolSize参数,他给的是Integer.MAX_VALUE。意思是在高负载的条件下线程数量可以无限的扩大。这就很容易引起OOM(Out of OutOfMemory)。

所以呢,咱们还是老老实实按实际情况通过ThreadPoolExecutor去创建线程池吧。下面附带ThreadPoolExecutor构造函数的参数简介,

1、corePoolSize:表示线程池保有的最小线程数。
2、maximumPoolSize:表示线程池创建的最大线程数。
3、keepAliveTime & unit:如果一个线程空闲了keepAliveTime &
unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。
4、workQueue:工作队列,就是还需要执行的任务排队队列。
5、threadFactory:通过这个参数你可以自定义如何创建线程。
6、handler:通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过
handler 这个参数来指定。ThreadPoolExecutor 已经提供了以下 4 种策略。
6.1、CallerRunsPolicy:提交任务的线程自己去执行该任务。
6.2、AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
6.3、DiscardPolicy:直接丢弃任务,没有任何异常抛出。
6.4、DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。

三、线程数量创建多少个合适

其实对于线程池来说,最重要的还是线程个数。多了容易OOM,少了呢程序的吞吐量又上不去。那么我们如何来定这个线程的数量呢?

首先,我们要区分自己的程序是IO密集型还是计算密集型。

IO密集型

针对IO密集型,也是我们绝大多数的程序,他的最佳线程数

最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]

计算密集型

既然是计算密集型,主要的压力就放在了CPU身上,那么他的最佳线程数理论上就是CPU的核数。但是在实际情况下,线程的数量一般会设置为“CPU 核数 +1”,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率。

最佳线程数 = CPU 核数 + 1

四、JAVA线程池工作原理

说起线程池的工作原理,又不得不说数据库连接池了。

虽然线程池和连接池都是池化设计,但是他们的工作原理却不一样。这从他们的使用过程中也能看出来。对于数据库连接池来说,他返回的是池内资源,也就是一个个的Connection。但是对于线程池来说,他却是把新任务直接加入连接池的工作列表里面去。参考下列例子:

		ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5,
				60L, TimeUnit.SECONDS,
				new SynchronousQueue<Runnable>());
		threadPoolExecutor.execute(()->{
			System.out.println("hello world");
		});

甚至说,JAVA在起名的时候,为了让大家清楚这并不是一个真正的“池“,他结尾也是以Executor(执行器)来命名的。

线程池的工作原理其实就是内部维护一个工作列表(workQueue ),execute方法把新任务加入到工作列表里面来,然后线程池内部的线程会取出工作列表的任务并执行

五、Springboot自带的线程池

顺带提一下把,springboot在启动后,在单例池里面,已经有了一个连接池叫applicationTaskExecutor,当然线程池的实现还是我们熟悉的ThreadPoolExecutor,但是不建议直接用spring这个默认的线程池,原因是它跟Executors创建线程池的类似,线程池的最大数量也一样没有限制。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

糖醋排骨不拿拿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值