并发编程系列(七):线程池原理

一、线程池
在java中,创建和销毁线程花费的时间和消耗的资源都较大,如果每来一个请求就创建一个线程,可能会导致系统资源的过渡消耗。为了解决该问题,引入了线程池。
通过创建一个线程池子来管理多个线程的使用,当有任务需要处理,则分配给线程池中的线程处理,线程处理完后不会立即销毁,而是等待后续任务。通过对线程的管理,避免大量线程创建的开销

线程池的优势:
1. 降低创建线程和销毁线程的性能开销
2. 提高响应速度,当有新任务需要执行是不需要等待线程创建就可以立马执行
3. 合理的设置线程池大小可以避免因为线程数超过硬件资源瓶颈带来的问题

 

二、java提供的线程池API

1.线程池基本使用

public class Test implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
    static ExecutorService service=Executors.newFixedThreadPool(3);
    public static void main(String[] args) {
        for(int i=0;i<100;i++) {
            service.execute(new Test());
        }
        service.shutdown();
    }
}

2.Java  中提供的线程池  Api
为了方便大家对于线程池的使用,在 Executors 里面提供了几个线程池的工厂方法,这样很多新手就不需要了解太多关于 ThreadPoolExecutor 的知识了,他们只需要直接使用 Executors 的工厂方法,就可以使用线程池:

  • newFixedThreadPool:该方法返回一个固定数量的线程池,线程数不变,当有一个任务提交时,若线程池中空闲,则立即执行,若没有,则会被暂缓在一个任务队列中,等待有空闲的线程去执行。
  • newSingleThreadExecutor: 创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中。
  • newCachedThreadPool:返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若用空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程会在 60 秒后自动回收
  • newScheduledThreadPool: 创建一个可以指定线程的数量的线程池,但是这个线程池还带有延迟和周期性执行任务的功能,类似定时器。

3.ThreadpoolExecutor
问题:请简单说下你知道的线程池和 ThreadpoolExecutor 有哪些构造参数?
上面提到的四种线程池的构建,都是基于 ThreadpoolExecutor 来构建的。

eg.ThreadpoolExecutor最完整构造方法各参数意义

public ThreadPoolExecutor(int corePoolSize, // 核心线程数量
                        int maximumPoolSize, // 最大线程数
                        long keepAliveTime, // 超时时间,超出核心线程数量以外的线程空余存活时间
                        TimeUnit unit, // 存活时间单位
                        BlockingQueue<Runnable> workQueue, // 保存执行任务的队列
                        ThreadFactory threadFactory, // 创建新线程使用的工厂
                        RejectedExecutionHandler handler // 当任务无法执行的时候的处理方式)

1) 核心线程数与最大线程数区别

核心线程表示主要使用的线程,超过核心线程而又小于最大线程数部分的线程相当于临时线程,不会长期维护和使用。两者相当于正式工与临时工的区别。

2) RejectedExecutionHandler

拒绝策略,当超过最大线程数后的线程处理方式。

eg.newFixedThreadPool

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

这个线程池执行流程如下:

1) 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务
2) 线程数等于核心线程数后,将任务加入阻塞队列
3) 由于队列容量非常大,可以一直添加
4) 执行完任务的线程反复去队列中取任务执行
用途:FixedThreadPool 用于负载比较大的服务器,为了资源的合理利用,需要限制当前线程数量.

eg.newCachedThreadPool

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

它的执行流程如下:
1) 没有核心线程,直接向 SynchronousQueue 中提交任务
2) 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个
3) 执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就被回收

eg.newSingleThreadExecutor

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

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

 

三、线程池原理
eg.原理实现流程图

判断当前工作线程数是否大于核心线程,若小于则创建一个核心工作线程并将工作线程数+1;否则将新进的任务加入到阻塞队列中。
创建的核心工作线程会不断的从阻塞队列中取出任务执行,若阻塞队列没有任务,则线程挂起阻塞状态。
阻塞队列如果没满,则将新任务加入队列;否则,新增加工作线程去执行任务(临时工作线程)。
若工作线程超过最大线程数,则不再增加新线程,执行拒绝策略。

 

ctl 的作用
在线程池中,ctl 贯穿在线程池的整个生命周期中
ctl: private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
它是一个原子类,主要作用是用来保存线程数量和线程池的状态。它用到了位运算,一个 int 数值是 32 个 bit 位,这里采用高 3 位来保存运行状态,低 29 位来保存线程数量。

private static final int COUNT_BITS = Integer.SIZE - 3; // 32-3
private static final int CAPACITY = (1 << COUNT_BITS) - 1; //将 1 的二进制向右位移 29 位,再减 1 表示最大线程容量

// 运行状态保存在 int 值的高 3 位 ( 所有数值左移 29 位 )
private static final int RUNNING = -1 << COUNT_BITS; // 接收新任务,并执行队列中的任务
private static final int SHUTDOWN = 0 << COUNT_BITS; // 不接收新任务,但是执行队列中的任务
private static final int STOP = 1 << COUNT_BITS; // 不接收新任务,不执行队列中的任务,中断正在执行中的任务
private static final int TIDYING = 2 << COUNT_BITS; // 所有的任务都已结束,线程数量为 0,处于该状态的线程池即将调用 terminated()方法
private static final int TERMINATED = 3 << COUNT_BITS; // terminated()方法执行完成

eg.状态转化图

Worker类
封装了 firstTask 和 thread 参数。

  • firstTask,线程运行第一次执行的任务
  • thread,worker类真正运行的线程,将worker(实现了Runnable接口)当做参数启动
  • runWorker方法,工作线程执行任务的真正逻辑

 

线程释放

在执行 execute 方法时,如果当前线程池的线程数量超过了 corePoolSize 且小于 maximumPoolSize,并且 workQueue 已满时,则可以增加工作线程,但这时如果超时没有获取到任务,也就是 timedOut 为 true 的情况,说明 workQueue 已经为空了,也就说明了当前线程池中不需要那么多线程来执行任务了,可以把多于 corePoolSize 数量的线程销毁掉,保持线程数量在 corePoolSize 即可。
什么时候会销毁?在 runWorker 方法执行完之后,也就是 Worker 中的 run 方法执行完,由 JVM 自动回收。
getTask 方法返回 null 时,在 runWorker 方法中会跳出 while 循环,然后会执行 processWorkerExit 方法。

 

拒绝策略
如果非核心线程数也达到了最大线程数大小,则根据不同的策略拒绝任务。拒绝策略如下:

  • AbortPolicy:直接抛出异常,默认策略;
  • CallerRunsPolicy:用调用者所在的线程来执行任务;
  • DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
  • DiscardPolicy:直接丢弃任务;

当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。


四、线程池注意事项


五、Future/Callable
线程池的执行任务有两种方法,一种是 submit、一种是 execute。这两个方法是有区别的:

序号executesubmit
1execute 只可以接收一个 Runnable 的参数submit 可以接收 Runable 和 Callable 这两种类型的参数
2execute 如果出现异常会抛出对于 submit 方法,如果传入一个 Callable,可以得到一个 Future 的返回值
3execute 没有返回值submit 方法调用不会抛异常,除非调用 Future.get

Callablee /Future  案例演示
Callable/Future 和 Thread 之类的线程构建最大的区别在于,能够很方便的获取线程执行完以后的结果。

public class CallableDemo implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(3000); // 阻塞案例演示
        return "hello world";
    }
    public static void main(String[] args) throws ExecutionException,
            InterruptedException {
        CallableDemo callableDemo = new CallableDemo();
        FutureTask futureTask = new FutureTask(callableDemo);
        new Thread(futureTask).start();
        System.out.println(futureTask.get());
    }
}

为什么需要使用回调?

因为结果值是由另一线程计算的,当前线程是不知道结果值什么时候计算完成,所以它传递一个回调接口给计算线程,当计算完成时,调用这个回调接口,回传结果值。
这个在很多地方有用到,比如 Dubbo 的异步调用,比如消息中间件的异步通信等等。利用 FutureTask、Callable、Thread 对耗时任务(如查询数据库)做预处理,在需要计算结果之前就启动计算。

 

Callable /Future源码分析

在刚刚实现的 demo 中,我们用到了两个 api,分别是 Callable 和 FutureTask。

Callable

Callable 是一个函数式接口,里面就只有一个 call 方法。子类可以重写这个方法,并且这个方法会有一个返回值

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

FutureTask
FutureTask 的类关系图如下:

它实现 RunnableFuture 接口,那么这个 RunnableFuture 接口的作用是什么呢。

@FunctionalInterface
public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

RunnableFuture 是一个接口,它继承了 Runnable 和 Future 这两个接口,Runnable 比较熟悉了,那么 Future 是什么呢?
Future 表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    // 当前的 Future 是否被取消,返回 true 表示已取消
    boolean isCancelled();
    // 当前 Future 是否已结束。包括运行完成、抛出异常以及取消,都表示当前 Future 已结束
    boolean isDone();
    // 获取 Future 的结果值。如果当前 Future 还没有结束,那么当前线程就等待,
    // 直到 Future 运行结束,那么会唤醒等待结果值的线程的。
    V get() throws InterruptedException, ExecutionException;
    // 获取 Future 的结果值。与 get() 相比较多了允许设置超时时间
    V get(long timeout, TimeUnit unit)
            throws InterruptedException, ExecutionException, TimeoutException;
}

分析到这里我们其实有一些初步的头绪了,FutureTask 是 Runnable 和 Future 的结合,如果我们把 Runnable 比作是生产者,Future 比作是消费者,那么 FutureTask 是被这两者共享的,生产者运行 run 方法计算结果,消费者通过 get 方法获取结果。
作为生产者消费者模式,有一个很重要的机制,就是如果生产者数据还没准备的时候,消费者会被阻塞。当生产者数据准备好了以后会唤醒消费者继续执行。


 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值