JAVA高并发(JUC)之ThreadPool线程池api及工作原理简析

这次讲解的是JUC的线程池,对于ThreadPool,我们可以理解为已经为我们创建了一堆线程,然后再让这些线程去做我们指定的事情。
首先先看看继承图:
在这里插入图片描述
Executors创建线程的三种方法:

ExecutorService threadPool = Executors.newFixedThreadPool(5);	//固定容量
ExecutorService threadPool = Executors.newSingleThreadExecutor(); 	//单例的、单个线程的线程池
ExecutorService threadPool = Executors.newCachedThreadPool(); 	//缓存的 即超出就自动创建线程的

接下来讲解一下这三个的区别:

首先我们看的是第一个固定容量的线程池Executors.newFixedThreadPool(5);
首先看代码:

/**
 * 主要特点:线程复用;控制最大并发数;管理线程。
 *
 * @author Cocowwy
 * @create 2020-05-05-20:20
 * Executor/ExecutorServic(Interface)
 * Executors  线程池的工具类
 */
public class MyThreadPoolDemo {
    
    
    public static void main(String[] args) {
    
    
        //一池五个受理线程
        ExecutorService threadPool = Executors.newFixedThreadPool(5);  //看源码是LinkedBlockingQueue<Runnable>()
        try {
    
    
            //模拟10个用户办理业务,但是只有5个受理窗口
            for (int i = 0; i < 10; i++) {
    
    
                threadPool.execute(() -> {
    
    
                    System.out.println(Thread.currentThread().getName() + "\t" + "办理业务");
                });
                Thread.sleep(400);
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            threadPool.shutdown(); //关闭线程池
        }
    }

}

结果如下:

接着我们加上一句线程睡眠一小会的代码。

public class MyThreadPoolDemo {
    
    
    public static void main(String[] args) {
    
    
        //一池五个受理线程
        ExecutorService threadPool = Executors.newFixedThreadPool(5);  //看源码是LinkedBlockingQueue<Runnable>()
        try {
    
    
            //模拟10个用户办理业务,但是只有5个受理窗口
            for (int i = 0; i < 10; i++) {
    
    
                threadPool.execute(() -> {
    
    
                    System.out.println(Thread.currentThread().getName() + "\t" + "办理业务");
                });
                Thread.sleep(400);
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            threadPool.shutdown(); //关闭线程池
        }
    }

}

在这里插入图片描述
在这里我们可以看到有序办理了每个业务。可以看出这个是固定了大小的线程池,每次都是从这个线程池中取的线程。

这是第二个,单例的线程池ExecutorService threadPool = Executors.newSingleThreadExecutor(); //一池1个受理线程

public class MyThreadPoolDemo {
    
    
    public static void main(String[] args) {
    
    
//        ExecutorService threadPool = Executors.newFixedThreadPool(5);  //一池五个受理线程,看源码是LinkedBlockingQueue<Runnable>()
        ExecutorService threadPool = Executors.newSingleThreadExecutor();  //一池1个受理线程

        try {
    
    
            //模拟10个用户办理业务,但是只有5个受理窗口
            for (int i = 0; i < 10; i++) {
    
    
                threadPool.execute(() -> {
    
    
              System.out.println(Thread.currentThread().getName() + "\t" + "办理业务");
                });
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            threadPool.shutdown(); //关闭线程池
        }
    }
}

在这里插入图片描述
我们可以看到一直是一个线程在受理业务。

接下来是第三个线程池ExecutorService threadPool = Executors.newCachedThreadPool(); //一池N个受理线程 可扩展的
接下来上代码:

public class MyThreadPoolDemo {
    
    
    public static void main(String[] args) {
    
    
//        ExecutorService threadPool = Executors.newFixedThreadPool(5);  //一池五个受理线程,看源码是LinkedBlockingQueue<Runnable>()
//        ExecutorService threadPool = Executors.newSingleThreadExecutor();  //一池1个受理线程
         ExecutorService threadPool = Executors.newCachedThreadPool();  //一池N个受理线程

        try {
    
    
            //模拟10个用户办理业务,但是只有5个受理窗口
            for (int i = 0; i < 10; i++) {
    
    
                threadPool.execute(() -> {
    
    
                    try {
    
    
                        Thread.sleep(400);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "\t" + "办理业务");
                });
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            threadPool.shutdown(); //关闭线程池
        }
    }

}

先看看效果:
在这里插入图片描述
在上面的代码中,我们可以发现的是,对从线程池取的线程睡了0.4s,然而却可以发现创建出了跟多的线程,所以我们可以发现当请求过多的时候会创建线程。
以上是线程池的简单api。

接下来看看线程池的源码:
首先点进newFixedThreadPool()的源码可以看到:
在这里插入图片描述
接下来点进去newSingleThreadExecutor()的源码可以看到:
在这里插入图片描述
接下来点进去newCachedThreadPool()的源码可以看到:


综上所述,返回的实际上只是一个ThreadPoolExecutor(可以看看继承图),利用构造器传入的不同的参数而已,而且我们也能发现底层是阻塞队列。
同时说明我们也可以通过ThreadPoolExecutor`来创建线程池,Executors只是一个创建线程池的工具类,实际上返回的还是ThreadPoolExecutor。

接着我们继续点ThreadPoolExecutor
在这里插入图片描述接着再点进这this,我们可以看到它有七个参数,:
在这里插入图片描述
下图是这七大参数的解释:
在这里插入图片描述
接下来结合下图理解理解上述的7大参数:
首先看看线程池的底层工作原理图:
在这里插入图片描述
看上图以及参数解析对照我们可以知道maximumPool包含corePoolmaximumPool表示最多能放的线程数,而corePool表示的就是线程的常驻数,可以理解为银行的有最多有5个受理窗口,但是常用的却只有2个,而候客区就相当于我们的阻塞队列(BlockingQueue),那当我们的阻塞队列满了之后,handle拒绝策略出来了,相当于银行门口立了块牌子,上面写着不办理后面的业务了!然后当客户都办理的差不多了,此时多出来(在corePool的基础上扩容的窗口)的窗口在经过keepAliveTime的时间后就关闭了,重新恢复到corePool个受理窗口。

接着再来总结一下线程池的工作流程:
首先线程池接收到任务,先判断核心线程数是否满了,没有满接客,满了就放到阻塞队列,如果阻塞队列没满,这些任务放在阻塞队列,如果满了,就扩容线程数到最大线程数,如果最大线程数也满了,就是我们的拒绝策略。这就是线程池四大步骤。 接客、放入队列,扩容线程,拒绝策略!
也可以看此图:
在这里插入图片描述

那我们实际开发中怎么进行配置呢?
在这里插入图片描述
为什么呢?
举个例子,回到之前讲的 newSingleThreadExecutor(); ;以及Executors.newCachedThreadPool( );创建的线程池,看看源码,

 	//这是Single的
	public static ExecutorService newSingleThreadExecutor() {
    
    
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    }
    //点进去LinkedBlockQueue
        public LinkedBlockingQueue() {
    
    
        this(Integer.MAX_VALUE);
    }

   //这是Cahed的
    public static ExecutorService newCachedThreadPool() {
    
    
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

对于newSingleThreadExecutor(); 而言LinkedBlockQueue的长度是Integer.MAX_VALUE
对于newCachedThreadPool()而言,maximumPool的值竟然为Integer.MAX_VALUE!!
两者均会导致OOM异常!

讲完了上面的参数之后我们来看看自定义线程池:

public class MyThreadPoolDemo {
    
    
    public static void main(String[] args) {
    
    
        //自定义线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
                5,
                2L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3), //不写的话默认也是Integer.MAX_VALUE
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());//默认的拒绝策略

        try {
    
    
            //模拟10个用户办理业务,但是只有5个受理窗口
            for (int i = 0; i < 9; i++) {
    
    
                threadPool.execute(() -> {
    
    
                    System.out.println(Thread.currentThread().getName() + "\t" + "办理业务");
                });
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            threadPool.shutdown(); //关闭线程池
        }
    }

threadPool 是我们自定义的线程池,连接过上面的参数的应该都知道,该线程池最大支持的并发量就应该是maximumPool+Queue的大小,即5+3=8,而超过了大小之后就会报错:java.util.concurrent.RejectedExecutionException 拒绝执行异常
在这里插入图片描述
接下来我们看看线程池的四大拒绝策略,上述是JDK默认的拒绝策略
在这里插入图片描述
接下来看看另外三种策略的运行结果,将上述代码的拒绝策略改成第二种new ThreadPoolExecutor.CallerRunsPolicy()
在这里插入图片描述
第三种new ThreadPoolExecutor.DiscardOldestPolicy()
在这里插入图片描述
不报错。
第四种new ThreadPoolExecutor.DiscardPolicy()

同样不报错。
以上策略均继承自RejectedExecutionHandler接口。
最后提一句怎么设置maximumPoolSize合理,

System.out.println(Runtime.getRuntime().availableProcessors()); //8核

一般设置为CPU核数加1。

以上就是本次线程池的讲解,如果有不合理之处,欢迎在下方留言!

猜你喜欢

转载自blog.csdn.net/Pzzzz_wwy/article/details/106432430