线程池的创建方式

发布时间 2023-03-25 19:12:20作者: _mcj

1.什么是线程池

随着多线程的大量使用,伴随着大量的线程创建与销毁等这些开销,为了减少这些开销,进行管理线程,线程池就应运而生了。因此线程池是一种基于池化思想管理和使用线程的机制,主要是为了方便管理线程,减少线程的频繁创建与销毁而浪费的资源。

2.线程池的使用

2.1 线程池的创建
线程池的创建方式按照其种类,大致可以分为两大类,分别是Executors与ThreadPoolExecutor。而这两大类又可以分为七个小类,其中Executors有六种,ThreadPoolExecutor有一种,分别如下:
Executors:

创建方式 方式说明
Executors.newFixedThreadPool 创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待
Executors.newCachedThreadPool 创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,如果线程数量不够,则创建新的线程,直到达到所设置的线程上限
Executors.newSingleThreadExecutor 船舰单个线程的线程池,可以保证任务按照顺序执行
Executors.newSingleThreadScheduledExecutor 创建一个单线程的可以延迟执行任务的线程池
Executors.newScheduledThreadPool 创建一个可以执行延迟任务的线程池
Executors.newWorkStealingPool 创建一个抢占式执行的线程池(任务执行顺序不确定)

ThreadPoolExecutor:

创建方式 方式说明
ThreadPoolExecutor() 这个方法是最原始的创建线程池的方法,其有七个参数,每个参数所代表的意义在下面会详细说明

2.2 各种线程池创建的举例说明
(1)Executors.newFixedThreadPool()
Executors.newFixedThreadPool()其共有两种构造方式,一种为newFixedThreadPool(int nThreads),其中的参数nThreads表示线程的数量。另一种为newFixedThreadPool(int nThreads, ThreadFactory threadFactory),其相比前一种多了个threadFactory,这个参数是线程的创建方式。这里的例子用的是第一种的线程池创建方式,线程的创建使用它的默认方式。如下:

public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(4);

        for (int i = 0; i < 10; i++) {
            int j = i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("第" + j + "个任务,当前线程名为:" + Thread.currentThread().getName());
                }
            });
        }

    }

执行结果:
image
分析:由执行结果可以看出,这边线程池执行任务,创建的线程为4个,达到了4个之后就不会创建新的线程了,会服用前面空闲下来的线程来继续执行新的任务。
PS:需要注意的是代码中的Executor.execute()方法,是线程池执行任务的方法,另外执行任务的方法还有另一个方法为ExecutorService.submit()方法,虽然这两个方法都是线程池执行任务的方法,不过他们也是有区别的,execute()的方法执行执行Runnable的任务,submit()方法除了能执行Runnable的任务,还能够执行Callable的任务,也就是submit()方法能够执行有返回值的任务,而execute()方法则不行。
(2)Executors.newCachedThreadPool()
Executors.newCachedThreadPool()方法同样有两种构造方式,一种为newCachedThreadPool(),这种方式没有参数,另一种为newCachedThreadPool(ThreadFactory threadFactory),这种相比前一种同样是多了一个线程创建方式的参数。这里同样是以第一种为例,如下:

public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            int j = i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("第" + j + "个任务,当前线程的名称:" + Thread.currentThread().getName());
                }
            });
        }
    }

执行结果:
循环十次:
image
循环一百次:
image
分析:由上面循环十次的执行结果和循环一百次的执行结果可以看出其线程是有缓存的,当来个新的任务是,如果当前没有空闲的线程,则会创建新的线程来执行新任务,如果此时有空闲的线程,则会使用这个空闲的线程来执行任务。另外当这个线程池中的任务都处于空闲,并达到一定的时间时,则会将该线程池回收,因此这就是这个线程池的程序是会结束,而其它线程池的程序不会结束的原因,另外,线程的空闲时间默认为60秒。
(3)Executors.newSingleThreadExecutor()
Executors.newSingleThreadExecutor()方法同样有两种构造方式,一种为newSingleThreadExecutor(),这种方式没有参数,另一种为newSingleThreadExecutor(ThreadFactory threadFactory),这种相比前一种同样是多了一个线程创建方式的参数。这里同样是以第一种为例,如下:

public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            int j = i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("第" + j + "个任务,当前线程名为:" + Thread.currentThread().getName());
                }
            });
        }
    }

执行结果:
image
分析:由上面的结果可以看出该线程池只会有一个线程来进行执行任务,而执行任务的顺序是有序的,会按照先进先出的顺序来进行执行。这里虽然线程池只有一个线程和创建线程数量一样,但是其避免了频繁创建销毁线程的开销,实现了这一个线程的复用。
(4)Executors.newSingleThreadScheduledExecutor()
Executors.newSingleThreadScheduledExecutor()方法同样有两种构造方式,一种为newSingleThreadScheduledExecutor(),这种方式没有参数,另一种为newSingleThreadScheduledExecutor(ThreadFactory threadFactory),这种相比前一种同样是多了一个线程创建方式的参数。这里同样是以第一种为例,如下:

public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
        for (int i = 0; i < 10; i++) {
            int j = i;
            System.out.println("第" + j + "个任务,加入时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            executor.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("第" + j + "个任务,当前线程名为:" + Thread.currentThread().getName() + ",执行时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
                }
            }, 5, TimeUnit.SECONDS);
        }
    }

执行结果:
image
分析:由上面的执行结果可以得出其确实实现了定时执行任务的功能,能够发现,任务的执行时间比任务的加入晚了五秒钟,并且每个任务都是如此。
PS:其中的ScheduledExecutorService.schedule()方法是具有定时执行功能的线程池特有的方法,表示延迟执行一次定时任务。另外除了这个之外其还有另外两种,分别是ScheduledExecutorService.scheduleAtFixedRate()ScheduledExecutorService.scheduleWithFixedDelay()。其中ScheduledExecutorService.scheduleAtFixedRate()表示固定频率执行,以上次任务的开始时间作为计算时间执行。而ScheduledExecutorService.scheduleWithFixedDelay()表示固定频率执行但是以上次人物的结束时间为计算时间执行。
(5)Executors.newScheduledThreadPool()
Executors.newScheduledThreadPool()方法同样有两种构造方式,一种为newScheduledThreadPool(int corePoolSize),这种方式只有一个参数表示会创建的线程数量,另一种为newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory),这种相比前一种同样是多了一个线程创建方式的参数。这里同样是以第一种为例,如下:


// schedule方式
public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);

        for (int i = 0; i < 10; i++) {
            int j = i;
            System.out.println("第" + j + "个任务,加入时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            executor.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("第" + j + "个任务,当前线程名称为:" + Thread.currentThread().getName() + ",当前执行时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
                }
            }, 5, TimeUnit.SECONDS);
        }

    }
	
	
// scheduleAtFixedRate方式
public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);

        // 以固定频率执行任务,其中initialDelay表示第一次执行与加入时间间隔多久,period表示两次执行之间的间隔时间为多少,也就是频率。
        // 需要注意的是如果线程有沉睡,则看沉睡时间与间隔时间哪个比较长,按照长的算,如果想让执行间隔时间按照睡眠时间加上间隔时间则使用executor.scheduleWithFixedDelay()方法
        System.out.println("加入时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        executor.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("当前线程名称为:" + Thread.currentThread().getName() + ",当前执行时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
                try {
                    Thread.sleep(3*1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, 3, 5, TimeUnit.SECONDS);

    }
	
	
// scheduleWithFixedDelay方式
public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);

        // 以固定频率执行任务,其中initialDelay表示第一次执行与加入时间间隔多久,period表示两次执行之间的间隔时间为多少,也就是频率。
        // 执行间隔时间按照睡眠时间加上间隔时间
        System.out.println("加入时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        executor.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                System.out.println("当前线程名称为:" + Thread.currentThread().getName() + ",当前执行时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
                try {
                    Thread.sleep(3*1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, 3, 5, TimeUnit.SECONDS);

    }

执行结果:
1.schedule方式
image
2.scheduleAtFixedRate方式
image
3.scheduleWithFixedDelay方式
image
分析:由上面可以看出这边是多个线程来执行延迟定时任务,以及三种定时任务的执行方式之间的区别从上面的执行结果中也能够看出。
(6)Executors.newWorkStealingPool()
Executors.newWorkStealingPool()方法同样有两种构造方式,一种为newWorkStealingPool(),这种方式没有参数,另一种为newWorkStealingPool(int parallelism),这种相比前一种多了并行度参数。这里同样是以第一种为例,如下:

public static void main(String[] args) {
        ExecutorService executor = Executors.newWorkStealingPool();
        for (int i = 0; i < 10; i++) {
            int j = i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("第" + j + "个任务,当前线程名为:" + Thread.currentThread().getName());
                }
            });
        }
    }

执行结果:
image
(7)ThreadPoolExecutor()
ThreadPoolExecutor()它的构造方法会比较多,不过看它里面的各个构造方法最后都会调用那个有所有参数的构造函数,也就是这个:

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

参数说明:
corePoolSize:这个表示核心线程的数量。
maximumPoolSize:这个表示最大线程数量。
keepAliveTime:这个表示空闲线程存活时间。
unit:这个是存活时间的单位。
workQueue:这个表示工作队列,是当核心线程满了之后会存放在这里。
threadFactory:这个是线程的创建方式。
handler:这个表示拒绝策略,其有jdk提供的四种拒绝策略和一种自定义的拒绝策略。
拒绝策略:

名字 描述
AbortPolicy 如果线程池拒绝了任务,直接报错。
CallerRunsPolicy 线程池让调用者去执行
DiscardPolicy 如果线程池拒绝了任务,直接丢弃。
DiscardOldestPolicy 如果线程池拒绝了任务,直接将工作队列中第一个任务给丢弃,将新任务入队。
自定义拒绝策略 实现RejectedExecutionHandler接口

流程大致说明:
当有任务进来时首先会判断当前正在执行的任务数是否小于核心线程数,如果小于核心线程数,则会调用核心线程来进行执行新任务,如果不小于则会进行判断当前工作队列是否已满,如果未满,则将任务放到任务队列中,如果已满,则会接着判断当前正在执行的任务数量是否小于最大线程数,如果小于最大线程数则会创建一个新的线程来执行该任务,如果不小于,最会按照拒绝策略决绝该任务。

public class TestMyThread {

    public static void main(String[] args) {
        TestMyThread testMyThread = new TestMyThread();
        testMyThread.test();
    }

    private void test() {
        ThreadFactory threadFactory = new MyThreadFactory("交易核心线程");

        Executor executor = new ThreadPoolExecutor(3,
                6,
                30,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(11),
                threadFactory,
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i <= 20; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }

    }

    class MyThreadFactory implements ThreadFactory {

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

        MyThreadFactory(String featureOfGroup) {
            namePrefix = featureOfGroup + ",线程编号:";
        }


        /**
         * Constructs a new {@code Thread}.  Implementations may also initialize
         * priority, name, daemon status, {@code ThreadGroup}, etc.
         *
         * @param r a runnable to be executed by new thread instance
         * @return constructed thread, or {@code null} if the request to
         * create a thread is rejected
         */
        @Override
        public Thread newThread(Runnable r) {
            String name = namePrefix + nextId.getAndIncrement();
            return new Thread(null, r, name, 0);
        }
    }

}

执行结果:
image
分析:
由执行结果可知其创建的线程数量最大为6个,然后在执行了17条任务之后,触发了异常,从异常可以看出其是触发了拒绝策略。

3.总结

其实Executors创建线程池的方法,里面距离创建线程池的方法也是调用ThreadPoolExecutor来实现的,所以还是比较建议使用ThreadPoolExecutor这种方式来创建线程池。