【并发编程:线程池】通俗易懂的入门线程池

为什么要使用线程池?

  • 我们先看下面俩个程序的执行结果(太简单了,一看就懂的,不提供源码了,直接截图了):
    使用线程的方式去执行程序.png
    使用线程池的方式去执行程序.png
  • 产生一个问题:为什么使用线程池会变的这么快呢?线程复用(第一种使用线程的方式,他创建了100001个线程,而使用线程池他只创建的俩个线程)
  • 同时又会有一个问题:使用线程池一定会提高性能么? 不是!

验证一下java提供的不同线程池的性能,来理解为什么使用线程池不一定会提高性能!

  • 验证的代码为这个代码的简化版,留一个进行验证,其他的注释了就是我验证的代码。
  • 线程的的执行速度,可以通过毫秒数去看,日志太多,图中只能看到大概,建议自己去尝试一下!
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import com.cangqiong.utils.test.threadPoll.MonkeyRejectedExecutionHandler;

public class ThreadPoolDemo {
	public static void main(String[] args) {
		// 定义各种线程池
		ExecutorService executorService1 = Executors.newCachedThreadPool();// 快
		ExecutorService executorService2 = Executors.newFixedThreadPool(10);// 慢
		ExecutorService executorService3 = Executors.newSingleThreadExecutor();// 最慢
		ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 0L, TimeUnit.MILLISECONDS,
				new LinkedBlockingQueue<Runnable>(10), new MonkeyRejectedExecutionHandler());// 自定义线程

		// 第1个线程池执行
		for (int i = 1; i <= 100; i++) {
			executorService1.execute(new MyTask(i));
		}
		
		// 第2个线程池执行
		for (int i = 1; i <= 100; i++) {
			executorService2.execute(new MyTask(i));
		}
		
		// 第3个线程池执行
		for (int i = 1; i <= 100; i++) {
			executorService3.execute(new MyTask(i));
		}
		
		// 第4个线程池执行
		for (int i = 1; i <= 100; i++) {
			threadPoolExecutor.execute(new MyTask(i));
		}
	}
}

/***
 * 业务的任务
 */
class MyTask implements Runnable {
	int i = 0;

	public MyTask(int i) {
		this.i = i;
	}

	@Override
	public void run() {
        System.out.println(Thread.currentThread().getName() + "线程正在执行第" + i + "个。当前的毫秒数为:" + System.currentTimeMillis());
		try {
			Thread.sleep(3000L);// 业务逻辑
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

Executors.newCachedThreadPool()的执行结果:执行速度非常快!

newCachedThreadPool的执行结果.png

Executors.newFixedThreadPool(10)的执行结果:速度比较慢!

newFixedThreadPool.png

Executors.newSingleThreadExecutor()的执行结果:速度非常非常非常慢!

image.png

  • 这里又有一个问题:为什么我们同样用一newSingleThreadExecutor去往数组中插入数据和执行这个3秒的任务。俩个的时间相差非常多?这要看执行的任务属于CPU密集型,还是内存的。所以使用线程池或者说使用很多线程去处理,性能不一定很高!
  • 因此在使用线程池的时候,要针对不同业务去选择合适的线程池!
  • 在实际开发中,不建议直接使用java提供的这些线程池,尽量根据业务去自定义线程池!

为什么使用这三种java提供的线程池,效果不一样呢?

  • 我们可以点开他的创建的源码,可以看到都调用了同一个方法
    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方法
  • ThreadPoolExecutor有7个参数,下面分别介绍一下。
  • corePoolSize:核心线程数,也是线程池中常驻的线程数。注意:线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务
  • maximumPoolSize:最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程。简单理解就是临时的线程。
  • keepAliveTime:非核心线程的存在时间。非核心线程的空闲时间超过这个时间,非核心线程就会被回收。如果corePoolSize = maximumPoolSize的时候,不存在非核心线程,所以也不存在回收!
  • unit:keepAliveTime的时间单位。
  • workQueue:用于保存任务的队列。当任务的数量超过了maximumPoolSize,这时候新的任务就会被存放在这个队列中。
  • threadFactory:创建线程的工厂类,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建。
  • handler:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy。简单理解就是maximumPoolSize满了放在队列中,队列也满了的时候,应该如何处理!

了解了参数是干什么的,那我们去分析这几个方法有什么区别。

  • newCachedThreadPool的核心线程为0,最大线程数为int的最大值。简单理解就是无核心线程,所有线程都是临时的,而且临时的线程上限非常高。换做生活中的例子,一个公司里面一个正式员工没有,全都是外包人员,项目来了之后就开始招收外包人员,干完活把外包人员释放掉。
  • newFixedThreadPool的corePoolSize = maximumPoolSize。简单理解就是无非核心线程,全部为核心线程。换做生活中的例子,一个公司里面都是正式员工,没有一个外包!
  • newSingleThreadExecutor的corePoolSize = maximumPoolSize = 1。简单理解就是只有一个核心线程。换做生活中的例子,一个公司只有自己一个人,单干的那种!

了解了这些的区别,那我们应该在什么时候去使用这些工具类呢?线程数量应该配置多大呢?

  • cpu密集型减少线程数:减少上下文切换。
  • io密集型增加线程数:大部分时间在等待其他的响应,上下文切换不是主要影响性能的点,可以让cpu去干其他事情。
  • 通过压测去确定这个值!

了解了几种线程池,为什么说不推荐使用呢?

  • newCachedThreadPool:他的最新线程数太大了,创建大量的线程会导致cpu需要进行大量的上下文切换,从而很容易引起cpu100%。
  • newFixedThreadPool:他的队列使用的是LinkedBlockingQueue,这是一个无界队列!任务比较多的时候,很容易引起OOM。
  • newSingleThreadExecutor:他只有一个线程,其他的任务都放在无界队列中。任务比较多的时候,很容易引起OOM。

结束语

  • 获取更多有价值的文章,让我们一起成为架构师!
  • 关注公众号,可以让你逐步对MySQL以及并发编程有更深入的理解!
  • 这个公众号,无广告!!!每日更新!!!
    作者公众号.jpg
posted @ 2022-01-12 21:34  程序java圈  阅读(55)  评论(0编辑  收藏  举报