java JUC 之 四种常用线程池 + Spring提供的线程池技术

目录

一. 线程池的概念

二. 使用线程池的原因

三. JUC常用四种创建线程池方式

四. 常见的四种线程池详解

五. 进一步应用 --> Spring提供的线程池技术ThreadPoolTaskExecutor的使用

六. 感谢大家的阅读!


一. 线程池的概念

        线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。

二. 使用线程池的原因

1. 在程序运行中,新任务的调用需要创建新的线程,在任务调用结束后,该线程需要被销毁,在创建和销毁的过程中是很费资源的,那么如果在并发量大的情况下,频繁的创建和销毁会占用大量的资源,为了减少资源的消耗,我们可以选择线程池,就是在应用程序开始的时候,就创建出一些线程以供以后使用,这样即使在任务使用线程结束之后,也不会销毁,而是继续把它放在线程池中,这样就使得线程池线程是可以复用的,就可以使每个任务的调用开销减少。

2. 线程池提供了一种资源限制和管理的手段,比如当执行一系列任务时候对线程的管理,每个ThreadPoolExecutor也保留了一些基本的统计数据,比如当前线程池完成的任务数目。

三. JUC常用四种创建线程池方式

注意:线程池的创建不允许使用 Executors 去创建,而是通过ThreadPoolExecutor的方式去创建,这样的处理方式可以规避资源耗尽的风险。 下面我们作为测试使用,只是介绍其使用,所以使用 Executors 去创建来测试。

Executors 创建的线程池对象的弊端如下:

1. FixedThreadPool 和 SingleThreadExecutor:

允许的请求队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。

2. CachedThreadPool 和 ScheduledThreadPool:

允许的请求队列长度为Integer.MAX_VALUE,可能创建大量的线程,从而导致OOM。

记住 : 实际操作中,请不要使用 Executors 去创建,而是通过ThreadPoolExecutor的方式去创建!!!!

1. Executors.newSingleThreadExecutor()(单个后台线程)

2. Executors.newFixedThreadPool(int)(固定大小线程池)

3. Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收)

4. Executors.newScheduledThreadPool(定时,延时线程池)

四. 常见的四种线程池详解

1. Executors.newSingleThreadExecutor()(单个后台线程)

        创建一个核心线程个数和最大线程个数都为1的线程池,并且阻塞队列长度为Integer.MAX_VALUE,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。只要线程个数比核心线程个数多并且当前空闲则回收。 

        如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。  

使用示例如下:

public class NewSingleThreadExecutor {

    public static void main(String[] args) {
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            // 这里是java 8 的 lambda 表达式
            // singleThreadExecutor.execute(() -> {}) 等价于  singleThreadExecutor.execute(new Runnable() {})
            singleThreadExecutor.execute(() -> {
                try {
                    // 预期结果 : 10个线程按顺序依此执行
                    System.out.println(Thread.currentThread().getName() + " index : " + index);
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

2. newFixedThreadPool(int)(固定大小线程池)

        创建一个核心线程个数和最大线程个数都为nThreads的线程池,并且阻塞队列长度为Integer.MAX_VALUE,每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。只要线程个数比核心线程个数多并且当前空闲则回收。

        FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。 但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

使用示例如下:

public class NewFixedThreadPool {
    public static void main(String[] args) {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int index = i;
            // 这里是java 8 的 lambda 表达式
            // fixedThreadPool.execute(() -> {}) 等价于  fixedThreadPool.execute(new Runnable() {})
            fixedThreadPool.execute(() -> {
                try {
                    // 预期结果 : 只有三个线程在轮询
                    System.out.println(Thread.currentThread().getName() + " index : " + index);
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

3. newCachedThreadPool() :(无界线程池)

        创建一个按需创建线程的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。初始线程个数为0,最多线程个数为Integer.MAX_VALUE,并且阻塞队列为同步队列。只要当前线程60s内空闲则回收。这个特殊在于加入到同步队列的任务会被马上被执行,同步队列里面最多只有一个任务,并且存在后马上会拿出执行。

        特点是: 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。 终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。 在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。

使用示例如下:

public class NewCachedThreadPool {

    public static void main(String[] args) {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            try {
                Thread.sleep(index * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 这里是java 8 的 lambda 表达式
            // cachedThreadPool.execute(() -> {}) 等价于  cachedThreadPool.execute(new Runnable() {})
            // 预期结果 :一个线程在轮询执行,如果当前线程还未终止就向线程池提交了新的任务,线程池就开启新的线程进行处理
            cachedThreadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " index : " + index));
            cachedThreadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 我是为了在 输出index的线程 还未结束时,就向线程池提交了新的任务,以开启新的线程处理 "));
        }
    }
}

4. newScheduledThreadPool(定时,延时线程池)

        创建一个最小线程个数corePoolSize,最大为Integer.MAX_VALUE,阻塞队列为DelayedWorkQueue的线程池。       

        是一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

使用示例如下:

public class NewScheduleThreadPool {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        // 这里是java 8 的 lambda 表达式
        // scheduledThreadPool.execute(() -> {}, delay, unit) 等价于  scheduledThreadPool.schedule(new Runnable() {}, delay, unit)
        // 测试 1 :预期结果 :任务延时 3 秒执行
        scheduledThreadPool.schedule(() -> System.out.println(Thread.currentThread().getName() + "延迟 2 秒 后执行"), 2, TimeUnit.SECONDS);

        // 这里是java 8 的 lambda 表达式
        // scheduledThreadPool.execute(() -> {}, initialDelay, period,unit) 等价于  scheduledThreadPool.schedule(new Runnable() {}, initialDelay, period,unit)
        // 测试 2 :预期结果 :任务延时 6 秒执行,并且每 2 秒执行一次。 最多有五条线程在轮询执行,因为创建线程池大小为 5
        scheduledThreadPool.scheduleAtFixedRate(()->{
            System.out.println(Thread.currentThread().getName() + "延迟 6 秒后启动,并且每 2 秒执行一次");
                    try {
                        // 模拟执行过程需要 1s 的时间
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                },
                0,
                2,
                TimeUnit.SECONDS);

        // scheduledThreadPool.scheduleWithFixedDelay(() -> {},initialDelay, delay, unit) 等价于  scheduledThreadPool.scheduleWithFixedDelay(new Runnable() {}, initialDelay, delay, unit)
        // 测试 3 :预期结果 :任务延时 6 秒执行,并且后面每次执行都是在执行完成后再延迟 2 秒执行一次。 最多有五条线程在轮询执行,因为创建线程池大小为 5
        scheduledThreadPool.scheduleWithFixedDelay(()->{
            System.out.println(Thread.currentThread().getName() + "启动后第一次延迟 6 秒执行,后面每次延时 2 秒执行");
                    try {
                        // 模拟执行过程需要 1s 的时间
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                },
                6,
                2,
                TimeUnit.SECONDS);


        // todo 这里的 scheduleAtFixedRate() 和 scheduleWithFixedDelay() 都是以固定的频率去执行,看结果相似,但是源码中的区别是 (举例:第一次执行是 5s 后,假如执行耗时 1s,执行频率是 2s):
        // todo scheduleAtFixedRate():  第三个参数为 period ,指的是两次成功执行之间的时间  (第一次执行是 5s 后,第二次执行是 7s 后,第三次执行是 9s 后)
        // todo scheduleWithFixedDelay(): 第三个参数为 delay ,指的是一次执行终止 和 下一次执行开始的延迟时间 (第一次执行是 5s 后,执行完是 6s 后,第二次执行是 8s 后,执行完是 9s 后,第三次执行是 11s 后)
    }
}

五. 进一步应用 --> Spring提供的线程池技术ThreadPoolTaskExecutor的使用

        ThreadPoolTaskExecutor是Spring为我们提供的线程池技术,其实,这个是spring对于JUC包下的ThreadPoolExecutor类的封装,(有点类似于装饰者模式。当然Spring提供的功能更加强大些,因为还有定时调度功能)。

        因为是Spring封装的,技术相对成熟,尤其对于生产环境来说,如果业务场景满足的话,更推荐大家使用这一种。

使用示例如下:

5.1 ThreadPoolTaskExecutor的配置:

<bean id="threadPoolTaskExecutor"
        class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <!-- 核心线程数 -->
        <property name="corePoolSize" value="3" />
        <!-- 最大线程数 -->
        <property name="maxPoolSize" value="10" />
        <!-- 队列最大长度 >=mainExecutor.maxSize -->
        <property name="queueCapacity" value="25" />
        <!-- 线程池维护线程所允许的空闲时间 -->
        <property name="keepAliveSeconds" value="300" />
        <!-- 线程池对拒绝任务(无线程可用)的处理策略 ThreadPoolExecutor.CallerRunsPolicy策略 ,调用者的线程会执行该任务,如果执行器已关闭,则丢弃.  -->
        <property name="rejectedExecutionHandler">
            <bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
        </property>
    </bean>

5.2 举例使用:

public class ThreadPoolTaskExecutorTest {

    // todo 温馨提示:这里注入需要依赖于 Spring 容器,所以此测试类不能直接运行,只是将写法列在这里以供参考,具体用法这里也列了一种,感兴趣大家可以再研究

    @Autowired
    private static ThreadPoolTaskExecutor threadPoolTaskExecutor;

    public static void main(String[] args) {

        // // 这里是java 8 的 lambda 表达式
        // threadPoolTaskExecutorTest.execute(() -> {}) 等价于 threadPoolTaskExecutorTest.execute(new Runnable() {})
        threadPoolTaskExecutorTest.execute(() -> {
            System.out.println("在这里进行任务的执行");
            // 模拟任务执行需要 2s
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("执行完毕!");
        });
    }
}

六. 感谢大家的阅读!

代码已上传至github,需要的可以自己去下载:https://github.com/higminteam/practice/tree/master/src/main/java/com/practice/concurrent/threadPool

 

其他关于JUC源码解析文章:

深入了解 Java JUC(一)之 atomic(原子数据类的包)

深入了解 Java JUC(二)之 从JUC锁机制AQS到重入锁、读写锁和CountDownLatch

java JUC 之 四种常用线程池 + Spring提供的线程池技术

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值