Java多线程——线程池

由浅入深理解线程池

第一章:创建线程的几种方式

继承Thread类

缺点:OOP单继承的局限性

class MyThread1 extends Thread{
	@Override
	public void run() {
		System.out.println("Mythread1");
	}
}

实现 Runnable 接口

class Mythread2 implements Runnable{

	@Override
	public void run() {
		System.out.println("Mythread2");
	}
}

解决了OOP单继承的局限性
避免了OOP单继承的局限性,灵活方便,方便同一个对象被多个线程使用

实现Callable接口

Runnable接口中run方法没有返回值,并且不可以抛出异常,使用Callable接口中的call方法可以有返回值,并且可以抛出异常。

class Mythread3 implements Callable<Integer>{

	@Override
	public Integer call() throws Exception {
		// TODO Auto-generated method stub
		System.out.println(Thread.currentThread().getName()+"启动了");
		return 1024;
	}
	
}

上面三种方式的测试

public class CreateThread {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		new MyThread1().start();
		new Thread(new Mythread2()).start();
		
		FutureTask<Integer> futureTask=new FutureTask<>(new Mythread3());
		Thread t1=new Thread(futureTask);
		t1.start();
		int result01=100;
		int result02=futureTask.get();//建议放到最后,要求获得Callable线程的计算结果,如果没有计算完成,就去强求,会导致阻塞直到计算完成
		System.out.println("****result*****"+(result01+result02));
		
	}
}

使用线程池

1、什么是线程池?
线程池: 简单理解,它就是一个管理线程的池子。
2、为什么要使用线程池

  • 它帮我们管理线程(解耦),避免增加创建线程和销毁线程的资源损耗。因为线程其实也是一个对象,创建一个对象,需要经过类加载过程,销毁一个对象,需要走GC垃圾回收流程,都是需要资源开销的。
  • 提高响应速度。 如果任务到达了,相对于从线程池拿线程,重新去创建一条线程执行,速度肯定慢很多。
  • 重复利用。线程用完,再放回池子,可以达到重复利用的效果,节省资源。

3、怎么用?
创建线程池的API

  1. Executors.newCachedThreadPool():无限线程池(一池多线程)——执行短期异步的小程序或者负载较轻的服务器。
  2. Executors.newFixedThreadPool(nThreads):创建固定大小的线程池(一池固定数目线程)——适用场景:执行长期任务,性能好很多。
  3. Executors.newSingleThreadExecutor():创建单个线程的线程池(一池一线程)——适用于一个任务一个任务执行的场景。
public class MythreadPoolDemo {
	 public static void main(String[] args){
	               //ExecutorService threadPool = Executors.newFixedThreadPool(5);//一池5个处理线程
	        ExecutorService threadPool = Executors.newFixedThreadPool(1);//一池1个线程
	       // ExecutorService threadPool = Executors.newCachedThreadPool();//一池N个线程

	        //模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程
	        try{
	            for(int i=1;i<=10;i++){
	                threadPool.execute(()->{
	                    System.out.println(Thread.currentThread().getName()+"\t 办理业务");
	                });
	            }
	        }catch (Exception e){
	            e.printStackTrace();
	        }finally {
	            threadPool.shutdown();
	        }
	    }
}

注意: 在实际的开发过程中,以上三个方法都不使用,而是直接通过new ThreadPoolExecutor()来创建。因为以上三种方法都会导致OOM,这一点在第三章会具体介绍。

 public static void main(String[] args){
	        ExecutorService threadPool = new ThreadPoolExecutor(2,
	                5,
	                1L,
	                TimeUnit.SECONDS,
	                new LinkedBlockingQueue<>(3),
	                Executors.defaultThreadFactory(),
	                new ThreadPoolExecutor.DiscardPolicy());

	        try{
	            for(int i=1;i<=11;i++){
	                threadPool.execute(()->{
	                    System.out.println(Thread.currentThread().getName()+"\t 办理业务");
	                });
	            }
	        }catch (Exception e){
	            e.printStackTrace();
	        }finally {
	            threadPool.shutdown();
	        }
		 
		// threadPoolInit();
	    }

第二章:线程池底层剖析

Excutors提供的三种创建线程池的方法底层都调用了ThreadPoolExecutor()方法:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));

ThreadPoolExecutor的API如下

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

下面我们来剖析每个参数的含义:

  • corePoolSize: 线程池核心线程数最大值
  • maximumPoolSize: 线程池最大线程数大小
  • keepAliveTime: 线程池中非核心线程空闲的存活时间大小
  • unit: 线程空闲存活时间单位
  • workQueue: 存放任务的阻塞队列
  • threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。
  • handler: 线城池的饱和策略事件,主要有四种类型。

对比Executors提供的三种方法底层参数的值我们可以发现,newCachedThreadPool,底层corePoolSize为0,maximumPoolsize为Integer.MAX_VALUE ,表示初始时线程池中没有线程,然后会根据需要不断增加线程数,线程池最大线程数为Integer.MAX_VALUE,这会导致OOM。线程存活时间keepAlivaTime为60L,表示线程超过这个时间之后,非核心线程会被销毁,知道减至核心线程数。存放任务的阻塞队列为SynchronousQueue,关于阻塞队列的知识,后期我们会进行详细的总结。
类比newCachedThreadPool我们就可以理解newFixedThreadPool和newSingleThreadExecutor的参数的含义了,它们核心线程数和最大线程数均为固定的一个值,线程池创建好之后就创建这些线程,并等待被使用。
下面贴出美团技术博客中的图,大佬就是大佬,描述的清晰明了:
在这里插入图片描述

第三章:正确的使用线程池

使用Executors创建线程池会带来的问题

我们首先来看看阿里开发手册中的规定:
【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,
这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
  这两个方法底层使用的是无界队列LinkedBlockingQueue,堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
  主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

除此之外拒绝策略 hanlder设置不当也会导致任务提交时出现异常。

如何正确的使用线程池

对于如何正确的使用Java线程池,这里参考了Java线程池详解,主要分为以下几部分

避免使用无界队列

不要使用 Executors.newXXXThreadPool() 快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免OOM,我们应该使用ThreadPoolExecutor的构造方法手动指定队列的最大长度:

ExecutorService executorService = new ThreadPoolExecutor(2, 2, 
				0, TimeUnit.SECONDS, 
				new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOM
				new ThreadPoolExecutor.DiscardPolicy());
明确拒绝任务时的行为

任务队列总有占满的时候,这时再 submit() 提交新的任务会怎么样呢? RejectedExecutionHandler 接口为我们提供了控制方式,接口定义如下:

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
获取处理结果和异常

如何设置线程池的参数

CPU密集型与IO密集型

CPU密集的意思是该任务需要大量的运算(计算密集型),而没有阻塞,CPU一直全速运行。CPU密集型任务配置尽可能少的线程数量:一般公式为:CPU核数+1个线程的线程池。

需要注意的是CPU密集任务只有在真正的多核CPU上才能全速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的计算能力就这些了。

由于IO密集型任务线程并不是一直执行任务,则应配置尽可能多的线程,如CPU核数*2
IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数/1-阻塞系数 阻塞系数在0.8-0.9之间。

动态设置参数

动态设置参数这块儿,暂时还没有深究,需要写完demo在回来完善,有兴趣可以先看看文末两个参考文献。

线程池在项目中的应用

在美团的技术文章中提到了两个应用场景:
场景一:快速响应用户请求
描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。。
分析:从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合通常非常复杂,伴随着调用与调用之间的级联、多级级联等情况,业务开发同学往往会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。
在这里插入图片描述
场景二:快速处理批量任务
描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。

分析:这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。
在这里插入图片描述

参考资料:

Java线程池实现原理及其在美团业务中的实践
如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。
Bilibili——周阳JUC视频

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值