线程池基本概念


java中的线程池是运用最多场景最多的并发框架,几乎所有需要异步或者并发执行任务的程序都可以使用线程池。合理的使用线程池可以带来三个好处:

  1. 降低资源消耗,通过重复利用几经创建的线程降低线程创建和销毁带来的性能消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果不能限制的创建既浪费资源又降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

线程

线程池是多个线程的集合体,创建线程的方式有三种:继承Thread、 实现Runnable接口和实现Callable接口。其中Callable可以带有返回值,返回值可以放在FutureTask对象中。

线程的生命周期
在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。
Thread通过new来新建一个线程,这个过程是是初始化一些线程信息,如线程名,id,线程所属group等,可以认为只是个普通的对象。调用Thread的start()后Java虚拟机会为其创建方法调用栈和程序计数器,同时将hasBeenStarted为true,之后调用start方法就会有异常。
处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。当线程获取cpu后,run()方法会被调用。不要自己去调用Thread的run()方法。之后根据CPU的调度在就绪——运行——阻塞间切换,直到run()方法结束或其他方式停止线程,进入dead状态。

Executor框架

java中线程池是通过Excutor框架实现的。
JDK中把线程工作单元和执行机制分离开来,工作单元包括Runnable和Callable,而执行机制由Executor框架提供。在执行机制中可以为不同的任务类型指定和修改执行策略。执行策略中需要考虑任务执行的几个方面:

  • 在什么(what)线程中执行任务?
  • 任务按照什么(what)循序执行(FIFO、LIFO、优先级)?
  • 有多少个任务能并发执行
  • 在队列中有多少个任务在等待执行
  • 如果系统由于过载而需要拒绝一个任务那么应该选择哪一个任务?另外,如何通知应用程序有任务被拒绝了
  • 在执行一个任务之前或者之后,应该进行哪些动作?

在JVM的线程模型中,java线程被一对一的映射为本地操作系统线程,java线程启动是创建一个本地操作系统线程,当线程终止时这个操作系统线程也被回收,操作系统会调度素有线程并将它们分配给可用的CPU。Executor框架的使用是将这些java应用程序线程映射为固定数量的线程,在底层操作系统内核将这些线程映射到硬件处理器上。Executor框架控制上层调度;而下层调度有操作系统内核控制,操作系统的调度不再收应用程序的控制。
Executor框架如何调度如图:

Executor框架的结构:主要有四部分组成:

  • 任务。包括被执行任务需要实现的接口:Runnable和Callable。
  • 工厂类。Executors,提供工厂方法创建线程池。
  • 任务的执行。包括任务执行机制的核心接口Excutor,以及继承自Excutor的ExcutorService接口。Excutor框架有两个关键类实现了ExcutorService接口(ThreadPoolExecutor和ScheduledThreadPoolExcutor)。
  • 异步计算的结果。包括接口Future和实现了Future接口的FutureTask类。

Excutor框架结构
Executor框架的使用示意图:

(1)Executor框架的基础为Executor接口

public interface Executor {
    void execute(Runnable command);
}

接口中只有一个方法,但是它确实异步任务执行的基础。


(2) ExecutorService: 增加Executor的行为,是Executor实现类的最直接接口。定义了线程池的行为。

(3).Executor框架提供的线程池有ThreadPoolExecutor、ScheduledThreadPoolExecutor.两种类型

ThreadPoolExecutor是我们使用最频繁的线程池。在使用Executors工厂来创建时,可以创建三种ThreadPoolExecutor类型的线程池: newFixedThreadPool、newSingleThreadExecutor、newCachedThreadPool。

  • newFixedThreadPool:将创建一个固定长度的线程池,每当提交一个任务是就创建一个线程,知道达到线程吃的最大数量。若任务数量大于 poolSize ,任务会被放在一个 queue 里顺序执行。
  • newSingleThreadExecutor是一个单线程的Executor,他创建单个工作者线程来职系那个任务,如果这个线程异常结束,会创建另一个线程来替代newSingleThreadExecutor能确保依照任务在队列中的顺序来串行执行。
  • newCachedThreadPool将创建一个可缓存的线程池,如果线程池的当前规模超过了处理请求是,将回收空闲的线程,而当需求增加时,则可以添加新的线程,线程吃的规模不存在任何限制。

对于这种ThreadPoolExecutor类型的线程池我们一般还是手动创建,自己去控制参数,更加安全。

ExecutorService threadPoolExecutor = new ThreadPoolExecutor(2, 2, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable r, java.util.concurrent.ThreadPoolExecutor executor) {
            System.out.println("自定义拒绝策略");
            throw new RejectedExecutionException("自定义拒绝策略");
        }
    });

ScheduledThreadPoolExecutor 通常使用工厂类Executors来创建。

  • newScheduledThreadPool创建一个固定长度的线程池,包含多个线程的ScheduledThreadPoolExecutor,而且以延迟或定时的方式来执行任务。创建一个定时调度池,这个调度池主要是以时间间隔调度为主。如果要创建调度池则使用ScheduledExecutorService接口完成,该接口之中包含有如下的两个方法:
//延迟启动
public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
//间隔调度
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit);
  • newSingleThreadScheduledExecutor只包含一个线程的ScheduledThreadPoolExecutor。

(4)、携带结果的任务Callable和.Future接口
Callable线程提交给ThreadPoolExecutor或者ScheduledThreadPoolExecutor线程池时,submit(…)可以返回一个FutureTask对象的结果.FutureTask实现了Future接口,可以通过futureTask.get()获取返回的结果,如果还没有完成主线程将一直阻塞;如果已经取消则会抛出CancellationException;如果任务执行过程中抛出异常get将异常封装在ExecutionException并重新抛出,当抛出此异常时可以通过getCause来获取被封装的初始异常。futureTask.cancle()取消线程。

线程池原理

一个线程池包括以下四个基本组成部分:
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务;
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Runnable–Callable):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(BlockingQueue):用于存放没有处理的任务。提供一种缓冲机制。

当调用 execute() 方法添加一个任务时,线程池会做如下判断:

  1. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务,如果没有则创建新的线程(此时需要获得全局锁)。
  2. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;BlockingQueue.
  3. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;(此时需要获得全局锁)
  4. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会执行拒绝策略。
  5. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  6. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

线程池拒绝策略:

  • AbortPolicy:默认策略,直接抛出RejectExecutionException异常
  • CallerRunsPolicy:使用调用者的线程来处理
  • DiscardPolicy:直接丢掉这个任务
  • DiscardOldestPolicy:丢掉最老的任务

配置线程池的注意事项:
在使用线程池时需注意线程池大小与性能的关系,注意并发风险、死锁、资源不足和线程泄漏等问题。

  • 线程池大小。多线程应用并非线程越多越好,需要根据系统运行的软硬件环境以及应用本身的特点决定线程池的大小。一般来说,如果代 码结构合理的话,线程数目与CPU 数量相适合即可。如果线程运行时可能出现阻塞现象,可相应增加池的大小;如有必要可采用自适应算 法来动态调整线程池的大小,以提高CPU 的有效利用率和系统的整体性能;
  • 并发错误。多线程应用要特别注意并发错误,要从逻辑上保证程序的正确性,注意避免死锁现象的发生。
  • 线程泄漏。这是线程池应用中一个严重的问题,当任务执行完毕而线程没能返回池中就会发生线程泄漏现象。

程池操作

Executor流程:创建–>提交–>执行–>结果
ExecutorService threadPoolExecutor = new ThreadPoolExecutor(2, 2, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), RejectedExecutionHandler handler)。

一、线程池提交
线程池提交有两种execute(Runnable command)和submit()

//submit三种形式
 Future<T> submit(Callable<T> task); //future.get()=T
 Future<T> submit(Runnable task, T result);//future.get()=T
 Future<?> submit(Runnable task); //future.get()=null

虽然submit有三种提交但是归根到底还是通过Executor中的execute(<? extend Runnable> runnable)
submit源码:

 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;
    }

二、线程取消
通过返回的FutureTask来取消。
线程取消时间点:

cancle源码:

public boolean cancel(boolean mayInterruptIfRunning) {
    if (!(state == NEW &&
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
              mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {    // in case call to interrupt throws exception
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                if (t != null)
                    t.interrupt();
            } finally { // final state
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        finishCompletion();
    }
    return true;
}

private void finishCompletion() {
    // assert state > COMPLETING;
    for (WaitNode q; (q = waiters) != null;) {
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            for (;;) {
                Thread t = q.thread;
                if (t != null) {
                    q.thread = null;
                    LockSupport.unpark(t);
                }
                WaitNode next = q.next;
                if (next == null)
                    break;
                q.next = null; // unlink to help gc
                q = next;
            }
            break;
        }
    }
    done();
    callable = null;        // to reduce footprint
}

三、执行–execute(Runnable command)

public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//如果线程池中运行的线程数量<corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果线程池中运行的线程数量>=corePoolSize,且线程池处于RUNNING状态,且把提交的任务成功放入阻塞队列中,就再次检查线程池的状态,
// 1.如果线程池不是RUNNING状态,且成功从阻塞队列中删除任务,则该任务由当前 RejectedExecutionHandler 处理。
// 2.否则如果线程池中运行的线程数量为0,则通过addWorker(null, false)尝试新建一个线程,新建线程对应的任务为null。
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);
}
// 如果以上两种case不成立,即没能将任务成功放入阻塞队列中,且addWoker新建线程失败,则该任务由当前 RejectedExecutionHandler 处理。
else if (!addWorker(command, false))
reject(command);
}
发布了8 篇原创文章 · 获赞 0 · 访问量 129

猜你喜欢

转载自blog.csdn.net/weixin_43612949/article/details/104821287
今日推荐