多线程之线程池使用

常用实现多线程的方式

  1. 继承 Thread
  2. 实现 Runable
  3. 实现 Callable
  4. 使用ExecutorService、Future实现

为什么要用线程池

  1. 线程的创建、销毁都是需要消耗资源
  2. 线程创建的太多,也会导致过度消耗内存,切换时也会浪费大量时间
  3. 线程池有队列,可以存储待执行的任务
  4. 线程池可以统一管理线程
  5. 线程复用,还可以控制最大并发数

JUC

Jdk1.5之后加入了JUC,也就是java.util.concurrent,用官方的解释就是 ‘并发编程中通常有用的实用程序类’ 。java线程池是通过Executor框架实现的,先看Executor和Executors
在这里插入图片描述

Executors

先看Executors它里面有这些方法
在这里插入图片描述
其中常用的线程池有
newSingleThreadExecutor 、
newFixedThreadExecutor、
newCachedThreadExecutor、
newScheduledThreadPool
这四种有兴趣的同学可以看一下底层源码,但我们只需要弄清他的原理不建议使用(原因看阿里巴巴编程规范),直击底层会发现这些线程池最终都是调用
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit,BlockingQueue workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
这是ThreadPoolExecutor类的构造方法,记住里面的七个参数,后面会详细注明每个参数的含义及运行的原理。而ThreadPoolExecutor类的顶级接口就是Executor,也就是我们接下来要说的。

Executor

Executor是线程池的顶级接口,但他却不是线程池,只是个执行线程的工具。我们先看看官方API文档怎么说的,
我们已知的子接口:
ExecutorService, ScheduledExecutorService
所有已知的实现类:
AbstractExecutorService,
ForkJoinPool(将大型任务分解为小任务的线程池),
ScheduledThreadPoolExecutor(支持定时调度的线程池),
ThreadPoolExecutor。
今天我们的主讲ThreadPoolExecutor。。
在这里插入图片描述

线程池的组成

线程池有四个部分组成:

  1. 线程池管理器,用于创建并管理线程
  2. 工作线程,线程池内的线程
  3. 任务接口:每个任务需实现的接口,永无线程调度及运行
  4. 任务队列:存放待处理的任务,提供缓存

线程池的状态

	private static final int COUNT_BITS = Integer.SIZE - 3;//workerCount所占位数
	private static final int CAPACITY   = (1 << COUNT_BITS) - 1;//workerCount上线
    // runState is stored in the high-order bits
    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,即将进入终结
    private static final int TERMINATED =  3 << COUNT_BITS;//终结状态

ThreadPoolExecutor

我们看ThreadPoolExecutor类里面有四个构造方法,主要看
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit,BlockingQueue workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
我们先来看看这个构造方法里面的七个参数的详细解释:

  1. corePoolSize: 核心线程数,指保留的线程池中线程数量的大小(不超过maximumPoolSize值 时,线程池中最多有corePoolSize 个线程工作)。
  2. maximumPoolSize : 指的是线程池的最多能拥有的线程数量(超过这个数量,任务将会存放指定队列)
  3. keepAliveTime : 指的是空闲线程结束的超时时间(超过corePoolSize部分的线程不工作时,过keepAliveTime 时间将停止该线程)。
  4. unit: 是一个枚举,表示 keepAliveTime 的单位(有NANOSECONDS, MICROSECONDS,MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS,7个可选值)。
  5. workQueue : 表示存放任务的队列(存放需要被线程池执行的线程队列)。
  6. threadFactory: 执行程序创建新线程时使用的工厂。
  7. handler: 拒绝策略,由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。

线程池工作过程

线程池是如何工作的呢,我们来好好整整ThreadPoolExecutor工作过程及七个参数何时用怎么用。
在这里插入图片描述

线程池刚创建是没有线程的,新任务到来不会立马启动,只有当调用execute() 方法添加一个任务时,线程池就会判断核心线程数corePoolSize是否已经达最大值,没有则创建线程执行任务,大于corePoolSize时就将任务塞到队列(workQueue )等待执行,当队列被塞满,会再次创建非核心线程执行任务,直到线程数达到maximumPoolSize,再进入的任务则会被拒绝策略 RejectedExecutionHandler 处理。线程会不断从队列中获取任务执行直到队列为空,有线程超过keepAliveTime 时间未工作则回收,直到只剩核心线程数工作。
举个生活例子解释,银行总共有10个窗口(maximumPoolSize),大厅有座位(workQueue ),银行平时固定开5个窗口(corePoolSize)办事,当5个窗口都被占满时,剩余的客人就只能在座位上等待,座位坐满了银行经理会将剩下的5个窗口也安排人办理事情,当10个窗口满了,座位也满了,这时候就让保安(RejectedExecutionHandler )安排后面来的客人。慢慢座位空了,窗口也逐渐开始没人,窗口长时间没人时银行经理就会安排减少窗口,直到只剩固定的五个窗口。

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
       	//工作线程数小于核心线程数,则调用addWorker创建线程工作
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 如果已开启的工作线程数大于或等于核心线程数,则把任务添加入堵塞队列
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // 添加失败,则继续开启工作线程,如果已经开启线程数小于最大线程数,则添加成功,否则,使用指定的拒绝策略拒绝
        else if (!addWorker(command, false))
            reject(command);
    }

	/**
	 * 提交Runnable,指定返回值
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

    /**
     * 提交Runnable,指定返回值
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }

    /**
     * 提交Callable
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

ThreadPoolExecutor参数的设置选择

其实关于参数的选择,ThreadPoolExecutor源码是有介绍的。

线程数大小的选择:
  1. 纯计算的任务多建议线程数为CPU数量或加一;
  2. 任务包含大量IO/网络等待等,线程数 = CPU 核数 × 目标 CPU 利用率 ×(1 + 平均等待时间 / 平均工作时间)
队列的选择:

在这里插入图片描述

  1. ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
  2. LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
  3. SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

这是三个阻塞队列,有兴趣可以了解下java的其他阻塞队列和非阻塞队列

拒绝策略 RejectedExecutionHandler的选择:

在这里插入图片描述

public enum RejectPolicy {

	/** 处理程序遭到拒绝将抛出RejectedExecutionException */
	ABORT(new ThreadPoolExecutor.AbortPolicy()),
	/** 放弃当前任务 */
	DISCARD(new ThreadPoolExecutor.DiscardPolicy()),
	/** 如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程) */
	DISCARD_OLDEST(new ThreadPoolExecutor.DiscardOldestPolicy()),
	/** 由主线程来直接执行 */
	CALLER_RUNS(new ThreadPoolExecutor.CallerRunsPolicy());

	private RejectedExecutionHandler value;

	private RejectPolicy(RejectedExecutionHandler handler) {
		this.value = handler;
	}

	/**
	 * 获取RejectedExecutionHandler枚举值
	 * 
	 * @return RejectedExecutionHandler
	 */
	public RejectedExecutionHandler getValue() {
		return this.value;
	}
}
  1. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
  2. ThreadPoolExecutor.DiscardPolicy:丢弃无法处理的任务,也不抛异常。如果允许丢弃任务,这事最好的方案。
  3. ThreadPoolExecutor.DiscardOldestPolicy:也是丢弃任务,只不过是丢弃即将被执行的任务,并提交当前任务
  4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
  5. 若以上策略还不能满足需要,可以自己扩展 RejectedExecutionHandler 接口
工厂(threadFactory)的选择:

在这里插入图片描述
如果不是特别指定就选择默认的工厂,还有一个privilegedThreadFactory,返回用于创建新线程的线程工厂,这些新线程与当前线程具有相同的权限

    public static ThreadFactory defaultThreadFactory() {
        return new DefaultThreadFactory();
    }
    public static ThreadFactory privilegedThreadFactory() {
        return new PrivilegedThreadFactory();
    }

最后

其实想要玩转多线程,还得了解线程的三大特性,可以看看JMM(java内存模型),volatile关键字的作用,还有线程锁、线程调度等等。java api文档下的这三个包里面的类都可以看看
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值