Java并发编程(线程池原理)


线程池的工作原理

  1. 为什么要有线程池?
    Java线程池主要用于管理线程组及其运行状态,以便Java虚拟机更好地利用CPU资源

  2. 什么是线程池?
    线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源。

  3. Java线程池的工作原理为:
    JVM先根据用户的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,在有任务执行完毕后,线程池调度器会发现有可用的线程,进而再次从队列中取出任务并执行。

  4. 线程池的主要的作用:

  • 线程复用
  • 线程资源管理
  • 控制操作系统的最大并发数,以保证系统高效(通过线程资源复用实现)且安全(通过控制最大线程并发数实现)地运行

线程复用

线程复用原理:

在这里插入图片描述

  1. Java中,每个Thread类都有一个start方法。
  2. 执行start方法启动线程时,Java虚拟机会调用该类的run方法。
  3. 查看Thread源代码,在Thread类的run方法中其实调用了Runnable对象的run方法,因此可以继承Thread类,在start方法中不断循环调用传递进来的Runnable对象,程序就会不断执行run方法中的代码。
  4. 以将在循环方法中不断获取的Runnable对象存放在Queue中,当前线程在获取下一个Runnable对象之前可以是阻塞的,这样既能有效控制正在执行的线程个数,也能保证系统中正在等待执行的其他线程有序执行。
  5. 这样就简单实现了一个线程池,达到了线程复用的效果。

线程池的核心组件和核心类

核心组件:
在这里插入图片描述

  1. 线程池管理器:用于创建并管理线程池。
  2. 工作线程:线程池中执行具体任务的线程。
  3. 任务接口:用于定义工作线程的调度和执行策略,只有线程实现了该接口,线程中的任务才能够被线程池调度。
  4. 任务队列:存放待处理的任务,新的任务将会不断被加入队列中,执行完成的任务将被从队列中移除。

核心类:
线程池是通过Executor框架实现的。

Executor框架包括3大部分:

  • 任务。也就是工作单元,包括被执行任务需要实现的接口:Runnable接口或者Callable接口;

  • 任务的执行。也就是把任务分派给多个线程的执行机制,包括Executor接口及继承自Executor接口的ExecutorService接口。

  • 异步计算的结果。包括Future接口及实现了Future接口的FutureTask类。

核心类为:ThreadPoolExecutor类

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

相关参数含义:
在这里插入图片描述

线程池工作流程

线程流程图:
在这里插入图片描述
在线程处于空闲状态的时间超过keepAliveTime时间时,正在运行的线程数量超过corePoolSize,该线程将会被认定为空闲线程并停止。因此在线程池中所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小。

简易流程图:
在这里插入图片描述
详解execute()方法(在线程池核心类ThreadPoolExecutor中):

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        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);
    }

查看源代码:

  1. 传入的Runnable对象为空时,抛出空指针异常
  2. 如果运行的线程少于corePoolSize,启动一个新线程任务。对addWorker的调用会自动检查runState和workerCount,这样可以防止虚假警报的增加线程时,它不应该执行,通过返回false。
  3. 如果任务可以成功排队,那么我们仍然需要来再次检查我们是否应该添加一个线程(因为在上次检查后已有线程已经死亡)或那样自从进入此方法后池就关闭了。所以我们重新检查状态,如果没有线程,如有必要回滚队列停止,或启动一个新线程。
  4. 如果我们不能将任务放入队列,那么我们尝试添加一个新的线程。如果它失败了,我们知道我们被关闭或饱和了因此拒绝这项任务。

线程池拒绝策略

  1. 若线程池中的核心线程数被用完且阻塞队列已排满,则此时线程池的线程资源已耗尽,线程池将没有足够的线程资源执行新的任务。
  2. 为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。
  3. JDK内置的拒绝策略有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy这4种,默认的拒绝策略在ThreadPoolExecutor中作为内部类提供。
  4. 在默认的拒绝策略不能满足应用的需求时,可以自定义拒绝策略。

1.AbortPolicy

ThreadPoolExecutor中AbortPolicy源代码:

    public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an {@code AbortPolicy}.
         */
        public AbortPolicy() { }

        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }

通过源代码可以看到AbortPolicy是直接抛出RejectedExecutionException异常。

2.CallerRunsPolicy

ThreadPoolExecutor中CallerRunsPolicy源代码:

    public static class CallerRunsPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code CallerRunsPolicy}.
         */
        public CallerRunsPolicy() { }

        /**
         * Executes task r in the caller's thread, unless the executor
         * has been shut down, in which case the task is discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();//执行被丢弃的任务r
            }
        }
    }

如果被丢弃的线程任务未关闭,则执行该线程任务。注意,CallerRunsPolicy拒绝策略不会真的丢弃任务。

3.DiscardOldestPolicy

ThreadPoolExecutor中DiscardOldestPolicy源代码:

    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardOldestPolicy} for the given executor.
         */
        public DiscardOldestPolicy() { }

        /**
         * Obtains and ignores the next task that the executor
         * would otherwise execute, if one is immediately available,
         * and then retries execution of task r, unless the executor
         * is shut down, in which case task r is instead discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();//丢弃线程队列中最后一个线程任务
                e.execute(r);//尝试提交当前任务
            }
        }
    }

移除线程队列中最早的一个线程任务,并尝试提交当前任务。

4.DiscardPolicy

ThreadPoolExecutor中DiscardPolicy源代码:

    public static class DiscardPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardPolicy}.
         */
        public DiscardPolicy() { }

        /**
         * Does nothing, which has the effect of discarding task r.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }

丢弃当前的线程任务而不做任何处理。如果系统允许在资源不足的情况下丢弃部分任务,则这将是保障系统安全、稳定的一种很好的方案。

5.自定义拒绝策略

我们可以看到前面4种拒绝策略都是实现了RejectedExecutionHandler接口,我们自定义拒绝策略也只需实现RejectedExecutionHandler接口即可。
例如:实现一个自定义拒绝策略DiscardOldestNPolicy,该策略根据传入的参数丢弃最老的N个线程,以便在出现异常时释放更多的资源,保障后续线程任务整体、稳定地运行。

public  class  DiscardOldestNPolicy   implements RejectedExecutionHandler {
    private int discardNumber = 5;
    private List<Runnable> discardList = new ArrayList<Runnable>();

    public DiscardOldestNPolicy(int discardNumber) {
        this.discardNumber = discardNumber;
    }

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (e.getQueue().size() > discardNumber) {
            //批量移除线程队列中的discardNumber个线程任务
            e.getQueue().drainTo(discardList, discardNumber);
            //清空discardList列表
            discardList.clear(); 
            if (!e.isShutdown()) {
                //尝试提交当前任务
                e.execute(r); 
            }
        }
    }
}

5种常用的线程池

Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService。然后通过ExecutorService实现Executor接口并执行具体的线程操作,ExecutorService接口有多个实现类可用于创建不同的线程池。
在这里插入图片描述

newCachedThreadPool:可缓存的线程池

  1. newCachedThreadPool用于创建一个缓存的线程池。
  2. 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。
  3. 调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。
  4. 终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。
  5. 创建方式:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

newFixedThreadPool:固定大小的线程池

  1. newFixedThreadPool用于创建一个固定线程数量的线程池,并将线程资源存放在队列中循环使用。
  2. 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。
  3. 在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
  4. 创建方式:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);

newScheduledThreadPool:可做任务调度的线程池

  1. 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
  2. 代码示例:
public class Demo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
        //创建一个延迟3s执行的线程
        scheduledThreadPool.schedule(new Runnable(){
            @Override
            public void run() {
                System.out.println("延迟三秒");
            }
        }, 3, TimeUnit.SECONDS);
        //创建一个延迟1s执行的线程且每3s执行一次线程
        scheduledThreadPool.scheduleAtFixedRate(new Runnable(){
            @Override
            public void run() {
                System.out.println("延迟 1 秒后每三秒执行一次");
            }
        },1,3,TimeUnit.SECONDS);

    }
}

newSingleThreadExecutor:单个线程的线程池

Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!

newWorkStealingPool:足够大小的线程池

  1. JDK1.8新增
  2. newWorkStealingPool创建持有足够线程的线程池来达到快速运算的目的,在内部通过使用多个队列来减少各个线程调度产生的竞争。
  3. 有足够的线程指JDK根据当前线程的运行需求向操作系统申请足够的线程,以保障线程的快速执行,并很大程度地使用系统资源,提高并发计算的效率,省去用户根据CPU资源估算并行度的过程。
    在这里插入图片描述
    想自己定义线程的并发数,则也可以将其作为参数传入
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值