并发源码-线程池使用

1、简介

简单来说使用线程池有以下几个目的:

  • 线程是稀缺资源,不能频繁的创建。
  • 解耦作用;线程的创建于执行完全分开,方便维护。
  • 应当将其放入一个池子中,可以给其他任务进行复用。

2、执行线程

  1. 主线程首先要创建实现 Runnable 或者 Callable 接口的任务对象。
  2. 把创建完成的实现 Runnable / Callable 接口的 对象直接交给 ExecutorService 执行ExecutorService.execute(Runnable command))或者也可以把 Runnable 对象或Callable 对象提交给 ExecutorService 执行(ExecutorService.submit(Runnable task)ExecutorService.submit(Callable task))。
  3. 如果执行 ExecutorService.submit() , ExecutorService 将返回一个实现 Future
    接口的对象(我们刚刚也提到过了执行 execute() 方法和 submit() 方法的区别, submit() 会返回一个 FutureTask 对象)。由于FutureTask 实现了 Runnable ,我们也可以创建 FutureTask ,然后直接交给 ExecutorService 执行。
  4. 最后,主线程可以执行 FutureTask.get() 方法来等待任务执行完成。主线程也可以
    执行 FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。

2.1 线程创建方式

线程池执行任务需要实现的 Runnable 接口 或 Callable 接口。 Runnable 接口或 Callable 接口实现类都可以被 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行。

Runnable 自 Java 1.0 以来一直存在,但 Callable 仅在 Java 1.5 中引入,目的就是为了来处理Runnable 不支持的用例。 Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。

Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以。所以,
如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。

2.2 线程提交

execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行
成功与否;

submit() 方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对
象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 g
et() 方法来获取返回值, get() 方法会阻塞当前线程直到任务完成,而使用 get(long t
imeout,TimeUnit unit) 方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务
没有执行完。

Future 接口以及 Future 接口的实现类 FutureTask 类都可以代表异步计算的结果。
当我们把 Runnable 接口 或 Callable 接口 的实现类提交给 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行。(调用 submit() 方法时会返回一个 FutureTask 对象)

在这里插入图片描述

2.3 关闭线程池

两个方法 shutdown()/shutdownNow()

但他们有着重要的区别:

  • shutdown() 执行后停止接受新任务,会把队列的任务执行完毕。
  • shutdownNow() 也是停止接受新任务,但会中断所有的任务,将线程池状态变为 stop。

两个方法都会中断线程,用户可自行判断是否需要响应中断。

shutdownNow() 要更简单粗暴,可以根据实际场景选择不同的方法。
我通常是按照以下方式关闭线程池的:

	  long start = System.currentTimeMillis();
        for (int i = 0; i <= 5; i++) {
            pool.execute(new Job());
        }

        pool.shutdown();

        while (!pool.awaitTermination(1, TimeUnit.SECONDS)) {
            LOGGER.info("线程还在执行。。。");
        }
        long end = System.currentTimeMillis();
        LOGGER.info("一共处理了【{}】", (end - start));

pool.awaitTermination(1, TimeUnit.SECONDS) 会每隔一秒钟检查一次是否执行完毕(状态为 TERMINATED),当从 while 循环退出时就表明线程池已经完全终止了。

2.4 异常处理

在 FutureTask 对象的 run() 方法中,该任务抛出的异常被捕获,然后在setException(ex); 方法中,抛出的异常会被放到 outcome 对象中,这个对象就是 submit() 方法会返回的 FutureTask 对象执行 get() 方法得到的结果。
但是在线程池中,并没有获取执行子线程的结果,所以异常也就没有被抛出来,即被“吞掉”了。

解决:
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("judge-pool-%d")
                .setUncaughtExceptionHandler((thread, throwable)-> logger.error("ThreadPool {} got exception", thread,throwable))
                .build();
                

使用有界队列时,需要注意线程池满了后,被拒绝的任务如何处理。
使用无界队列时,需要注意如果任务的提交速度大于线程池的处理速度,可能会导致内存溢出。

3、最佳实践

3.1 创建线程池

使用有界队列,控制线程创建数量

  • Executors 返回线程池对象的弊端如下:
    FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,
    可能堆积大量的请求,从而导致OOM。
    CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,
    可能会创建大量线程,从而导致OOM。

  • 使用 ThreadPoolExecutor 的构造函数声明线程池
    Executors 返回线程池对象的弊端如下:
    FixedThreadPool 和 SingleThreadExecutor :允许请求的队列长度为 Integer.MAX_VALUE,
    可能堆积大量的请求,从而导致 OOM。
    CachedThreadPool 和 ScheduledThreadPool :允许创建的线程数量为 Integer.MAX_VALUE ,
    可能会创建大量线程,从而导致 OOM。

  • 自定义线程池

public class ThreadPoolExecutorDemo {

    private static AtomicInteger size = new AtomicInteger(1);

    private static final int CORE_POOl_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final long KEEP_ALIVE_TIME = 1L;

    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOl_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "name:  " + size.getAndIncrement());
                    }
                },
                new ThreadPoolExecutor.CallerRunsPolicy());
		
		// execute
        for (int i = 0; i < 10; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
        
        // submit
        List<Future<String>> futureList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Future<String> future = executor.submit(() -> {
                Thread.sleep(1000);
                return Thread.currentThread().getName();
            });
            futureList.add(future);
        }
        for (Future<String> fut : futureList) {
            try {
                System.out.println(new Date() + "::" + fut.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        
        // 关闭线程池
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println("Finished all threads");
    }
}

3.2 线程池的大小配置

实际使用中需要根据自己机器的性能、业务场景来手动配置线程池的参数比如核心线程数、使用的任务队列、饱和策略等等。如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队 列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM 这样很明显是有问题的!CPU 根本没有得到充分利用。但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。

要想合理的配置线程池大小,首先我们需要区分任务是计算密集型还是I/O密集型。

  • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

在我们日常的开发中,我们的任务几乎是离不开I/O的,常见的网络I/O(RPC调用)、磁盘I/O(数据库操作),并且I/O的等待时间通常会占整个任务处理时间的很大一部分,在这种情况下,开启更多的线程可以让 CPU 得到更充分的使用,一个较合理的计算公式如下:

线程数 = CPU数 * CPU利用率 * (任务等待时间 / 任务计算时间 + 1)

例如我们有个定时任务,部署在4核的服务器上,该任务有100ms在计算,900ms在I/O等待,则线程数约为:4 * 1 * (1 + 900 / 100) = 40个。

3.3 线程池命名

我们应该显示地给我们的线程池命名,这样有助于我们定位问题。

  • 利用 guava 的 ThreadFactoryBuilder
	ThreadFactory threadFactory = new ThreadFactoryBuilder()
                        .setNameFormat(threadNamePrefix + "-%d")
                        .setDaemon(true).build();
		ExecutorService threadPool = new ThreadPoolExecutor(
		corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, 
		workQueue, threadFactory)
  • 自己实现 ThreadFactor
	public final class NamingThreadFactory implements ThreadFactory {
            private final AtomicInteger threadNum = new AtomicInteger();
            private final ThreadFactory delegate;
            private final String name;
            public NamingThreadFactory(ThreadFactory delegate, String name) {
                this.delegate = delegate;
                this.name = name; // TODO consider uniquifying this
            }
            @Override
            public Thread newThread(Runnable r) {
                Thread t = delegate.newThread(r);
                t.setName(name + " [#" + threadNum.incrementAndGet() + "]");
                return t;
            }
        }public class UserThreadFactory implements ThreadFactory {

    private final String namePrefix;
    private final AtomicInteger nextId = new AtomicInteger(1);

    public  UserThreadFactory(String whatFeaturOfGroup) {
        namePrefix = "From UserThreadFactory's " + whatFeaturOfGroup + "-Worker-";
    }

    @Override
    public Thread newThread(Runnable task) {
        String name = namePrefix + nextId.getAndIncrement();
        Thread thread = new Thread(null, task, name, 0);
        System.out.println(thread.getName());
        return thread;
    }
}

3.4 线程池监测

ThreadPoolExecutor提供了获取线程池当前的线程数和活跃线程数、已经执行完成的任务数、正在排队中的任务数等等。

3.5 线程池隔离

很多业务都依赖于同一个线程池,当其中一个业务因为各种不可控的原因消耗了所有的线程,导致线程池全部占满。这样其他的业务也就不能正常运转了,这对系统的打击是巨大的。比如我们 Tomcat 接受请求的线程池,假设其中一些响应特别慢,线程资源得不到回收释放;线程池慢慢被占满,最坏的情况就是整个应用都不能提供服务。所以我们需要将线程池进行隔离。

因此,需要将线程池进行隔离。通常的做法是按照业务进行划分:比如下单的任务用一个线程池,获取数据的任务用另一个线程池。这样即使其中一个出现问题把线程池耗尽,那也不会影响其他的任务运行。

  • Hystrix 线程池隔离
    Hystrix 是一款开源的容错插件,具有依赖隔离、系统容错降级等功能。
	// 订单服务
	public class CommandOrder extends HystrixCommand<String> {

    private final static Logger LOGGER = LoggerFactory.getLogger(CommandOrder.class);

    private String orderName;

    public CommandOrder(String orderName) {


        super(Setter.withGroupKey(
                //服务分组
                HystrixCommandGroupKey.Factory.asKey("OrderGroup"))
                //线程分组
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("OrderPool"))

                //线程池配置
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
                        .withCoreSize(10)
                        .withKeepAliveTimeMinutes(5)
                        .withMaxQueueSize(10)
                        .withQueueSizeRejectionThreshold(10000))

                .andCommandPropertiesDefaults(
                        HystrixCommandProperties.Setter()
                                .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD))
        )
        ;
        this.orderName = orderName;
    }


    @Override
    public String run() throws Exception {

        LOGGER.info("orderName=[{}]", orderName);

        TimeUnit.MILLISECONDS.sleep(100);
        return "OrderName=" + orderName;
    }


}
	// 用户服务
	public class CommandUser extends HystrixCommand<String> {

    private final static Logger LOGGER = LoggerFactory.getLogger(CommandUser.class);

    private String userName;

    public CommandUser(String userName) {


        super(Setter.withGroupKey(
                //服务分组
                HystrixCommandGroupKey.Factory.asKey("UserGroup"))
                //线程分组
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("UserPool"))

                //线程池配置
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
                        .withCoreSize(10)
                        .withKeepAliveTimeMinutes(5)
                        .withMaxQueueSize(10)
                        .withQueueSizeRejectionThreshold(10000))

                //线程池隔离
                .andCommandPropertiesDefaults(
                        HystrixCommandProperties.Setter()
                                .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD))
        )
        ;
        this.userName = userName;
    }


    @Override
    public String run() throws Exception {

        LOGGER.info("userName=[{}]", userName);

        TimeUnit.MILLISECONDS.sleep(100);
        return "userName=" + userName;
    }


}

可以看到两个任务分成了两个线程池运行,他们之间互不干扰。

获取任务任务结果支持同步阻塞和异步非阻塞方式,可自行选择。

它的实现原理其实容易猜到:

利用一个 Map 来存放不同业务对应的线程池。

	   public static void main(String[] args) throws Exception {
        CommandOrder commandPhone = new CommandOrder("手机");
        CommandOrder command = new CommandOrder("电视");


        //阻塞方式执行
        String execute = commandPhone.execute();
        LOGGER.info("execute=[{}]", execute);

        //异步非阻塞方式
        Future<String> queue = command.queue();
        String value = queue.get(200, TimeUnit.MILLISECONDS);
        LOGGER.info("value=[{}]", value);


        CommandUser commandUser = new CommandUser("张三");
        String name = commandUser.execute();
        LOGGER.info("name=[{}]", name);
    }

3.6、整合Springboot

	@Configuration
	public class TreadPoolConfig {
		// 注册自定义线程池到容器
	    @Bean(value = "consumerQueueThreadPool")
	    public ExecutorService buildConsumerQueueThreadPool(){
	        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
	                .setNameFormat("consumer-queue-thread-%d").build();
	
	        ExecutorService pool = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
	                new ArrayBlockingQueue<Runnable>(5),namedThreadFactory,new ThreadPoolExecutor.AbortPolicy());
	
	        return pool ;
	    }
	}
	
	// 使用
	@Resource(name = "consumerQueueThreadPool")
    private ExecutorService consumerQueueThreadPool;
    
    @Override
    public void execute() {
        //消费队列
        for (int i = 0; i < 5; i++) {
            consumerQueueThreadPool.execute(new ConsumerQueueThread());
        }

    }

Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列

3.7、动态线程池实现

在运行期线程池使用方调用此方法设置corePoolSize之后,线程池会直接覆盖原来的corePoolSize值,
并且基于当前值和原始值的比较结果采取不同的处理策略。

对于当前值小于当前工作线程数的情况,说明有多余的worker线程,此时会向当前idle的worker线程
发起中断请求以实现回收,多余的worker在下次idel的时候也会被回收;

对于当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的worker线程来执行队列任务。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值