【Java多线程】案例(3):线程池

目录

一、线程池是什么?

二、Java标准库中的线程池

1. ThreadPoolExecutor类

2. Executors类

3. 创建线程池的两种方式中应该怎么选择?

三、自己实现线程池


一、线程池是什么?

在Java中,池(Pool)的概念被广泛应用于各种场景,如:常量池、数据库连接池、对象池等,这些池的设计都是为了提高资源的重用性和系统的性能,降低资源创建和销毁的开销。线程池达成的也是类似的效果。

假设你是一个餐厅的经理,而餐厅的厨房就是一个需要管理的多线程系统。

  1. 厨房中的厨师就是线程: 想象一下,你的厨房里有一群厨师,每个厨师都是一个线程,负责烹饪不同的菜肴。

  2. 顾客点菜就是任务: 每当顾客来点菜,相当于有一个新的任务进入系统,需要厨师去做。

  3. 任务的处理速度: 假设每个菜肴需要一定的时间来烹饪。如果每次有顾客点菜都要招募一个新的厨师(创建新线程),那么会增加很多开销,比如为新厨师分配工作空间、准备厨具等,这就是线程创建和销毁的开销。

  4. 线程池的作用: 相反,如果你事先准备好一群厨师(线程池),当顾客点菜时,只需从这群厨师中选一个空闲的厨师来做即可,不需要再额外招募新的厨师(创建新线程)。

  5. 控制厨师数量: 另外,你可能会考虑到厨房的大小和工作效率,不会让太多的厨师同时在厨房里工作(控制线程数量),以免拥挤和混乱。

通过这种方式,你能更有效地利用厨师(线程)的资源,减少了频繁招募和解散厨师(创建和销毁线程)的开销,提高了整个厨房(系统)的效率和稳定性。

二、Java标准库中的线程池

1. ThreadPoolExecutor类

ThreadPoolExecutor是线程池的主要实现类之一。它提供了一个灵活的线程池实现,可以根据需要调整核心线程数、最大线程数、线程存活时间等参数。通过 ThreadPoolExecutor,可以创建自定义的线程池,以满足特定的需求。

这个线程池用起来比较复杂,构造方法中有很多的参数.(面试考点)

对于上图中的四种构造方法,只要掌握最后一种,即带7个参数的构造方法,前面的就都能理解了,面试中会问到的也是这个带7个参数的构造.

这7个参数主要可以分为几组来理解:

  1. 参数一(corePoolSize)和参数二(maximunPoolSize):分别表示线程池的核心线程数最大线程数。可以理解为,如果任务比较轻量,仅有核心线程就可以解决了;如果任务比较繁重,则会创建几个临时线程(核心线程+临时线程<=最大线程数)来解决任务,等到任务执行的差不多了,临时线程就会被释放(回收).
  2. 参数三(keepAliveTime)和参数四(unit):分别表示临时线程(非核心线程)的最长空闲时间设置该空闲时间的单位。临时线程在这个设定的时间内一直没有任务执行,就会被回收。unit的类型是TimeUnit,这是一个枚举类,用于表示时间单位,它提供了一组枚举常量用于表示不同的时间单位,包括秒、毫秒、微秒、纳秒等。
  3. 参数五(workQueue):用于指定等待执行的任务队列,即案例(2)的阻塞队列。线程池会提供一个 submit 方法,让其他线程将任务提交给线程池,submit 中就是将任务加入到这个任务队列。
  4. 参数六(threadFactory):创建线程的工厂,提供了多个静态方法,是线程的不同初始化方式,默认的线程工厂是通过 Executors 类中的静态方法 defaultThreadFactory() 返回的。
  5. 参数七(handler):任务量超出负荷的拒绝策略。

使用自定义线程池的代码示例:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Demo2 {
    public static void main(String[] args) {
        //自定义线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                3,  //核心线程数
                6,             //最大线程数
                60,            //临时线程的最长空闲时间
                TimeUnit.SECONDS,   //空闲时间的单位
                new ArrayBlockingQueue<>(100),  //传递任务的阻塞队列
                Executors.defaultThreadFactory(),   //用于创建新线程的线程工厂
                new ThreadPoolExecutor.AbortPolicy()    //超出负荷时的拒绝策略
        );

        for (int i = 0; i < 100; i++) {
            int id = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello " +  id  + " " + Thread.currentThread().getName());
                }
            });
        }
        
        //关闭线程池
        threadPool.shutdown();
    }
}

可以看见,上述自定义线程池这种方式使用起来是比较麻烦的,Java大佬也是又提供了更方便的标准库,对 ThreadPoolExecutor 又进一步封装了。

2. Executors类

Executors 本质上是对 ThreadPoolExecutor 类的封装,它提供了各种静态工厂方法,用于创建不同类型的线程池,以满足不同的需求和场景。

Executors 创建线程池的几种方式:

  • newFixedThreadPool(int nThreads)创建一个固定线程数的线程池,

  • newCachedThreadPool()创建线程数目动态增长的线程池

  • newSingleThreadExecutor()创建一个单线程的线程池,该线程池中只包含一个线程。

  • newScheduledThreadPool(int corePoolSize)创建一个定时执行任务的线程池,该线程池中包含指定数量的核心线程。是进阶版的Timer(定时器)。

这个类的使用比较简单,同样使用 submit 方法提交任务到线程池即可.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

class MyRunnable implements Runnable {
    @Override
    public synchronized void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + ": hello thread");
        }
    }
}

public class Demo1 {
    public static void main(String[] args) {
        //Executors 创建线程的几种方式

        //创建固定线程数的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        //创建线程数目动态增长的线程池
        ExecutorService threadPool1 = Executors.newCachedThreadPool();
        //创建只包含单个线程的线程池
        ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
        //设定延迟时间后执行命令,或定期执行命令,是进阶版的Timer  参数1:核心线程数 参数2:线程工厂(默认值)
        ScheduledExecutorService threadPool3 = Executors.newScheduledThreadPool(10);

        MyRunnable myRun = new MyRunnable();
        for (int i = 0; i < 5; i++) {
            threadPool.submit(myRun);
        }

        threadPool.shutdown();
    }
}

3. 创建线程池的两种方式中应该怎么选择?

使用 Executors 工厂方法的情况:

  1. 简单的线程池需求: 如果你只需要一个简单的线程池来执行任务,而且不需要对线程池的参数进行精细调整,那么可以使用 Executors 提供的工厂方法。例如,执行一些简单的并发任务,不需要对线程池的大小、任务队列等进行特定配置。

  2. 便捷性Executors 提供了许多预定义的线程池类型,使用这些工厂方法可以更加方便地创建标准类型的线程池,而无需手动配置线程池的参数。

自定义线程池的情况:

  1. 特定的线程池需求: 如果你的应用程序对线程池的行为有特定的需求,比如需要配置线程池的大小、任务队列类型、拒绝策略等,就需要自定义线程池。自定义线程池可以满足更复杂的场景需求。

  2. 性能优化: 通过自定义线程池,可以根据具体的应用场景和硬件环境进行性能优化。例如,调整核心线程数、最大线程数、任务队列类型等,以达到更好的并发性能和资源利用率。

  3. 更精细的控制: 自定义线程池可以提供更精细的控制和配置选项,如设置线程池的命名、拒绝策略、线程工厂等,以满足特定的业务需求和管理要求。

总结:

  • 如果只是简单的并发任务执行,或者需要快速创建标准类型的线程池,可以使用 Executors 提供的工厂方法。

  • 如果需要对线程池的行为进行精细控制,或者应用程序对并发性能和资源利用有较高要求,建议自定义线程池以满足特定的需求。

 

三、自己实现线程池

为了更好地理解线程池的执行流程,这里实现一个简单的线程池:

  • 核心操作为 submit,将任务加入到线程池中
  • 要有n个工作线程,使用 Runnable 描述一个任务
  • 使用一个 BlockingQueue 组织所有的任务
  • 每个工作线程要做的事情:不停的从 BlockingQueue 中取任务执行
  • 指定一下线程池中的最大线程数 maxWorkerCount;当 当前线程数达到这个最大值时,就不在新增线程了
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

//带最大线程数和核心线程的线程池
class MyThreadPool2 {

    //阻塞队列
    private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);
    private int maxPoolSize;
    //保存当前的所有线程
    private List<Thread> threadList = new ArrayList<>();

    /**
     * @param corePoolSize 核心线程数
     * @param maxPoolSize  最大线程数
     */
    public MyThreadPool2(int corePoolSize, int maxPoolSize) {
        this.maxPoolSize = maxPoolSize;
        //创建n个线程,并设定每个线程要执行的任务
        for (int i = 0; i < corePoolSize; i++) {
            Thread t = new Thread(() -> {
                try {
                    while (true) {
                        Runnable runnable = queue.take();
                        runnable.run();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }
            });
            t.start();
            threadList.add(t);
        }
    }

    public void submit(Runnable runnable) throws InterruptedException {
        //此处进行判定,判定当前任务数量(任务队列的元素个数)是否较多
        //如果多了,就创建新的线程;如果不多,就不需要
        queue.put(runnable);
        //设置一个任务数量的阈值(自己按需求指定),达到这个阈值,且当前线程数量不为最大线程数,就创建新线程
        if (queue.size() >= threadList.size() * 50 && threadList.size() < maxPoolSize) {
            for (int i = 0; i < maxPoolSize - threadList.size(); i++) {
                Thread t = new Thread(() -> {
                    try {
                        while (true) {
                            Runnable task = queue.take();
                            task.run();
                        }
                    } catch (InterruptedException e) {
                        throw new RuntimeException();
                    }
                });
                t.start();
                threadList.add(t);
            }
        }
    }
}

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool2 myThreadPool = new MyThreadPool2(10, 20);
        for (int i = 1; i <= 10000; i++) {
            int id = i;
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello " + id + " " + Thread.currentThread().getName());
                }
            });
        }
    }
}

还有一些地方可以改进和优化:

  1. 线程池的关闭机制:目前线程池没有提供关闭的方法,最好添加一个 shutdown() 方法,用于正确关闭线程池。关闭线程池时,应该停止向队列中添加新任务,并等待已提交的任务执行完成。

  2. 异常处理:线程池中的线程应该能够处理异常情况,例如当任务执行过程中发生异常时,应该能够捕获并处理异常,而不是简单地抛出 RuntimeException

  3. 任务拒绝策略:在任务队列满时,当前实现是通过阻塞 put() 方法来等待队列空闲。但是,当队列满时,可能希望采取一些其他的任务拒绝策略,比如直接抛出异常、丢弃任务等。

对于工作中的日常开发而言,掌握标准库中的线程池的使用即可。 

  • 18
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值