线程池总结

简介: 线程池总结

为什么要有线程池


线程的创建虽然比进程轻量, 但是在频繁创建情况下, 系统的开销是不可忽略的.

创建个线程池, 我们就可以从线程池拿线程, 这是纯粹的用户态操作.

如果从系统创建线程, 则涉及到用户态与内核态间的切换, 真正的创建是在内核态完成的.

而纯用户态操作时间是可控的, 涉及到内核态时间就不可控.


结论 : 线程池可以提高线程创建效率, 减少每次启动, 销毁线程的损耗.


标准库中的线程池


标准库中提供了现成的线程池, 可以通过下面代码来创键:


ExecutorService poll = Executors.newFixedThreadPool(10);


使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.

返回值类型为 ExecutorService.

通过 ExecutorService.submit 可以添加一个任务到线程池中.


为什么这里不是直接 new 对象, 而是通过调用 Executors 类的静态方法来创建对象.


这里就涉及到了工厂模式, 啥是工厂模式呢?


创建对象并非直接 new, 而是使用一些其他的方法 (通常是静态方法) 协助我们把对象创建出来.


工厂模式是用来填构造方法的坑的.

举个例子 :

在一个类里, 要想提供多种不同的构造对象的方式就得基于重载.

但重载规定参数列表必须不同, 这就出现了一个问题, 如果我们想通过相同的参数构造不同的对象呢?


比如这个类 :

c712b59626d24f98bdbadd5bef63eb65.png


为了解决上述问题, 我们可以构造一个工厂类 :fdbfcf4b74bd4663838747ec627468d5.png



这便是工厂模式.


Executors 创建线程池的几种方式


1.newFixedThreadPool: 创建固定线程数的线程池

public class Test {
    public static void main(String[] args) {
        ExecutorService poll = Executors.newFixedThreadPool(10);
        poll.submit(new Runnable() { //将任务添加进线程池中
            @Override
            public void run() {
                System.out.println("111");
            }
        });
        System.out.println("222");
    }
}

d96ac178c1d145e7b1cb625a2b5f162a.png

可以看到线程池执行完任务后并没有结束, 而是一直运行, 其实它里面内置了线程来执行任务, 是前台线程, 会阻止线程的结束.


2.newCachedThreadPool: 创建线程数目动态增长的线程池.

public class Test {
    public static void main(String[] args) {
        ExecutorService poll = Executors.newCachedThreadPool();
        poll.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("111");
            }
        });
        System.out.println(222);
    }
}


3.newSingleThreadExecutor: 创建只包含单个线程的线程池.

public class Test {
    public static void main(String[] args) {
        ExecutorService poll = Executors.newSingleThreadExecutor();
        poll.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("111");
            }
        });
        System.out.println(222);
    }
}


4.newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

public class Test {
    public static void main(String[] args) {
        ScheduledExecutorService poll = Executors.newScheduledThreadPool(1);
        poll.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("111");
            }
        },3, TimeUnit.SECONDS); //设置3S后执行
        System.out.println(222);
    }
}

ec559146c2df48718f7cb2f53e13189c.png


线程池的简单模拟


class MyThreadPool {
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
    //实现一个固定线程的线程池
    public MyThreadPool(int n) throws InterruptedException {
        for(int i = 0; i < n; i++) {  //循环n次, 创建n个线程
            Thread t = new Thread(() -> {
                while(true) {   //保证每个线程一直在取任务
                    try {
                        Runnable runnable = queue.take();
                        runnable.run();   //执行任务
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();  //启动线程
        }
    }
}
public class ThreadDemo3 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(10);
        for(int i = 0; i < 10000; i++) {
            int m = i;
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("打印数字: " + m);  //注意这里不能为i
                }
            });
        }
    }
}


676b0c84b43043749f469153caa45100.png


为什么打印数字要用 m 而不直接用 i 呢? 定义 m 变量是多此一举吗?

这里和 lambda 表达式有关, 这里虽然没有用到 lambda 表达式, 但是这里用到了匿名内部类, lambda 表达式本质上也是匿名内部类.


lambda 表达式中捕获的变量必须是 final 修饰的 或者是 “实际 final”(没有被final修饰, 但是代码中没有对该变量进行修改过)


这里 i 就是一直被修改的变量, 而 m 不一样, 我们每次进入循环都会再创建一个 m , 这样保证每次循环里的 m 都不一样, 每个 m 都不会改变, 这就满足了“实际 final”.


Executors 本质上是 ThreadPoolExecutor 类的封装, 也就是说 Executors 是一个工厂类.


ThreadPoolExecutor 的参数介绍


我们可以在 java.util.concurrent 下找到该类.

7150ee8e055b48b1b46e035639139106.png


来看看它的构造方法:

76a26272ffbe464c9c8ea4d6614ddce1.png


要明白构造方法, 首先要明白它的参数意义, 我们就拿它参数最多的构造方法来说明 :

c2581854b9cc444fbfaac15a1da44a05.png


  1. corePoolSize : 核心线程数(最主要的线程, 不会被销毁)
  2. maximumPoolSize : 最大线程数(核心线程数 + 临时线程数)

如果当前任务比较多, 线程池就会多创建一些 “临时线程”, 当任务少了, 比较空闲了, 线程池就会把多出来的临时线程销毁掉.(核心线程不会动)

  1. keepAliveTime : 保持存活的最大时间.(当任务比较少时, 整体空闲下来的时候, 临时线程不会立刻被销毁, 而是会存活一段时间, 等待任务, 如果这段时间内还没有接到新任务, 那就会被销毁)
  2. BlockingQueue workQueue : 线程池要管理很多任务, 这些任务是通过阻塞队列来组织的, 我们可以手动指定的给线程池一个队列, 此时就可以很方便的控制 / 获取队列的信息了, submit方法就是将任务放到该队列中.
  3. ThreadFactory threadFactory : 工厂类, 就是创建线程的辅助类.
  4. RejectedExecutionHandler handler : 线程池的拒接策略.(如果线程池的池子满了, 继续往里添加任务, 如何拒绝)


线程池的执行流程


9b5c800abb5f4278b6fcaf20ae75d06e.png


当添加新任务时, 首先进行判断, 判断核心线数是否为满, 如果没满, 就创建核心线程执行任务, 如果核心线程已满, 则判断任务队列是否有地方存放该任务, 如果有, 就将任务保存在任务队列中, 等待执行, 如果满了, 再判断最大可容纳的线程数, 如果没有超过这个数量, 就创建非核心线程执行任务, 如果超出, 就调用 handler 实现拒绝策略.


标准库中提供的四种拒绝策略(经典面试题)


135cf03896574601982c8fe7d09a356d.png


ThreadPoolExecutor.AbortPolicy : 如果线程池满了, 继续添加任务,则会直接抛出异常.

ThreadPoolExecutor.CallerRunsPolicy : 添加的线程自己负责执行该任务.(哪个线程将该任务给它, 哪个线程就去负责执行)

ThreadPoolExecutor.DiscardOldestPolicy : 丢弃最老的任务.(运行时间最长的任务)

ThreadPoolExecutor.DiscardPolicy : 丢弃最新任务.(也就是要添加的任务)


相关文章
|
6月前
|
NoSQL Java 应用服务中间件
|
缓存 Java 应用服务中间件
线程池的10个坑你都遇到过吗
日常开发中,为了更好管理线程资源,减少创建线程和销毁线程的资源损耗,我们会使用线程池来执行一些异步任务。但是线程池使用不当,就可能会引发生产事故。大家看完肯定会有帮助的~
229 0
|
2月前
|
监控 Java API
如何快速地实现一个线程池
如何快速地实现一个线程池
23 0
|
3月前
|
算法 Java
线程池
【8月更文挑战第22天】
42 4
|
3月前
|
Java 调度
基于C++11的线程池
基于C++11的线程池
|
5月前
|
存储 缓存 安全
线程池相关详解
线程池相关详解
|
6月前
|
Java C++
c++简单线程池实现
c++简单线程池实现
|
监控 Java
线程池的讲解和实现
线程池的讲解和实现
KeyAffinityExecutor 线程池
KeyAffinityExecutor 线程池
|
缓存 NoSQL Java
【线程池】
【线程池】
148 0