线程池

文章目录

    • 1、线程池的好处?
    • 2、自定义线程池
        • 1、定义拒绝策略接口
        • 2、定义阻塞队列
        • 2、定义线程池
        • 4、测试类
    • 3、线程池的创建与七个重要参数
        • 1、corePoolSize 核心线程数目 (最多保留的线程数)
        • 2、maximumPoolSize 最大线程数目
        • 3、keepAliveTime 生存时间 - 针对救急线程
        • 4、TimeUnit 线程活动保持时间的单位
        • 5、workQueue 阻塞队列
        • 6、ThreadFactory 线程工厂
        • 7、拒绝策略
    • 4、线程池处理任务的流程(工作原理)
    • 5、3种线程池,优劣?
        • 1、newFixedThreadPool
        • 2、newSingleThreadExecutor
        • 3、newCacheThreadPool
        • 4、SchedueledThreadPool Executor
    • 6、线程池execute 和 submit的区别 ?

1、线程池的好处?

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。

2、自定义线程池

线程池_第1张图片

1、定义拒绝策略接口

@FunctionalInterface // 拒绝策略
interface RejectPolicy<T> {
    void reject(BlockingQueue<T> queue, T task);
}

2、定义阻塞队列

@Slf4j(topic = "c.BlockingQueue")
class BlockingQueue<T> {
    /**
     *  1. 任务队列:双向队列
     */
    private Deque<T> queue = new ArrayDeque<>();

    /**
     *  2. 锁:
     *  线程池在从任务队列中获取任务时,只能让一个线程获取任务,其他的线程需要等待获取其他任务
     *  因此需要用锁保护队列头的任务,防止多个线程都获取这个任务
     *  同理如果有多个线程都向这个队列中添加任务,同样存在线程安全问题,因此需要保护队列尾的元素
     */
    private ReentrantLock lock = new ReentrantLock();

    /**
     *  3. 生产者条件变量
     *  对于生产者,当阻塞队列中任务满的时候,需要生产者线程在等待队列上等待
     */
    private Condition fullWaitSet = lock.newCondition();

    /**
     *  4. 消费者条件变量
     *  对于消费者,当阻塞队列中任务为空的时候,需要消费者线程在等待队列中等待
     */
    private Condition emptyWaitSet = lock.newCondition();

    /**
     *  5. 容量:队列的容量
     */
    private int capcity;

    public BlockingQueue(int capcity) {
        this.capcity = capcity;
    }

    /**
     * 阻塞获取:消费者线程
     * 1、在从队列中获取任务时,需要加锁
     * 2、如果队列空时,线程需要在等待队列上等待
     * 3、获取队列头部的元素并返回
     * 4、唤醒阻塞的生产者线程,告诉他队列不满了
     *
     */
    public T take() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                try {
                    emptyWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 阻塞添加:生产者线程
     * 1、向队列中添加任务时需要加锁
     * 2、当队列满时,需要在等待队列上等待
     * 3、向阻塞队列中添加任务
     * 4、唤醒阻塞的消费者线程,告诉他队列不为空了
     *
     */
    public void put(T task) {
        lock.lock();
        try {
            while (queue.size() == capcity) {
                try {
                    log.debug("等待加入任务队列 {} ...", task);
                    fullWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("加入任务队列 {}", task);
            queue.addLast(task);
            emptyWaitSet.signal();
        } finally {
            lock.unlock();
        }
    }

    // 带超时阻塞获取
    public T poll(long timeout, TimeUnit unit) {
        lock.lock();
        try {
            // 将 timeout 统一转换为 纳秒
            long nanos = unit.toNanos(timeout);
            while (queue.isEmpty()) {
                try {
                    // 返回值是剩余时间
                    if (nanos <= 0) {
                        return null;
                    }
                    nanos = emptyWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }

    // 带超时时间阻塞添加
    public boolean offer(T task, long timeout, TimeUnit timeUnit) {
        lock.lock();
        try {
            long nanos = timeUnit.toNanos(timeout);
            while (queue.size() == capcity) {
                try {
                    if(nanos <= 0) {
                        return false;
                    }
                    log.debug("等待加入任务队列 {} ...", task);
                    nanos = fullWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("加入任务队列 {}", task);
            queue.addLast(task);
            emptyWaitSet.signal();
            return true;
        } finally {
            lock.unlock();
        }
    }

    public int size() {
        lock.lock();
        try {
            return queue.size();
        } finally {
            lock.unlock();
        }
    }

    public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
        lock.lock();
        try {
            // 判断队列是否满
            if(queue.size() == capcity) {
                rejectPolicy.reject(this, task);
            } else {  // 有空闲
                log.debug("加入任务队列 {}", task);
                queue.addLast(task);
                emptyWaitSet.signal();
            }
        } finally {
            lock.unlock();
        }
    }
}

2、定义线程池

@Slf4j(topic = "c.ThreadPool")
class ThreadPool {
    // 任务队列
    private BlockingQueue<Runnable> taskQueue;

    // 线程集合
    private HashSet<Worker> workers = new HashSet<>();

    // 核心线程数
    private int coreSize;

    // 获取任务时的超时时间
    private long timeout;

    private TimeUnit timeUnit;

    private RejectPolicy<Runnable> rejectPolicy;

    public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity, RejectPolicy<Runnable> rejectPolicy) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.taskQueue = new BlockingQueue<>(queueCapcity);
        this.rejectPolicy = rejectPolicy;
    }

    // 执行任务
    public void execute(Runnable task) {
        /**
         * 1、当任务数没有超过 coreSize 时,直接交给 worker 对象执行
         * 2、如果任务数超过 coreSize 时,加入任务队列暂存
         */
        synchronized (workers) {
            if(workers.size() < coreSize) {
                Worker worker = new Worker(task);
                log.debug("新增 worker{}, {}", worker, task);
                workers.add(worker);
                worker.start();
            } else {
                //当队列满的时候,但是还有剩余的任务没有执行,那么有哪些选择:
                // 1) 死等
                // 2) 带超时等待
                // 3) 让调用者放弃任务执行
                // 4) 让调用者抛出异常
                // 5) 让调用者自己执行任务
                taskQueue.tryPut(rejectPolicy, task);
            }
        }
    }

    //worker代表一个线程
    class Worker extends Thread{
        private Runnable task;

        public Worker(Runnable task) {
            this.task = task;
        }

        @Override
        public void run(){
            /**
             * 执行任务:
             * 1、当 task 不为空,执行任务:task != null
             * 2、当 task 执行完毕,从任务队列获取任务并执行:(task = taskQueue.take()) != null
             */
            //while(task != null || (task = taskQueue.take()) != null) {
            while(task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
                try {
                    log.debug("正在执行...{}", task);
                    task.run();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    task = null;
                }
            }
            synchronized (workers) {
                log.debug("worker 被移除{}", this);
                workers.remove(this);
            }
        }
    }
}

4、测试类

@Slf4j(topic = "c.TestPool")
public class TestPool {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(1,
                1000, TimeUnit.MILLISECONDS, 1, (queue, task)->{
            //1、死等:queue.put(task);
            //2、带超时等待:queue.offer(task, 1500, TimeUnit.MILLISECONDS);
            //3、让调用者放弃任务执行:log.debug("放弃{}", task);
            //4、让调用者抛出异常:throw new RuntimeException("任务执行失败 " + task);
            //5、让调用者自己执行任务
            task.run();
        });
        for (int i = 0; i < 4; i++) {
            int j = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("{}", j);
            });
        }
    }
}

3、线程池的创建与七个重要参数

可以通过ThreadPoolExecutor来创建一个线程池:

public ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler)

1、corePoolSize 核心线程数目 (最多保留的线程数)

当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。

线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程

核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。

如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉

2、maximumPoolSize 最大线程数目

线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。

3、keepAliveTime 生存时间 - 针对救急线程

该线程池中救急线程闲置超时时长

一个救急线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉

如果设置allowCoreThreadTimeOut = true,则会作用于核心线程

4、TimeUnit 线程活动保持时间的单位

可选的单位有天、小时、分钟、毫秒、微秒和纳秒

5、workQueue 阻塞队列

用于保存等待执行的任务的阻塞队列

该线程池中的任务队列:维护着等待执行的Runnable对象

当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务

ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则创建救急执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误

LinkedBlockingQueue:一这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize

SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

6、ThreadFactory 线程工厂

可以为线程创建时起个好名字

7、拒绝策略

当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。

AbortPolicy:直接抛出异常。

CallerRunsPolicy:让调用者运行任务

DiscardOldestPolicy:放弃队列中最早的任务,本任务取而代之

DiscardPolicy:放弃本次任务的执行

4、线程池处理任务的流程(工作原理)

1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。

2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。

3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

线程池_第2张图片

  1. 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
  2. 当线程数达到corePoolSize并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue
    队,直到有空闲的线程。
  3. 如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急。(如果是无界队列,不会出现救急线程)
  4. 如果线程到达 maximumPoolSize仍然有新任务这时会执行拒绝策略。
  5. 当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由
    keepAliveTimeunit 来控制。

5、3种线程池,优劣?

Executor框架最核心的类是ThreadPoolExecutor,它是线程池的实现类。

❑ 通过Executor框架的工具类Executors,可以创建3种类型的ThreadPoolExecutor。

❑ FixedThreadPool。

❑ SingleThreadExecutor。

❑ CachedThreadPool。

1、newFixedThreadPool

FixedThreadPool被称为可重用固定线程数的线程池。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>());
}

FixedThreadPoolcorePoolSizemaximumPoolSize都被设置为创建FixedThreadPool时指定的参数nThreads。当线程池中的线程数大于corePoolSize时,keepAliveTime为救急线程等待新任务的最长时间,超过这个时间后救急线程将被终止。这里把keepAliveTime设置为0L,意味着救急线程会被立即终止。

如图为FixedThreadPool的execute()方法的运行示意图:
线程池_第3张图片

(1) 如果当前运行的线程数小于核心线程数coolPoolSize,那么就会创建新的线程来执行任务

(2) 如果当前运行的线程数等于核心线程数coolPoolSize,那么就会将任务加入LinkedBlockingQueue

(3) 线程执行完线程池中的任务后,会在循环中反复从LinkedBlockingQueue中取出任务来执行

FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。使用无界队列作为工作队列会对线程池带来如下影响。

  1. maximumPoolSize将是一个无效参数
  2. keepAliveTime将是一个无效参数
  3. 运行中的FixedThreadPool不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution方法)。

特点:

  • 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
  • 阻塞队列是无界的,可以放任意数量的任务
  • 适用于任务量已知,相对耗时的任务

2、newSingleThreadExecutor

面试题:单线程线程池的应用场景?

SingleThreadExecutor是使用单个worker线程的Executor。

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
                                                (new ThreadPoolExecutor(1, 1,
                                                0L, TimeUnit.MILLISECONDS,
                                                new LinkedBlockingQueue<Runnable>()));
}

SingleThreadExecutorcorePoolSize和maximumPoolSize被设置为1。其他参数与FixedThreadPool相同。SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。SingleThreadExecutor使用无界队列作为工作队列对线程池带来的影响与FixedThreadPool相同

线程池_第4张图片
(1) 如果当前运行的线程数少于corePoolSize(即线程池中无运行的线程),则创建一个新线程来执行任务。

(2) 如果当前线程池中有一个运行的线程,将任务加入LinkedBlockingQueue

(3) 线程执行完 (1) 中的任务后,会在一个无限循环中反复从LinkedBlockingQueue获取任务来执行。

**使用场景:**希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。

**区别:**自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一
个线程,保证池的正常工作

Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改,FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法

Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改,对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改

3、newCacheThreadPool

面试题:如果提交一个cpu密集型的任务怎么选取线程池 ?

CachedThreadPool是一个会根据需要创建新线程的线程池。

CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPoolmaximumPool是无界的。这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>());
}

核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着全部都是救急线程(60s 后可以回收) ,救急线程可以无限创建,队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)

整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况。

线程池_第5张图片

(1) 首先执行SynchronousQueue.offer(Runnable task)向同步队列中添加任务。如果当前maximumPool中有救急线程正在执行SynchronousQueue. poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成;否则执行(2)

(2) 当初始maximumPool为空,或者maximumPool中当前没有救急线程时,将没有线程执行SynchronousQueue. poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤 (1) 将失败。此时CachedThreadPool会创建一个新线程执行任务,execute()方法执行完成。

(3) 在步骤 (2) 中新创建的线程将任务执行完后,会执行SynchronousQueue. poll(keepAliveTime,TimeUnit.NANOSECONDS)。这个poll操作会让空闲线程最多在SynchronousQueue中等待60秒钟。如果60秒钟内主线程提交了一个新任务(主线程执行步骤1)),那么这个救急线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于空闲60秒的空闲线程会被终止,因此长时间保持空闲的CachedThreadPool不会使用任何资源。

SynchronousQueue是一个没有容量的阻塞队列。每个插入操作必须等待另一个线程的对应移除操作,反之亦然。CachedThreadPool使用SynchronousQueue,把主线程提交的任务传递给空闲线程执行。

4、SchedueledThreadPool Executor

整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线
程也不会被释放。用来执行延迟或反复执行的任务

ScheduledThreadPoolExecutor会把待调度的任务(ScheduledFutureTask)放到一个DelayQueue中。

DelayQueue封装了一个PriorityQueue,这个PriorityQueue会对队列中的Scheduled-FutureTask进行排序。排序时,time小的排在前面(时间早的任务将被先执行)。如果两个ScheduledFutureTask的time相同,就比较sequenceNumber,sequenceNumber小的排在前面(也就是说,如果两个任务的执行时间相同,那么先提交的任务将被先执行)。

6、线程池execute 和 submit的区别 ?

可以使用两个方法向线程池提交任务,分别为execute()和submit()方法。execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。

threadsPool.execute(new Runnable() {
                    @Override
                    public void run() {
                        //TODO Auto-generated method stub
                    }
                });

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

Future<Object> future = executor.submit(harReturnValuetask);
               try {
                        Object s = future.get();
               } catch (InterruptedException e) {
                            //处理中断异常
               } catch (ExecutionException e) {
                            //处理无法执行任务异常
               } finally {
                            //关闭线程池
                            executor.shutdown();
               }

例如:submit方法:提交任务 task,用返回值 Future 获得任务执行结果

private static void method1(ExecutorService pool) throws InterruptedException, ExecutionException {
        Future<String> future = pool.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                log.debug("running");
                Thread.sleep(1000);
                return "ok";
            }
        });

        log.debug("{}", future.get());
    }

你可能感兴趣的:(Java并发编程)