7.Thread Pool(线程池)

概念:线程池顾名思义是存放线程的池子,意思是当程序需要用到线程的时候,就从线程池中取出线程,用完之后就可以将线程放回线程池中去。

7.1线程池相关概念

7.1.1 线程池数量

Question1:线程池中的线程数量是不是越多越好呢?答案当然是否定的!
1、线程是一个对象,也是操作系统的资源,线程创建、销毁需要实现。如果创建时间+销毁时间 > 执行任务时间,这样创建线程就很划不来了。
2、创建线程需要占用堆内存,操作系统线程占用系统内存,根据JVM规范,一个线程默认最大栈大小是1M,这个栈空间是需要从系统内存中分配的,线程过多,也会消耗很多系统内存。
3、操作系统需要频繁切换线程上下文(各个线程都会去争抢CPU的执行时间),这样也很影响性能。

Question2:线程池的数量是多少合适?
计算型任务:CPU数量的1-2倍
IO型任务:要根据具体的IO阻塞时长进行考量决定,如tomcat中默认的最大线程数:200。也可以根据需要在一个最小数量和最大数量间自动增减线程数。(可以监控CPU的情况来确定,一般来说CPU占用达到80%就差不多了)

7.1.2 线程池相关概念

线程池管理器:用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务
工作线程:线程池中线程在没有任务时处于等待状态,可以循环的执行任务。
任务接口:每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等。
任务队列:用于存放没有处理的任务,提供一种缓冲机制。

好比图中的这个例子,有很多的任务,但是执行任务的人只有4个,也就是工作线程数只有4个,但是队列可以先缓存任务,等这些在执行任务的人执行完了他当前的任务之后,他再从队列中继续拿任务过来执行。

7.1.3 线程池API

7.1.3.1 接口定义和实现类

7.1.3.2 方法定义

ExecutorService的方法定义:


ScheduledExecutorService的方法定义:

7.1.3.3 Executor工具类

JDK中提供了三种线程池的工厂类:
1)newFixedThreadPool(int nThreads):创建一个固定大小、任务队列无界的线程池。核心线程数等于最大线程池数。
2)newCachedThreadPool():创建一个大小无界的缓冲线程池,它的任务队列是一个同步队列,任务加入到池中,如果池中有空闲线程,则用空闲线程执行,如无则创建新线程执行,池中的线程空闲时间超过60秒,将被销毁释放,线程数随任务的多少变化,适用于执行耗时小的异步任务。核心线程数为0,最大线程数为Integer.MAX_VALUE。
3)newSingleThreadExecutor():只有一个线程来执行无界任务队列的单一线程池,该线程池确保任务按加入的顺序一个一个依次执行。当唯一的线程因任务异常终止时,将创建一个新的线程来继续执行后续的任务,与newFixedThreadPool(1)的区别在于,单一线程池的池大小在newSingleThreadExcutor方法中硬编码不能再改变了。

7.1.3.4 任务execute过程

7.2 ThreadPoolExecutor实例

下面将进行几个测试,测试核心线程池、最大线程池、任务队列、超过核心线程数量的线程存活时间等。

7.2.1 任务队列无界

ThreadPoolDemo1:

/**
 * @Author: Vander
 * @Date: 2019/3/26
 * @Description: 线程池信息: 核心线程数量5,最大数量10,无界队列,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略的
 */
public class ThreadPoolDemo1 {

    /**
     * 使用无界队列
     *
     * @throws Exception
     */
    public static ThreadPoolExecutor threadPoolExecutor() throws Exception {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(), threadFactory());
        // 预计结果:线程池线程数量为:5,超出数量的任务,其他的进入队列中等待被执行
        return threadPoolExecutor;
    }

    public static ThreadFactory threadFactory() {
        ThreadFactory factory = r -> {
           Thread thread = new Thread(r);
           String threadName = String.format("ThreadGroup:%s-threadId:%s", thread.getThreadGroup().getName(), thread.getId());
           thread.setName(threadName);
           return thread;
        };
        return factory;
    }

}

Main:

public class Main {

    public static void main(String args[]) throws Exception {
        testCommon(ThreadPoolDemo1.threadPoolExecutor());
    }

    /**
     * 测试: 提交15个执行时间需要3秒的任务,看线程池的状况
     *
     * @param threadPoolExecutor 传入不同的线程池,看不同的结果
     * @throws Exception
     */
    public static void testCommon(ThreadPoolExecutor threadPoolExecutor) throws Exception {
        // 测试: 提交15个执行时间需要3秒的任务,看超过大小的2个,对应的处理情况
        for (int i = 0; i < 15; i++) {
            int n = i;
            threadPoolExecutor.submit(() -> {
                try {
                    System.out.println("开始执行:" + n);
                    Thread.sleep(3000L);
                    System.err.println("执行结束:" + n);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            System.out.println("任务提交成功 :" + i);
        }
        // 查看线程数量,查看队列等待数量
        Thread.sleep(500L);
        System.out.println("当前线程池线程数量为:" + threadPoolExecutor.getPoolSize());
        System.out.println("当前线程池等待的数量为:" + threadPoolExecutor.getQueue().size());
        // 等待15秒,查看线程数量和队列数量(理论上,超出核心线程数量的线程会被自动销毁)
        Thread.sleep(15000L);
        System.out.println("当前线程池线程数量为:" + threadPoolExecutor.getPoolSize());
        System.out.println("当前线程池等待的数量为:" + threadPoolExecutor.getQueue().size());
    }

}

使用无界队列的结果是任务都被缓存起来了,任务创建起来之后会被放到无界队列中等待执行,由于无界队列不会满,所以最大线程数设置了也不起作用。

7.2.2 任务队列有界,并设置拒绝任务策略

/**
 * @Author: Vander
 * @Date: 2019/3/26
 * @Description: 线程池信息: 核心线程数量5,最大数量10,队列大小3,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略的
 */
public class ThreadPoolDemo2 {

    /**
     *
     * @throws Exception
     */
    public static ThreadPoolExecutor threadPoolExecutor() throws Exception {
        // 创建一个 核心线程数量为5,最大数量为10,等待队列最大是3的线程池,也就是最大容纳13个任务。
        // 默认的策略是抛出RejectedExecutionException异常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3), new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.err.println("有任务被拒绝执行了");
            }
        });
        return  threadPoolExecutor;
        // 预计结果:
        // 1、 5个任务直接分配线程开始执行
        // 2、 3个任务进入等待队列
        // 3、 队列不够用,临时加开5个线程来执行任务(5秒没活干就销毁)
        // 4、 队列和线程池都满了,剩下2个任务,没资源了,被拒绝执行。
        // 5、 任务执行,5秒后,如果无任务可执行,销毁临时创建的5个线程
    }

}

执行的结果可以看到“有任务被拒绝执行”。

7.2.3 使用无界的同步队列

SynchronousQueue,实际上它不是一个真正的队列,因为它不会为队列中元素维护存储空间。与其他队列不同的是,它维护一组线程,这些线程在等待着把元素加入或移出队列。

在使用SynchronousQueue作为工作队列的前提下,客户端代码向线程池提交任务时,而线程池中又没有空闲的线程能够从SynchronousQueue队列实例中取一个任务,那么相应的offer方法调用就会失败(即任务没有被存入工作队列)。此时,ThreadPoolExecutor会新建一个新的工作者线程用于对这个入队列失败的任务进行处理(假设此时线程池的大小还未达到其最大线程池大小maximumPoolSize)。

简单的说,由于同步队列不缓存任务,所以它一拿到任务就会丢给线程池去处理,线程池中如果有空闲线程,空闲线程就会去处理该任务,若没有空闲线程,则创建新的线程去处理。

在实际应用中不建议直接将任务的最大值设置为Integer的最大值,可以控制一下它最大值才使用它。

/**
 * @Author: Vander
 * @Date: 2019/3/26
 * @Description:
 */
public class ThreadPoolDemo3 {

    /**
     * 3、 线程池信息:
     * 核心线程数量0,最大数量Integer.MAX_VALUE,SynchronousQueue队列,超出核心线程数量的线程存活时间:60秒
     *
     * @throws Exception
     */
    public static ThreadPoolExecutor threadPoolExecutor() throws Exception {

        // SynchronousQueue,实际上它不是一个真正的队列,因为它不会为队列中元素维护存储空间。与其他队列不同的是,它维护一组线程,这些线程在等待着把元素加入或移出队列。
        // 在使用SynchronousQueue作为工作队列的前提下,客户端代码向线程池提交任务时,
        // 而线程池中又没有空闲的线程能够从SynchronousQueue队列实例中取一个任务,
        // 那么相应的offer方法调用就会失败(即任务没有被存入工作队列)。
        // 此时,ThreadPoolExecutor会新建一个新的工作者线程用于对这个入队列失败的任务进行处理(假设此时线程池的大小还未达到其最大线程池大小maximumPoolSize)。

        // 和Executors.newCachedThreadPool()一样的
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
                new SynchronousQueue<>());
        // 预计结果:
        // 1、 线程池线程数量为:15,超出数量的任务,其他的进入队列中等待被执行
        // 2、 所有任务执行结束,60秒后,如果无任务可执行,所有线程全部被销毁,池的大小恢复为0
        return threadPoolExecutor;
    }

}

预计结果:
1、线程池线程数量为:15,超出数量的任务,其他的进入队列中等待被执行
2、所有任务执行结束,60秒后,如果无任务可执行,所有线程全部被销毁,池的大小恢复为0。

7.2.4 线程池终止的方式(shutdown和shutdownNow)

线程池终止线程执行的方式有两种:
1、shutdown
2、shutdownNow

7.2.4.1 shutdown

/**
 * @Auther: Vander
 * @Date: 2019/3/26
 * @Description: 测试终止线程池的shutdown方法
 */
public class ThreadPoolDemo7 {

    /**
     * 7、 终止线程:线程池信息: 核心线程数量5,最大数量10,队列大小3,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略的
     *
     * @throws Exception
     */
    private void threadPoolExecutorTest7() throws Exception {
        // 创建一个 核心线程数量为5,最大数量为10,等待队列最大是3 的线程池,也就是最大容纳13个任务。
        // 默认的策略是抛出RejectedExecutionException异常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.err.println("有任务被拒绝执行了");
            }
        });
        // 测试: 提交15个执行时间需要3秒的任务,看超过大小的2个,对应的处理情况
        for (int i = 0; i < 15; i++) {
            int n = i;
            threadPoolExecutor.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("开始执行:" + n);
                        Thread.sleep(3000L);
                        System.err.println("执行结束:" + n);
                    } catch (InterruptedException e) {
                        System.out.println("异常:" + e.getMessage());
                    }
                }
            });
            System.out.println("任务提交成功 :" + i);
        }
        // 1秒后终止线程池
        Thread.sleep(1000L);
        threadPoolExecutor.shutdown();
        // 再次提交提示失败
        threadPoolExecutor.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("追加一个任务");
            }
        });

    }
}

结果分析
1、10个任务被执行,3个任务进入队列等待,2个任务被拒绝执行
2、调用shutdown后,不接收新的任务,等待13任务执行结束
3、追加的任务在线程池关闭后,无法再提交,会被拒绝执行

7.2.4.2 shutdownNow

/**
 * @Auther: Vander
 * @Date: 2019/3/26
 * @Description: 立刻终止线程:线程池信息: 核心线程数量5,最大数量10,队列大小3,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略的
 */
public class ShutdownNowDemo {

    /**
     *
     * @throws Exception
     */
    public static void threadPoolExecutor() throws Exception {
        // 创建一个 核心线程数量为5,最大数量为10,等待队列最大是3 的线程池,也就是最大容纳13个任务。
        // 默认的策略是抛出RejectedExecutionException异常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3), (r, executor) -> System.err.println("有任务被拒绝执行了"));
        // 测试: 提交15个执行时间需要3秒的任务,看超过大小的2个,对应的处理情况
        for (int i = 0; i < 15; i++) {
            int n = i;
            threadPoolExecutor.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("开始执行:" + n);
                        Thread.sleep(3000L);
                        System.err.println("执行结束:" + n);
                    } catch (InterruptedException e) {
                        System.out.println("异常:" + e.getMessage());
                    }
                }
            });
            System.out.println("任务提交成功 :" + i);
        }
        // 1秒后终止线程池
        Thread.sleep(1000L);
        List<Runnable> shutdownNow = threadPoolExecutor.shutdownNow();
        // 再次提交提示失败
        threadPoolExecutor.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("追加一个任务");
            }
        });
        System.out.println("未结束的任务有:" + shutdownNow.size());
    }
}

结果分析:
1、10个任务被执行,3个任务进入队列等待,2个任务被拒绝执行
2、调用shutdownnow后,队列中的3个线程不再执行,10个线程被终止
3、追加的任务在线程池关闭后,无法再提交,会被拒绝执行

7.3 ScheduledThreadPoolExecutor实例

7.3.1一次性的定时任务

/**
 * @Auther: Vander
 * @Date: 2019/3/26
 * @Description: 定时执行线程池信息:3秒后执行,一次性任务,到点就执行
 */
public class ThreadPoolDemo1 {
    /**
     * 核心线程数量5,最大数量Integer.MAX_VALUE,DelayedWorkQueue延时队列,超出核心线程数量的线程存活时间:0秒
     *
     * @throws Exception
     */
    public static ThreadPoolExecutor threadPoolExecutor() throws Exception {
        // 和Executors.newScheduledThreadPool()一样的
        ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
        threadPoolExecutor.schedule(() -> System.out.println("任务被执行,现在时间:" + System.currentTimeMillis()), 3000, TimeUnit.MILLISECONDS);
        System.out.println(
                "定时任务,提交成功,时间是:" + System.currentTimeMillis() + ", 当前线程池中线程数量:" + threadPoolExecutor.getPoolSize());
        // 预计结果:任务在3秒后被执行一次
        return threadPoolExecutor;
    }
}

定时任务实现的本质是使用延迟队列,DelayedWorkQueue得等到时间到了才能从队列中取出任务,否则取不出来。

7.3.2 scheduleAtFixedRate和scheduledWithFixedDelay

不管是scheduleAtFixedRate和scheduledWithFixedDelay都不会并发执行线程,区别在于,假设当前线程要执行3秒,FixedRate、FixedDelay设置的是1秒,则FixedRate的线程会马上执行下一次任务,而FixedDelay会在上个线程执行完之后,重新计时,该线程要到第4秒才执行任务。

7.3.2.1 scheduleAtFixedRate示例

public class FixedRateDemo {

    public static void main(String args[]){
        testFixedRate();
    }

    public static void testFixedRate(){
        ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(5);
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
            System.out.println(df.format(new Date()));// new Date()为获取当前系统时间
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, 0, 2, TimeUnit.SECONDS);
    }

}

执行结果:


执行结果

7.3.2.2 scheduledWithFixedDelay示例

public class FixedDelayDemo {
    public static void main(String args[]){
        testFixedDelay();
    }

    public static void testFixedDelay(){
        ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(5);
        scheduledExecutorService.scheduleWithFixedDelay(() -> {
            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
            System.out.println(df.format(new Date()));// new Date()为获取当前系统时间
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, 0, 2, TimeUnit.SECONDS);
    }
}

执行结果:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352

推荐阅读更多精彩内容