JAVA并发编程-8-线程池

上一篇看这里:JAVA并发编程-7-并发容器

一、为什么要使用线程池

1、降低资源的消耗。降低线程创建和销毁的资源消耗;
2、提高响应速度:线程的创建时间为T1,执行时间T2,销毁时间T3,免去T1和T3的时间
3、提高线程的可管理性。

二、手动实现一个线程池

如何实现线程呢?有2个关键点
1、线程必须在池子已经创建好了,并且可以保持住,要有容器保存多个线程;
2、线程还要能够接受外部的任务,运行这个任务。容器保持这个来不及运行的任务.

/**
 * 类说明:自己线程池的实现
 */
public class MyThreadPool2 {
    // 线程池中默认线程的个数为5
    private static int WORK_NUM = 5;
    // 队列默认任务个数为100
    private static int TASK_COUNT = 100;

    // 工作线程组
    private WorkThread[] workThreads;

    // 任务队列,作为一个缓冲
    private final BlockingQueue<Runnable> taskQueue;
    private final int worker_num;//用户在构造这个池,希望的启动的线程数

    // 创建具有默认线程个数的线程池
    public MyThreadPool2() {
        this(WORK_NUM, TASK_COUNT);
    }

    // 创建线程池,worker_num为线程池中工/作线程的个数
    public MyThreadPool2(int worker_num, int taskCount) {
        if (worker_num <= 0) worker_num = WORK_NUM;
        if (taskCount <= 0) taskCount = TASK_COUNT;
        this.worker_num = worker_num;
        taskQueue = new ArrayBlockingQueue<>(taskCount);
        workThreads = new WorkThread[worker_num];
        for (int i = 0; i < worker_num; i++) {
            workThreads[i] = new WorkThread();
            workThreads[i].start();
        }
    }


    // 执行任务,其实只是把任务加入任务队列,什么时候执行有线程池管理器决定
    public void execute(Runnable task) {
        try {
            taskQueue.put(task);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }


    // 销毁线程池,该方法保证在所有任务都完成的情况下才销毁所有线程,否则等待任务完成才销毁
    public void destroy() {
        // 工作线程停止工作,且置为null
        System.out.println("ready close pool.....");
        for (int i = 0; i < worker_num; i++) {
            workThreads[i].stopWorker();
            workThreads[i] = null;//help gc
        }
        taskQueue.clear();// 清空任务队列
    }

    // 覆盖toString方法,返回线程池信息:工作线程个数和已完成任务个数
    @Override
    public String toString() {
        return "WorkThread number:" + worker_num
                + "  wait task number:" + taskQueue.size();
    }

    /**
     * 内部类,工作线程
     */
    private class WorkThread extends Thread {

        @Override
        public void run() {
            Runnable r = null;
            try {
                while (!isInterrupted()) {
                    r = taskQueue.take();
                    if (r != null) {
                        System.out.println(getId() + " ready exec :" + r);
                        r.run();
                    }
                    r = null;//help gc;
                }
            } catch (Exception e) {
                // TODO: handle exception
            }
        }

        public void stopWorker() {
            interrupt();
        }

    }
}

上面类中,在构造方法中,定义好线程容器,并且在其中创建指定数量的线程,定义了任务队列来存放任务。

execute方法就是向任务队列中放入需要执行的任务。

定义了WorkThread作为单个工作线程,它不断的从taskQueue任务队列中去取得任务,然后去运行它。

public class TestMyThreadPool {
    public static void main(String[] args) throws InterruptedException {
        // 创建3个线程的线程池
        MyThreadPool2 t = new MyThreadPool2(3,0);
        t.execute(new MyTask("testA"));
        t.execute(new MyTask("testB"));
        t.execute(new MyTask("testC"));
        t.execute(new MyTask("testD"));
        t.execute(new MyTask("testE"));
        System.out.println(t);
        Thread.sleep(10000);
        t.destroy();// 所有线程都执行完成才destory
        System.out.println(t);
    }

    // 任务类
    static class MyTask implements Runnable {

        private String name;
        private Random r = new Random();

        public MyTask(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        @Override
        public void run() {// 执行任务
            try {
                Thread.sleep(r.nextInt(1000)+2000);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getId()+" sleep InterruptedException:"
                        +Thread.currentThread().isInterrupted());
            }
            System.out.println("任务 " + name + " 完成");
        }
    }
}

我们自己实现的线程池还有很多缺点:
1,线程池在创建时,就创建启动好了线程,如果任务数量小于线程数的时候,会导致线程无意义的等待。我们不能很好的控制线程的数量
2,阻塞队列满了的时候,queue只能被阻塞,线程池应用速度变慢,没有一个有效的机制来处理这种情况

三、JDK中的线程池和工作机制

1、线程池的创建及参数

线程池的创建主要依赖ThreadPoolExecutor类,它是jdk中所有线程池的父类。它的构造参数如下:

  • int corePoolSize :线程池中核心线程数,< corePoolSize ,就会创建新线程,= corePoolSize ,这个任务就会保存到BlockingQueue,如果调用prestartAllCoreThreads()方法就会一次性的启动corePoolSize 个数的线程。
  • int maximumPoolSize, 允许的最大线程数,BlockingQueue也满了,< maximumPoolSize时候就会再次创建新的线程
  • long keepAliveTime, 线程空闲下来后,存活的时间,这个参数只在> corePoolSize才有用
  • TimeUnit unit, 存活时间的单位值
  • BlockingQueue workQueue, 保存任务的阻塞队列
  • ThreadFactory threadFactory, 创建线程的工厂,给新建的线程赋予名字
  • RejectedExecutionHandler handler :饱和策略,包括下面几种

AbortPolicy :直接抛出异常,默认;
CallerRunsPolicy:用调用者所在的线程来执行任务
DiscardOldestPolicy:丢弃阻塞队列里最老的任务,队列里最靠前的任务
DiscardPolicy :当前任务直接丢弃
实现自己的饱和策略,实现RejectedExecutionHandler接口即可

2、提交任务的方法

execute(Runnable command) 不需要返回值
Future submit(Callable task) 需要返回值

3、关闭线程池的方法

shutdownNow():设置线程池的状态,还会尝试停止正在运行或者暂停任务的线程
shutdown()设置线程池的状态,只会中断所有没有执行任务的线程

4、工作机制

来看一段重要的源码:
在这里插入图片描述
如果小于核心线程数,就直接增加任务。否则就尝试将任务放到等待队列中,如果放失败了,就在小于最大线程数的条件下进行新线程创建,如果仍然没有成功,就拒绝任务。

在这里插入图片描述

四、合理配置线程池

线程池的使用也有它的合理性,并不是线程越多就越好,需要根据不同类型的任务来合理的配置线程池。

根据任务的性质来:
计算密集型(CPU),IO密集型,混合型

  • 计算密集型:这类的主要任务有 加密,大数分解,正则……., 这类任务是计算量比较大,比较耗费cpu,应该尽量减少cpu在线程之间的轮转,所以线程数适当小一点,最大推荐:机器的Cpu核心数+1,为什么+1,防止页缺失,这是指当某个线程因为调度原因不得不去等待下一个时间片时,+1的线程可以被执行 (机器的Cpu核心=Runtime.getRuntime().availableProcessors()😉
  • IO密集型:读取文件,数据库连接,网络通讯, 这类任务的特点是线程可能需要长时间等待。线程数适当大一点,可以充分的利用cpu,配置的线程数量可以参考机器的Cpu核心数*2
  • 混合型:尽量拆分,拆分成一个IO密集型和一个计算密集型,但是当IO密集型需要的时间>>计算密集型需要的时间,拆分意义不大。当IO密集型所需时间和计算密集型差不多的时候,是非常有意义的。

队列的选择上,应该使用有界,无界队列可能会导致内存溢出,OOM

五、预定义的线程池

jdk中给我们预定义了一些线程池,来方便特殊情况下的使用。

  • FixedThreadPool
    创建固定线程数量的,适用于负载较重的服务器,使用了无界队列
  • SingleThreadExecutor
    创建单个线程,需要顺序保证执行任务,不会有多个线程活动,使用了无界队列
  • CachedThreadPool
    会根据需要来创建新线程的,执行很多短期异步任务的程序,使用了SynchronousQueue
  • WorkStealingPool(JDK7以后)
    基于ForkJoinPool实现
  • ScheduledThreadPoolExecutor
    需要定期执行周期任务,Timer不建议使用了。
    newSingleThreadScheduledExecutor:只包含一个线程,只需要单个线程执行周期任务,保证顺序的执行各个任务
    newScheduledThreadPool 可以包含多个线程的,线程执行周期任务,适度控制后台线程数量的时候
    方法说明:
    schedule:只执行一次,任务还可以延时执行
    scheduleAtFixedRate:提交固定时间间隔的任务
    scheduleWithFixedDelay:提交固定延时间隔执行的任务

六、Executor框架

Executor的继承关系大体如下:在这里插入图片描述
Executor框架的基本使用流程:在这里插入图片描述

下一篇:JAVA并发编程-9-并发安全

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值