up:: 线程池构造函数

说明:

(1) 本篇博客的主要内容:

● 是演示JDK给我们提供的4种自动创建线程的方式;可以得出【不推荐采用自动创建线程的方式,去创建线程】的结论;

● 自己根据具体需求,创建线程池时,线程数量的确定原则;


一:线程池应该手动创建还是自动创建?:应该手动创建;

手动创建更好,因为这样可以更加明确线程池的运行规则,避免资源耗尽的风险;

自动创建线程,会带来一些问题;


二:我们可以直接调用JDK封装好的构造方法,去自动创建线程池;但这可能会带来一些问题;

1.自动创建线程池的策略一:Executors.newFixedThreadPool();

 
     package threadPool;
 
     import java.util.concurrent.ExecutorService;
     import java.util.concurrent.Executors;
 
     /**
      * 演示JDK提供的自动创建线程池的方法:newFixedThreadPool();
      */
     public class FixedThreadPoolTest {
         public static void main(String[] args) {
             ExecutorService executorService = Executors.newFixedThreadPool(4);
 
             for (int i = 0; i <100 ; i++) {
                 executorService.execute(new TaskTest());
             }
         }
     }
 
     /**
      * 任务;
      * 这个任务,仅仅用于演示;
      */
     class TaskTest implements Runnable {
         @Override
         public void run() {
             try {
                 Thread.sleep(500);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName());
         }
     }
 

说明:

(1) 程序内容说明;(这部分不是重点啦)

(2) newFixedThreadPool(int nThreads)原理分析;

● 这种线程池的特点:核心线程数和最大线程数相同,都设置为我们传递参数大小;队列使用的是无界队列;

● 那么很显然,对于这种线程池;因为队列是无界队列,所以,如果任务很多处理不完的时候,任务就可能会堆积在队列中,从而产生OOM错误;

● 这种线程池为什么要使用无界队列?:因为,无界队列可以满足这种线程池功能;因为,这种线程池,最大线程数和核心线程数相同,那么当任务很多的时候,我们不得不使用无界队列去存储新来的任务;

(3) 演示OOM内存溢出异常;

 
     package threadPool;
 
     import java.util.concurrent.ExecutorService;
     import java.util.concurrent.Executors;
 
     /**
      * 演示JDK提供的自动创建线程池的方法:newFixedThreadPool():产生OOM错误的情况;
      */
     public class FixedThreadPoolOOM {
         //为了能更对的把任务弄到队列中,更好的能看到OOM的效果,我们这个线程池的线程数设为了1
         private static ExecutorService executorService = Executors.newFixedThreadPool(1);
 
         public static void main(String[] args) {
             for (int i = 0; i <Integer.MAX_VALUE ; i++) {
                 executorService.execute(new TaskTest1());
             }
         }
 
     }
 
     class TaskTest1 implements Runnable{
         @Override
         public void run() {
             try {
                 Thread.sleep(1000000000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
 
         }
     }

● 为了能够更好的让任务到队列中去排队,以更快的能看到内存溢出,我们把线程池的线程数设为了1;

● 每个任务,我们都让其休眠很长时间,以模拟每个任务的执行都要占用很长时间;从而,能够更快的看到内存溢出;

● 我们执行的任务次数很大;这样能更好的消耗内存,从而能更快的看到OOM异常;

● 自己的PC机器的异常还是挺大的,所以为了能更快的消耗内存;我们这儿设置一下程序的可用异常;

● 运行程序,会报OOM异常;

2.自动创建线程池的策略二:Executors.newSingleThreadExecutor();

 
     package threadPool;
 
     import java.util.concurrent.ExecutorService;
     import java.util.concurrent.Executors;
 
     public class SingleThreadExecutor {
         private static ExecutorService executorService = Executors.newSingleThreadExecutor();
 
         public static void main(String[] args) {
             for (int i = 0; i < 100; i++) {
                 executorService.execute(new TaskTest2());
             }
         }
     }
 
     /**
      * 任务;这个任务,仅仅用于演示;
      */
     class TaskTest2 implements Runnable {
         @Override
         public void run() {
             try {
                 Thread.sleep(500);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName());
         }
     }

说明:

(1) newSingleThreadExecutor()内容说明;

● 这种线程池的特点:核心线程数和最大线程数相同,都设置为了1;队列使用的是无界队列;

● 那么很显然,对于这种线程池;因为队列是无界队列,所以,如果任务很多处理不完的时候,任务就可能会堆积在队列中,占用大量的内存,就可能会产生OOM错误;

● 这种线程池为什么要使用无界队列?:因为,无界队列可以满足这种线程池功能;因为,这种线程池,最大线程数和核心线程数都是1,那么当任务很多的时候,我们不得不使用无界队列去存储新来的任务;

● newSingleThreadExecutor();这种线程池在实际中,使用的不多;

(2) 运行结果;

3.自动创建线程池的策略三:Executors.newCachedThreadPool();

这种方式创建的线程池可以缓存;这种线程池,会自动回收多余的线程;

 
     package threadPool;
 
 
     import java.util.concurrent.ExecutorService;
     import java.util.concurrent.Executors;
 
     public class CachedThreadPool {
         public static void main(String[] args) {
             ExecutorService executorService = Executors.newCachedThreadPool();
             for (int i = 0; i <1000 ; i++) {
                 executorService.execute(new TaskTest3());
             }
         }
     }
 
     /**
      * 任务;这个任务,仅仅用于演示;
      */
     class TaskTest3 implements Runnable {
         @Override
         public void run() {
             try {
                 Thread.sleep(500);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName());
         }
     }
 

说明:

(1) newCachedThreadPool()内容说明;

● 这种方式创建的线程池可以缓存;这种线程池,会自动回收多余的线程;

● 那么,因为newCachedThreadPool()创建线程时,没有限制最大线程数;那么,如果任务很多的话,其就会创建很多的线程,这就可能会导致OOM异常;

● 又因为,其核心线程数为0;所以如果长期没有任务的话,其所有的线程都会被销毁;

● 因为,这种线程池,我们没有限制最大线程数,所以,我们可以不需要队列来存储任务;;;;所以,这种线程池的队列,可以使用【new SynchronousQueue<Runnable()】这种没有容量的直接交接队列;

(2) 运行结果;

4.自动创建线程池的策略四:Executors.newScheduledThreadPool();

这种方式创建的线程池支持定时及周期性的去执行任务;在实际开发中,遇到定时任务的时候,就可以使用这种线程池;

 
     package threadPool;
 
     import java.util.concurrent.ExecutorService;
     import java.util.concurrent.Executors;
     import java.util.concurrent.ScheduledExecutorService;
     import java.util.concurrent.TimeUnit;
 
     public class ScheduledThreadPool {
         public static void main(String[] args) {
             ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
             //用法一:延时执行任务;比如,这儿我们设置:5秒后,再执行这个任务;
             scheduledExecutorService.schedule(new TaskTest4(), 5, TimeUnit.SECONDS);
             //用法二:以一定频率重复运行任务;比如,这儿我们设置:第一是在1秒后执行这个任务,然后每隔3秒中,再执行一下这个任务;
             scheduledExecutorService.scheduleAtFixedRate(new TaskTest4(), 1, 3, TimeUnit.SECONDS);
             //……
         }
     }
 
     /**
      * 任务;这个任务,仅仅用于演示;
      */
     class TaskTest4 implements Runnable {
         @Override
         public void run() {
             try {
                 Thread.sleep(500);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName());
         }
     }

说明:

(1) newScheduledThreadPool()内容说明;

● 这种方式创建的线程池支持定时及周期性的去执行任务;在实际开发中,遇到定时任务的时候,就可以使用这种线程池;

● 这种线程池,核心线程数设为我们传递值,最大线程数没有限制;

● 这种线程池,使用的是DelayedWorkQueue延迟队列;延迟队列的能力,就是可以根据我们的设置的时间先后,把队列中的任务作延迟;这是符合newScheduledThreadPool()这种线程池的的需求的;

(2) 运行结果;

5.四种线程池的总结;


三:所以,比较合适的做法是:根据自己的业务场景去设置线程池参数,手动创建线程池;

1. 根据具体的业务场景,去设置线程池的参数;

● 比如,自身硬件设备的内存有多大;想给线程取什么名字(如果我们在创建线程时,想给线程取一个自己的名字;那么就需要利用自己的线程工厂);

● 还比如,任务被拒绝时,该如何记录日志;

● 再比如,根据我们业务的并发量,来决定线程池的线程数量;

2.线程池的线程数量,设为多少?

(1) 如果任务是CPU密集型(大量的计算的任务;比如加密,计算hash等;执行这些任务时,CPU会是满负荷的;)的,那么线程池的线程数量应该设为CPU核心数的1-2倍;(比如,我们的CPU是8核,那么我们线程池的核心线程数就可以设为8-16之间)

(2) 如果任务是耗时IO型(读写数据库、文件,网络通信等;一旦涉及到网络通信时,一般CPU是不工作的;或者读写文件时,外设的速度是低于CPU的速度的,所以CPU很可能会空闲)的,那么线程池的线程数量可以设为CPU核心数的很多倍;(比如,我们的CPU是8核,那么我们线程池的核心线程数就可以设为80;;;;虽然有80个线程,但是实际执行的时候,因为读写速度或者网络速度较慢,所以会有很多线程在等待,即会有很多线程是没有占用CPU的,,但是,由于我们设置了80个线程,这就就能充分利用CPU)

(3) 一个可以参考的线程数计算方法是:线程数=CPU核心数*(1 + 平均等待时间/平均工作时间);

● 平均等待时间长,表示线程的等待时间;;平均工作时间,指的是线程占用数据库的时间;

● 比如,对于读取数据库这种业务,线程大部分时间是等待的,其等待时间假如说是100秒,而其占用CPU的计算时间假如说是1秒;那么,其线程数=CPU核心数*(1+100/1);假如CPU是8核,那么此时线程池的核心线程数就可以设为808;

(4) 其实,整体的原则就是:

● 如果一个业务在执行的时候,大部分时间是需要占用CPU的,那么线程数就需要设置的小一点;(因为,业务的大部分时间已经占用了CPU了,CPU的空闲时间已经不多了,即CPU的可压榨空间已经很少了;继续创建线程的意义不大);

● 如果一个业务在执行的时候,大部分时间是不需要占用CPU的,那么线程数就可以设置大一点;即,我们尽量多开几个线程,以充分压榨CPU;

(5) 在实际开发中,最精准的策略是:根据不同的程序去做压测;根据压测结果,得到一个相对合适的线程数量;