关于Java线程池,你需要知道的知识

目录

一、什么是线程池?

二、线程池的参数

1.corePoolSize:核心线程数

2.maximumPoolSize:最大线程数

3.keepAliveTime:最大空闲时间

4.timeUnit:keepAliveTime的时间单位

5.workQueue:任务队列

5.1 ArrayBlockingQueue  数组组成的有界阻塞队列

5.2 LinkedBlockingQueue 链表组成的有界阻塞队列 参数为Integer.MAX_VALUE

5.3 LinkedTransferQueue 链表组成的无界阻塞队列

5.4 LinkedBlockingDeque  链表组成的双向阻塞队列

5.5 PriorityBlockingQueue 支持优先级排序的无界阻塞队列

5.6 DelayQueue  优先队列实现的延迟无界阻塞队列

5.7 SynchronousQueue 不存储元素的队列 又称单元素队列

6.threadFactory:线程工厂

7.rejectedExecutionHahdler:拒绝策略

三、线程池工作原理

1.工作原理流程

2.实现线程池的方式:

四、任务提交方式

五、线程池如何关闭

六、怎么合理配置线程数量


一、什么是线程池?

        线程池是多线程处理方式(继承Thread类,实现Runnable接口,实现Callable接口(通过FutureTask构造器),线程池)中的一种,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程,每个线程池都使用默认的堆栈大小(-Xss参数,在linux、os、oracle系统中默认1024kb,windows系统中,大小取决于实际物理内存),以默认的优先级运行,并处于多线程单元中,如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果线程池中所有线程都保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程,但数目永远不会超过最大值(maximumPoolSize),超过最大值的线程可以排队,但它们要等到其他线程完成后才启动。

 线程池的作用:

         线程池的主要作用是控制线程数量,处理过程中将任务放入队列,然后在创建线程后启动这些任务,如果线程数量超过了最大数,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取得任务来执行。

主要特点:线程复用,控制最大线程数,管理线程。

1).通过复用已经创建的线程,降低线程反复创建和回收对资源的消耗。

2).提高响应速度,当任务到达时不需要等待线程创建而现有的线程可立刻执行。

3).提高线程的可管理性,线程是稀缺资源,如果无限制创建,除了消耗系统资源,还会降低系统的稳定性,通过线程池可以实现对线程的统一管理、监控和调优。

线程池框架:

 Java中线程池是通过Executor框架实现的,该框架用到了Executor,Executors,ExecutorService,AbstractExecutorService,ScheduleExecutorService,ThreadPoolExecutor,ScheduleThreadPoolExecutor.

二、线程池的参数

1.corePoolSize:核心线程数

        线程池中常驻的线程数量,创建了线程以后,当有任务请求进来,就会安排池中常驻线程去执行任务,当线程池中线程数量达到corePoolSize后,就会把任务放到缓存队列中(wordQueue)

2.maximumPoolSize:最大线程数

         线程池中能够容纳的同时执行的最大线程数,此值必须大于等于1,当前任务数量达到corePoolSize并且workQueue队列的数量也满了以后,就会创建新的线程去执行任务。

3.keepAliveTime:最大空闲时间

        当线程数量超过corePoolSize后,其中的线程空闲时间达到keepAliveTime后,多余的线程就会被销毁直到剩下的线程数量为corePoolSize为止

4.timeUnit:keepAliveTime的时间单位

        最大空闲时间的计量单位。

5.workQueue:任务队列

        工作队列,存放被提交但尚未执行的任务。

5.1 ArrayBlockingQueue  数组组成的有界阻塞队列

        此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问对列,所谓公平是指阻塞堆列线程可按照阻塞的顺序访问队列。非公平性对先等待的线程是不公平的,当队列可用时,阻塞队列的线程都可以竞争访问队列的资格。

5.2 LinkedBlockingQueue 链表组成的有界阻塞队列 参数为Integer.MAX_VALUE

        此队列按照先进先出的原则对元素进行排序。

5.3 LinkedTransferQueue 链表组成的无界阻塞队列

        相比于其他队列,LinkedTransferQueue多了tryTransfer和transfer方法

5.4 LinkedBlockingDeque  链表组成的双向阻塞队列

5.5 PriorityBlockingQueue 支持优先级排序的无界阻塞队列

        此队列逻辑上是无界的,但是资源耗尽时试图执行add也将失败,导致OOM

5.6 DelayQueue  优先队列实现的延迟无界阻塞队列

        只有在延迟期满之后才能从中提取元素。

5.7 SynchronousQueue 不存储元素的队列 又称单元素队列

        其中一个线程的插入操作必须等待另一个线程的对应移除操作,反之亦然。

线程池中最常用的是ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue

队列中常用的方法:

add()   向队列添加一个元素,如果队列已满,抛出IllegalStageException

remove()  移除队列头节点元素,如果对列为空,抛出NoSuchElementException

element()  检查队列,由元素就返回头节点,否则抛出NoSuchElementException

offset()      向队列中添加一个元素,成功返回true,失败返回false

poll()         删除队列中的头节点,成功返回元素,失败返回null

peek()       检查队列,由元素返回头节点,对列为空则返回null

put()        向队列中添加元素,如果队列满了就阻塞

take()       从队列中删除并返回头节点,如果列队中没有元素就阻塞

6.threadFactory:线程工厂

        表是线程池中生成工作线程的线程工厂,一般使用默认即可(Executors.defaultThreadFactory())。它主要是为了给线程池起一个标识,也就是为线程池起一个具有意义的名称。

也可以自己实现ThreadFactory接口:

public class MyThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        thread.setName("一个全新的线程");
        return thread;
    }
}

7.rejectedExecutionHahdler:拒绝策略

7.1 AbortPolicy 

        线程池满了直接抛出RejectedExecutionHandlerException,并中断任务执行。这是线程池默认的拒绝策略,可及时反馈程序运行的状态。如果是比较关键的业务,推荐使用这个策略,这样的话如果系统不能承受更大的并发量的时候,就可以通过异常及时发现。

7.2 DiscardPolicy

        直接抛弃当前任务,不抛异常。如果允许任务被抛弃,这是性能最好的一种拒绝策略。

7.3 DiscardOldestPolicy

        抛弃任务队列中等待最久的任务,并把当前任务添加到任务队列尝试提交。不建议使用。

7.4 CallerRunPolicy 

        不抛异常也不丢弃任务,而是尝试将任务返回给调用者,也即将任务交给main(调用executor方法线)线程执行。

如果要自定义拒绝策略,可以通过实现rejectedExecution()方法实现

public class MyExecutionHandler implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        //这里可以写自己的拒绝策略逻辑
    }
}

三、线程池工作原理

1.工作原理流程

 1).创建线程以后,等待提交过来的任务请求

 2).当调用executor()或者submit()方法添加一个请求任务时,线程池会做如下判断:

                如果正在运行的线程数量小于corePoolSize,那么马上安排线程执行这个任务。

                如果正在运行的线程数量大于等于corePoolSize,那么就将任务放到任务队列中

                如果任务队列没满就成功放入队列

                如果任务队列满了并且正在运行的线程数小于maximumPoolSize,就创建非核心线程立刻执行这个任务(这里创建的线程不是首先去执行队列中的任务)。

                如果队列满了且正在运行的线程数量大于等于maximumPoolSize,那么线程池就会启动饱和拒绝策略;

 3).当一个线程执行完当前任务后,它会从队列中取下一个任务来执行。

 4).当一个线程无事可做,空闲时间超过keepAliveTime,线程池就会判断:

                如果当前线程池中线程数量大于corePoolSize,那么这个线程就会被回收

                直到线程池中线程数量收缩到corePoolSize。

2.实现线程池的方式:

1).通过Executors工具类创建:

带有时间调度的线程池 (每秒执行等)

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);

指定线程数的线程池 适用于处理长期任务

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);

一池一线程  适用于一个任务接一个任务的场景

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

一池多线程,带缓存 适用于处理量较大的短期任务

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

使用目前机器上可用的处理器作为它的并行级别。 Java8之后的

ExecutorService workStealingPool = Executors.newWorkStealingPool();

上述的前四种线程池创建,最终构造方法都是用了ThreadPoolExecutor类的同一个构造器,只不过是所传参数的区别,构造器源码如下

package java.util.concurrent;
public class ThreadPoolExecutor extends AbstractExecutorService {
    //略...
        public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
}

但是,在在阿里巴巴Java开发手册中,不建议直接使用Executors去创建:

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutors的方式,
这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors返回的线程池对象的弊端如下:

(1).Executors.newFixedThreadPool()和Executors.newSIngleThreadExecutor()方法在调用构造器的时候传入的workQueue参数为new LinkedBlockingQueue<Runnable>(),此队列长度为Integer.MAX_VALUE,源码如下:

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

这样做可能导致大量堆积请求到队列,造成OOM。

(2).Executors.newScheduledThreadPool()和Executors.newCachedThreadPool方法在调用构造器的时候,传入的参数maximumPoolSize为Integer.MAX_VALUE,源码如下:

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

这样做可能导致大量线程被创建,造成OOM。

2).推荐的线程池实现方式的自己手写线程池,通过newThreadPoolExecutor实现:

         ExecutorService threadPool = new ThreadPoolExecutor(2,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(4),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardPolicy());

手写线程池时,根据实际业务合理配置线程池参数,是最稳妥的做法。

四、任务提交方式

1).executor方法

void execute(Runnable command);
executorService.execute(() -> {
		System.out.println("ThreadPoolDemo.execute");
	});

2).submit()方法

Future<?> future = threadPool.submit(() -> {
       System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
       Object o = future.get();

总结execute()方法和submit()方法的区别:

(1).execute()只能提交Runnalbe类型的任务,无返回值。

(2).submit()既可以提交Runnable类型的任务也可以提交Callable类型的任务,会有一个类型为Future的返回值,如果提交的是Runnable类型的任务,future.get()得到的是null。

(3).execute()提交的任务在执行中遇到异常会直接抛出,而submit()不直接抛出,只有在Future的get方法获取返回值时,才会获得异常抛出。

(4).future在的get方法在为获得返回值之前会一直阻塞,我们可以在while循环中使用Future的isDone方法来判断任务是否执行完,从而去控制get方法,使得submit方法可以正确使用。

 while(true) {
     /idDone:如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。
            if(future.isDone()) {
                System.out.println("任务执行完成:" + future.get());
                break;
            }
        }
        es.shutdown();

五、线程池如何关闭

        如何优雅的关闭线程池是一个头疼的问题,开启线程是简单的,但想要停止却不是那么容易。通常大部分程序都是使用jdk提供的俩个方法来关闭线程池,分别是shutdown和shutdownNow;

通过调用线程池的shutdown()或shutdownNow()方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt()方法来中断线程(中断仅仅是给线程打上一个标记,并不代表这个线程真正停止了,如果线程不去响应中断,那么这个标记将毫无作用),所以无法响应中断的线程可能永远无法停止。

但是它们存在一定区别,shutdownNow首先将线程池状态设置为STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将只是将线程池的状态设置成shutdown状态,然后中断所有没有正在执行任务的线程。

        只要调用了这俩个关闭方法中的任意一个,isShutdown方法就会返回true,当所有的任务都已关闭后,才表示线程池关闭成功,这时候调用isTerminaed方法会返回true,至于应该调用哪一种方法来关闭线程池,应该交由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

六、怎么合理配置线程数量

 在实际开发中,要根据任务的性质来决定我们创建的核心线程数的大小,可以从以下角度分析:

·任务的性质:CPU密集型、IO密集型和混合型

·任务优先级:高、中、低

·任务执行时间:长、中、短

·任务依赖性:是否依赖于系统其它资源,例如:数据库连接

对于CPU密集的任务,由于任务需要大量的运算而没有阻塞(没有多线程IO操作就不会阻塞),CPU一直在高速运算,一般配置CPU核数+1的线程池数量。多出来的一个线程是作为其他线程出现问题的备用线程。

但是对于单核CPU来说,其运算能力总共就那么多,设置多的线程数也不可能使程序的到加速。

由此可见,在多核CPU环境下,设置合理的线程数量其实本质就是合理的运用了CPU的空闲时间,以此来提高了效率。

IO密集型再多线程环境,由于存在锁的竞争以及阻塞,所以线程会有等待并不是一直在执行任务,因此就尽可能多的配置线程数,通常的公式是CPU核数*2

还有一种情况是IO密集型会有大量的IO以及阻塞,导致大部分线程阻塞,这时候为了更充分利用阻塞的时间(而不是因为阻塞,这段时间被浪费),就设置更多的线程数量,这时可以参考公式:

cpu核数/(1-阻塞系数)

阻塞系数一般在0.8~0.9之间

例如:CPU核数为8,阻塞系数为0.9。那么设置的线程数为:  8/(1-0.9) = 80    

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

码海无涯回头是岸

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

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

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

打赏作者

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

抵扣说明:

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

余额充值