深入理解Java并发编程(三):线程池

线程池的作用

1、线程重用,提高系统效率:创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率;

2、控制线程并发数量:线程并发数量过多,会导致抢占系统资源从而导致阻塞;

3、对线程进行一些简单的管理。

线程池中重要的几个类

Executor:顶级接口,只有一个execute()抽象方法;

ExecutorService:继承了Executor接口,真正的线程池接口。

AbstractExecutorService:对ExecutorService的执行任务类型的方法提供了一个默认实现。

ScheduledExecutorService:和Timer/TimerTask类似,解决那些需要任务重复执行的问题。

ThreadPoolExecutor:ExecutorService的默认实现。

ScheduledThreadPoolExecutor:继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

ForkJoinPool:继承AbstractExecutorService,但又和我们常用的ThreadPoolExecutor原理不同,是另一种实现方式。顾名思义,ForkJoinPool运用了Fork/Join原理,使用“分而治之”的思想,将大任务分拆成小任务分配给多个线程执行,最后合并得到最终结果,加快运算(ForkJoinPool在jdk1.7引入,在jdk1.8进行了优化)。

注意区分Executor接口和Executors工厂类

线程池主要处理流程

常见线程池

这里常见的线程池指的是利用Executors静态工厂构建的线程池,能满足某些特定场景的使用,但是一般不推荐使用。我们在日常开发中最好通过ThreadPoolExecutor去创建线程池,一来可以更加明确线程池的运行规则,二来可以规避资源耗尽的风险。

Executors静态工厂常用方法有以下几个:

newFixedThreadPool(int corePoolSize):

创建固定数目线程的线程池。这种线程池能控制最大线程数量。超出线程数目的任务会在队列中等待,而队列的最大值为Integer.MAX_VALUE,所以当任务数量堆积过多时,可能会导致OOM。

newCachedThreadPool():

创建一个可缓存的线程池。调用execute将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有60秒未被使用的线程。这种线程池能创建的线程数量最大值为Integer.MAX_VALUE,所以当线程数量过多时,可能会导致OOM。

newSingleThreadExecutor():

创建一个单线程化的线程池。这种线程池中只有一个线程,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。但是队列的最大值为Integer.MAX_VALUE,所以当任务数量堆积过多时,可能会导致OOM。

newScheduledThreadPool(int corePoolSize):

创建一个支持定时及周期性的任务执行的固定数目的线程池,多数情况下可用来替代Timer类。这种线程池能创建的线程数量最大值为Integer.MAX_VALUE,所以当线程数量过多时,可能会导致OOM。

newSingleThreadScheduledExecutor():

创建单线程化的支持定时及周期性的任务执行的线程池。

newWorkStealingPool(int var1):

创建持有足够线程的线程池来支持给定的并行级别,并通过使用多个队列,减少竞争,它需要传一个并行级别的参数,如果不传,则被设定为默认的CPU数量。并行级别决定了同一时刻最多有多少个线程在执行。该线程池创建的线程都为守护线程。(这是jdk1.8中新增加的一种线程池实现)

通过ThreadPoolExecutor创建线程池

避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。

int corePoolSize:该线程池中核心线程数最大值,线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程。核心线程默认情况下会一直存活在线程池中,即使这个核心线程处于闲置状态。如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果被闲置的话,超过一定时间就会被销毁掉。需要注意的是在初创建线程池时线程不会立即启动,直到有任务提交才开始启动线程并逐渐时线程数目达到corePoolSize。若想一开始就创建所有核心线程需调用prestartAllCoreThreads方法。

int maximumPoolSize:该线程池中线程总数最大值,线程总数 = 核心线程数 + 非核心线程数。需要注意的是当核心线程满且阻塞队列也满时才会判断当前线程数是否小于最大线程数,并决定是否创建新线程。

long keepAliveTime:该线程池中非核心线程闲置超时时长,一个非核心线程,如果闲置的时长超过这个参数所设定的时长,就会被销毁掉,如果设置allowCoreThreadTimeOut为true,则会同时作用于核心线程。

TimeUnit unit:keepAliveTime的时间单位。

BlockingQueue workQueue:该线程池中的任务队列,维护着等待执行的Runnable对象。当所有的核心线程都在工作时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。主要有3种类型的BlockingQueue可供选择:无界队列,有界队列和同步移交。

ThreadFactory threadFactory:执行程序创建新线程时使用的工厂。

RejectedExecutionHandler handler:阻塞队列已满且线程数达到最大值时所采取的饱和策略。java默认提供了4种饱和策略的实现方式:中止、抛弃、抛弃最旧的、调用者运行。

常用的workQueue类型

无界队列

队列大小无限制,常用的为无界的LinkedBlockingQueue,使用该队列作为阻塞队列时要尤其当心,当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM(cpu和内存飙升服务器挂掉)。

有界队列

常用的有两类,一类是遵循FIFO原则的队列如ArrayBlockingQueue与有界的LinkedBlockingQueue,另一类是优先级队列如PriorityBlockingQueue。PriorityBlockingQueue中的优先级由任务的Comparator决定。 使用有界队列时队列大小需和线程池大小互相配合,线程池较小有界队列较大时可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量。

同步移交队列

如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。如果所有线程都在工作则会新建一个线程来处理这个任务。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。

饱和策略

AbortPolicy中止策略

该策略是默认饱和策略。

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
} 

使用该策略时在饱和时会抛出RejectedExecutionException(继承自RuntimeException),调用者可捕获该异常自行处理。

DiscardPolicy抛弃策略

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}

如代码所示,不做任何处理直接抛弃任务。

DiscardOldestPolicy抛弃旧任务策略

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
} 

如代码,先将阻塞队列中的头元素出队抛弃,再尝试提交任务。如果此时阻塞队列使用PriorityBlockingQueue优先级队列,将会导致优先级最高的任务被抛弃,因此不建议将该种策略配合优先级队列使用。

CallerRunsPolicy调用者运行

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
} 

既不抛弃任务也不抛出异常,直接运行任务的run方法,换言之将任务回退给调用者来直接运行。使用该策略时线程池饱和后将由调用线程池的主线程自己来执行任务,因此在执行任务的这段时间里主线程无法再提交新任务,从而使线程池中工作线程有时间将正在处理的任务处理完成。

自定义

如果以上策略都不符合业务场景,那么可以自己定义一个拒绝策略,只要实现RejectedExecutionHandler接口,并且实现rejectedExecution方法就可以了。具体的逻辑就在rejectedExecution方法里去定义就OK了。

public class MyRejectPolicy implements RejectedExecutionHandler{
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        //Sender是我的Runnable类,里面有message字段
        if (r instanceof Sender) {
            Sender sender = (Sender) r;
            //直接打印
            System.out.println(sender.getMessage());
        }
    }
}

并发下线程池的最佳数量计算

CPU密集型应用

线程池大小设置为N+1(尽量使用较小的线程池)。对于计算密集型的任务,在拥有N个处理器的系统上,当线程池的大小为N+1时,通常能实现最优的效率。即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保CPU的时钟周期不会被浪费。

IO密集型任务

可以使用稍大的线程池,一般为2*CPU核心数。IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。

混合型任务

可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。 因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

在IO优化文档中,有这样地公式: 最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目。即线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。但根据短板效应,真实的系统吞吐量并不能单纯根据CPU来计算。那要提高系统吞吐量,就需要从“系统短板”(比如网络延迟、IO)着手。

模拟使用Executors出现OOM情况

package com.trs.demo;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPools {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(10);
        TaskTest1 taskTest1 = new TaskTest1();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            newFixedThreadPool.execute(taskTest1);
        }
    }

}

/**
 * 测试线程任务类1
 * 
 * @author Admin
 *
 */
class TaskTest1 implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "TaskTest1 start!");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "TaskTest1 end!");
    }

}

通过指定JVM参数:-Xmx8m -Xms8m 运行以上代码,会抛出OOM。

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
	at java.util.concurrent.LinkedBlockingQueue.offer(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor.execute(Unknown Source)
	at com.trs.demo.ThreadPools.main(ThreadPools.java:13)

以上代码指出,ThreadPools.java的第13行,就是代码中的newFixedThreadPool.execute(taskTest1);。

在以上的代码中其实已经说了,真正的导致OOM的其实是LinkedBlockingQueue.offer方法。LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。而newFixedThreadPool中创建LinkedBlockingQueue时,并未指定容量,此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值