10:java基础-线程

文章目录

一:线程基础面试题

什么是线程?

  • 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,可以使用多线程对进行运算提速。
  • 比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒
  • 线程模型分为KLT模型与ULT模型,JVM使用的KLT模型,Java线程与OS线程保持1:1的映射关系,也就是说有一个java线程也会在操作系统里有一个对应的线程。

协程 (纤程,用户级线程),

  • 协程 (纤程,用户级线程),目的是为了追求最大力度的发挥硬件性能和提升软件的速度,协程基本原理是:在某个点挂起当前的任务,并且保存栈信息,去执行另一个任务;等完成或达到某个条件时,再还原原来的栈信息并继续执行(整个过程线程不需要上下文切换)。
  • Java原生不支持协程,在纯java代码里需要使用协程的话需要引入第三方包,如:quasar

线程在运行生命周期中的状态

  • NEW,新建
  • RUNNABLE,运行
  • BLOCKED,阻塞
  • WAITING,等待
  • TIMED_WAITING,超时等待
  • TERMINATED,终结
    在这里插入图片描述
    在这里插入图片描述

如何创建线程池

不建议使用这种线程池

protected static final ExecutorService threadPool = Executors.newCachedThreadPool();
protected static final ScheduledExecutorService timerThreadPool = Executors.newScheduledThreadPool(5);
  • 1:newFixedThreadPool(int nThreads)
    创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
  • 2:newCachedThreadPool()
    创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
  • 3:newSingleThreadExecutor()
    这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。
  • 4: newScheduledThreadPool(intcorePoolSize)
    创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer

自定义线程池

@Configuration
public class ExecutorServiceConfig {
    private static final ThreadFactory THREAD_FACTORY = new ThreadFactoryBuilder().setNameFormat("analyse-pool-%d").build();

    @Bean
    public ExecutorService myExecutorService() {
        return new ThreadPoolExecutor(10,
                20,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1024),
                THREAD_FACTORY,
                new ThreadPoolExecutor.CallerRunsPolicy());
    }
}

什么时候使用线程池?

  • 单个任务处理时间比较短
  • 需要处理的任务数量很大

因为

  • 如果并发的请求数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁线程,如此一来会大大降低系统的效率。可能出现服务器在为每个请求创建新线程和销毁线程上花费的时间和消耗的系统资源要比处理实际的用户请求的时间和资源更多。

使用线程池的好处?

  • 重用存在的线程,减少线程创建,消亡的开销,提高性能
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资
  • 源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池都有哪几种工作队列

  • ArrayBlockingQueue
    基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则,对元素进行排序。
  • LinkedBlockingQueue
    基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
  • SynchronousQueue
    不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
  • PriorityBlockingQueue
    具有优先级的无限阻塞队列
  • DelayQueue
    DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。

线程池参数?

  • corePoolSize:
    线程池的基本大小,当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。说白了就是,即便是线程池里没有任何任务,也会有corePoolSize个线程在候着等任务。
  • maximumPoolSize:
    最大线程数,不管你提交多少任务,线程池里最多工作线程数就是maximumPoolSize。
  • keepAliveTime:
    线程的存活时间。当线程池里的线程数大于corePoolSize时,如果等了keepAliveTime时长还没有任务可执行,则线程退出。
  • unit:
    这个用来指定keepAliveTime的单位,比如秒:TimeUnit.SECONDS。
  • workQueue:
    用于保存等待执行任务的阻塞队列,提交的任务将会被放到这个队列里。
  • threadFactory:
    线程工厂,用来创建线程。主要是为了给线程起名字,默认工厂的线程名字:pool-1-thread-3。
  • handler:
    拒绝策略,即当线程和队列都已经满了的时候,应该采取什么样的策略来处理新提交的任务。
    默认策略是AbortPolicy(抛出异常),其他的策略还有:
    CallerRunsPolicy(只用调用者所在线程来运行任务)
    DiscardOldestPolicy(丢弃队列里最近的一个任务,并执行当前任务)
    DiscardPolicy(不处理,丢弃掉)

如何设置线程池参数合理

  • 在Java中设置线程池参数需要考虑多个因素,以确保线程池的性能和资源使用最优化。以下是一些关键的参数和考虑因素:
  1. 核心线程数 (corePoolSize)
    定义:线程池中始终保持活跃的线程数量,即使它们处于空闲状态。
    设置依据:
    任务类型:对于CPU密集型任务,通常设置为CPU核心数的1到2倍。对于IO密集型任务,可以设置更高,因为线程会因等待IO操作而阻塞。
    系统资源:考虑系统的其他负载和资源限制。
  2. 最大线程数 (maximumPoolSize)
    定义:线程池允许的最大线程数。
    设置依据:
    任务频率和数量:如果任务频繁且数量大,可能需要更多的线程。
    资源限制:考虑系统的内存和CPU限制,避免创建过多线程导致系统崩溃。
  3. 空闲线程存活时间 (keepAliveTime)
    定义:当线程数超过核心线程数时,这些多余的线程在空闲状态下能存活的时间。
    设置依据:
    任务的执行频率:如果任务执行较为稀疏,可以设置较短的存活时间,以释放不需要的资源。
    系统资源:如果资源紧张,考虑减少存活时间。
  4. 工作队列 (workQueue)
    定义:用于存放等待执行的任务的队列。
    选择类型:
    直接交付队列 (SynchronousQueue):适用于任务处理快速,立即执行的场景。
    有界队列 (ArrayBlockingQueue):适用于任务流量大,需要限制队列大小的场景。
    无界队列 (LinkedBlockingQueue):可能会导致内存溢出,适用于确信任务数量合理的场景。
  5. 线程工厂 (ThreadFactory)
    定义:用于创建新线程的工厂。
    设置依据:
    自定义线程特性:如设置线程名称,设置为守护线程等。
  6. 拒绝策略 (RejectedExecutionHandler)
    定义:当任务无法被线程池执行时的处理策略。
    常用策略:
    ThreadPoolExecutor.AbortPolicy:抛出异常。
    ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
    ThreadPoolExecutor.DiscardPolicy:静默丢弃无法处理的任务。
    ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧)的一个任务,并尝试提交新任务。
  • 实践建议
    监控:定期监控线程池的状态,如队列大小、活跃线程数等,以调整配置。
    性能测试:在生产环境前进行压力测试,以验证线程池的配置是否合理。
    动态调整:根据应用的实际运行情况动态调整线程池参数。
    合理配置线程池是确保Java应用性能和稳定性的关键。每个应用可能需要不同的配置,因此理解每个参数的影响并根据实际情况进行调整非常重要。

线程池任务提交过程

在这里插入图片描述

  • 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  • 如果此时线程池中的数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
  • 如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
  • 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
  • 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

什么是 ThreadLocal?(以下简称 TL)

  • 简单说,ThreadLocal 为每个使用该变量的线程提供独立的变量副本。所以每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本,我们通常把这叫“线程隔离”。

ThreadLocal如何实现线程隔离

  • ThreadLocal在Java中提供了一种线程局部变量的概念,它能够实现线程间的数据隔离。每个使用ThreadLocal存储数据的线程都有自己独立的数据副本,这些数据对其他线程来说是不可见的。这是通过在ThreadLocal对象内部为每个线程维护一个独立的值来实现的。

实现原理

  1. 内部存储:ThreadLocal内部通过一个ThreadLocalMap来存储数据。ThreadLocalMap是ThreadLocal的一个静态内部类,每个Thread对象都包含一个ThreadLocalMap的引用,但这个引用默认是null,只有当线程首次调用ThreadLocal的set或get方法时,才会创建这个线程的ThreadLocalMap。
  2. 键值对存储:在ThreadLocalMap中,每个ThreadLocal实例作为键(key),每个线程的局部变量副本作为值(value)。这样,即使是多个线程访问同一个ThreadLocal实例,由于它们在自己的ThreadLocalMap中维护着不同的值,因此互不影响。
  3. 线程隔离:当线程访问ThreadLocal变量时,实际上是在自己的ThreadLocalMap中进行操作。这意味着每个线程只能看到自己在ThreadLocal变量中设置的值,实现了线程间的数据隔离。

使用示例

public class ThreadLocalExample {
    private static final ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(() -> {
            threadLocalValue.set(1);
            System.out.println(Thread.currentThread().getName() + ": " + threadLocalValue.get());
        }, "Thread A").start();

        new Thread(() -> {
            threadLocalValue.set(2);
            System.out.println(Thread.currentThread().getName() + ": " + threadLocalValue.get());
        }, "Thread B").start();
    }
}

注意事项

  • 虽然ThreadLocal提供了很好的线程隔离能力,但它也可能导致内存泄漏问题。因为ThreadLocalMap的生命周期与线程一样长,如果没有显式地移除ThreadLocal存储的对象,那么即使这个对象已经不再需要,它也不会被垃圾回收,因为ThreadLocalMap持有对它的强引用。因此,使用完ThreadLocal后,应该调用其remove()方法来清理资源。

Thread 类中的start() 和 run() 方法有什么区别?

  • 通过start()方法来启动一个线程,此时线程处于就绪状态,可以被JVM来调度执行,在调度过程中,JVM通过调用线程类的run()方法来完成实际的业务逻辑,当run()方法结束后,此线程就会终止,所以通过start()方法可以达到多线程的目的。
  • 如果直接调用线程类的run()方法,会被当做一个普通的函数调用,程序中仍然只有主线程这一个线程,即start()方法呢能够异步的调用run()方法,但是直接调用run()方法确实同步的,无法达到多线程的目的。

如何在Java中实现线程?

  • 一是实现Runnable接口,然后将它传递给Thread的构造函数,创建一个Thread对象;
  • 二是直接继承Thread类

如何避免死锁?

  • Java多线程中的死锁
    死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,
  • 死锁的发生必须满足四个条件:
    互斥条件:
    一个资源每次只能被一个进程使用。
    请求与保持条件:
    一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    不剥夺条件:
    进程已获得的资源,在末使用完之前,不能强行剥夺。
    循环等待条件:
    若干进程之间形成一种头尾相接的循环等待资源关系。

避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。

sleep方法和wait方法的区别以及共同点

区别

  • sleep()方法和wait()方法是Java中用于线程控制的两个方法,它们有以下区别:
  1. 调用方式:
    sleep()方法是Thread类的静态方法,直接通过Thread.sleep()调用。
    wait()方法是Object类的实例方法,需要在同步代码块或同步方法中调用。
  2. 使用对象:
    sleep()方法会让当前线程暂停执行,但不会释放对象锁。
    wait()方法会让当前线程暂停执行,并释放对象锁,使得其他线程可以进入同步块或方法。
  3. 调用位置:
    sleep()方法通常用于在指定时间内暂停当前线程的执行。
    wait()方法通常用于线程间的协作,等待其他线程改变共享对象的状态。
  4. 异常处理:
    sleep()方法会抛出InterruptedException,需要进行异常处理。
    wait()方法需要在同步代码块中调用,并且需要捕获InterruptedException异常。
  5. 唤醒方式:
    sleep()方法会在指定时间后自动唤醒。
    wait()方法需要通过调用对象的notify()或notifyAll()方法来唤醒等待的线程。

总的来说,sleep()主要用于线程的暂停执行,而wait()主要用于线程间的协作和等待条件的改变。在使用时需要根据具体的需求和场景选择合适的方法。

共同点

  1. 都可以用于线程间的等待:sleep()和wait()都可以让线程暂停执行一段时间,用于控制线程的执行顺序或协调多个线程之间的操作。
  2. 都会抛出InterruptedException:sleep()和wait()方法都会抛出InterruptedException异常,需要在调用时处理这个异常。
  3. 都会暂停当前线程的执行:无论是sleep()还是wait(),都会暂停当前线程的执行,让出CPU资源。

notify()和 notifyAll()有什么区别?

  • 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
  • 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
  • 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

sleep() 和 yield()方法

  • sleep() 和 yield() 都是Java中用于线程同步与调度的两个方法,分别位于 java.lang.Thread 类中。尽管它们都与线程暂停执行有关,但它们的作用机制、应用场景以及效果上有显著差异。以下是两者的详细对比:

sleep()

  • 功能:
    sleep(long millis) 方法允许调用线程在指定的毫秒数内暂停执行。它可以接受一个可选的纳秒参数(long nanos),用于更精确地控制暂停时间。
    sleep() 会使调用它的线程进入阻塞状态,即线程暂时放弃CPU使用权,不参与任何操作,也不响应中断,直到指定的休眠时间过去。
  • 线程状态变化:
    调用 sleep() 后,线程立即从 运行状态 转变为 阻塞状态,并保持此状态直到休眠时间结束,之后线程进入 就绪状态,等待被操作系统调度恢复执行。
  • 优先级与调度:
    sleep() 不考虑线程优先级,当一个线程调用 sleep() 释放CPU后,所有优先级级别的线程都有机会获得CPU时间片。
  • 异常处理:
    sleep() 方法可能会抛出 InterruptedException 异常,表明线程在睡眠期间被中断。编写代码时通常需要捕获或声明此异常。
  • 应用场景:
    常用于模拟延迟、定时任务、等待特定时间间隔等场景,特别是在多线程环境中,sleep() 可以用来协调线程间的执行顺序,防止某个线程过于频繁地占用CPU资源。

yield()

  • 功能:
    yield() 方法是一个暗示性的操作,它提示当前线程愿意主动放弃当前时间片,将CPU执行权暂时让给其他线程。
    但是,这种让步是自愿且非强制的,实际效果取决于操作系统或JVM的线程调度策略。调度器可以选择忽略这个提示,继续让当前线程执行。
  • 线程状态变化:
    调用 yield() 后,线程从 运行状态 直接进入 就绪状态,与其他处于就绪状态的线程一起竞争CPU资源。如果调度器决定让当前线程继续执行,线程可能很快又回到运行状态。
  • 优先级与调度:
    yield() 在实践中往往倾向于让位给相同或更高优先级的线程,但这不是绝对的,具体行为取决于底层调度算法。
    由于其非强制性,yield() 并不能保证低优先级线程能够得到执行机会。
  • 异常处理:
    yield() 方法不会抛出异常,它是一个简单的方法调用,没有复杂的错误处理需求。
  • 应用场景:
    通常用于优化多线程环境中的性能,尤其是在大量线程竞争CPU资源且每个线程工作量相对较小的情况下,yield() 可以作为一种简单的协作机制,减少不必要的线程上下文切换。
    但由于其不确定性,yield() 在实际编程中使用较少,更多时候作为调试工具或在特定场景下尝试提高程序并发性能时使用。

综上所述,sleep() 和 yield() 的主要区别如下:

  • 确定性:sleep() 指定的暂停时间是确定的,线程一定会在指定时间过后恢复;而 yield() 的效果不确定,可能立即恢复也可能长时间不恢复。
  • 阻塞与就绪:sleep() 使线程进入阻塞状态,明确等待一段时间;yield() 只是将线程置于就绪状态,等待重新调度。
  • 中断处理:sleep() 可以响应中断,抛出 InterruptedException;yield() 不涉及中断,无异常处理。
  • 优先级考虑:虽然两者都不严格遵守线程优先级,但 yield() 在实践中可能更倾向于考虑优先级。
  • 使用场景:sleep() 适用于需要精确控制暂停时间、实现定时任务等场景;yield() 适用于希望线程间更平滑地共享CPU资源,但效果不可靠,通常作为优化手段而非核心同步机制。

你对线程优先级的理解是什么?

  • 每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个int变量(从1-10),1代表最低优先级,10代表最高优先级。

特点:

  1. 优先级设置:可以使用setPriority(int priority)方法设置线程的优先级,范围从Thread.MIN_PRIORITY(1)到Thread.MAX_PRIORITY(10)。
  2. 默认优先级:新创建的线程的优先级与创建它的父线程的优先级相同,默认为Thread.NORM_PRIORITY(5)。
  3. 调度策略:线程优先级只是给操作系统一个提示,告诉它应该优先考虑哪些线程。具体的调度策略取决于操作系统的实现。
  4. 不同操作系统:不同操作系统对线程优先级的处理可能有所不同,因此在跨平台开发时要注意线程优先级可能会有差异。
  5. 不保证:Java规范并不保证线程优先级的绝对行为,因此在编写程序时不应过度依赖线程优先级来控制程序的行为。

注意事项:

  • 避免过度依赖:线程优先级应该谨慎使用,过度依赖线程优先级可能导致不可预测的行为,应该尽量避免使用线程优先级来控制程序逻辑。
  • 平衡性能和公平性:线程优先级可以用于平衡性能和公平性,但要注意不要过度使用,以免影响程序的可维护性和可移植性。
  • 优先级继承:子线程的优先级通常会继承父线程的优先级,但也可以通过setPriority()方法单独设置。
  • 优先级调整:在某些情况下,操作系统可能会忽略线程优先级,因此不应该过度依赖线程优先级来控制程序的行为。

总的来说,线程优先级可以在一定程度上影响线程的调度顺序,但在实际开发中应该谨慎使用,避免过度依赖线程优先级来控制程序的行为。

/**
 * @author guisong.zhang
 * @date 2024/4/18 23:27
 * @description 线程优先级
 */
public class ThreadPriorityExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println("Thread 1 running");
        });
        Thread thread2 = new Thread(() -> {
            System.out.println("Thread 2 running");
        });

        // 设置线程优先级
        thread1.setPriority(Thread.MIN_PRIORITY); // 1
        thread2.setPriority(Thread.MAX_PRIORITY); // 10

        // 启动线程
        thread1.start();
        thread2.start();
    }
}

用户线程和守护线程有什么区别?

用户线程(User Thread):

  • 特点:用户线程是程序的主要执行线程,当程序中所有的用户线程都执行完毕时,程序就会退出,不会等待用户线程之外的守护线程。
  • 创建方式:通过new Thread()创建的线程默认是用户线程。
  • 作用:用于执行程序的核心业务逻辑,是程序的主要执行线程。
  • 示例:主线程、业务处理线程等。

守护线程(Daemon Thread):

  • 特点:守护线程是为其他线程提供服务的线程,当所有的用户线程执行完毕后,守护线程会被强制终止,即使它还在执行。
  • 创建方式:通过setDaemon(true)方法将线程设置为守护线程。
  • 作用:用于在后台提供服务或执行一些辅助性任务,如垃圾回收、定时任务等。
  • 示例:垃圾回收线程、定时任务线程等。

区别总结:

  1. 程序退出:当所有的用户线程执行完毕时,程序会退出,不会等待守护线程;而守护线程会随着用户线程的结束而结束。
  2. 执行顺序:用户线程的执行不受守护线程的影响,但守护线程的执行依赖于用户线程。
  3. 作用:用户线程用于执行程序的核心业务逻辑,而守护线程用于提供服务或执行辅助性任务。

代码示例

/**
 * @author guisong.zhang
 * @date 2024/4/18 23:30
 * @description 用户线程与守护线程
 */
public class DaemonThreadExample {
    public static void main(String[] args) {
        Thread userThread = new Thread(() -> {
            System.out.println("User Thread is running");
        });

        Thread daemonThread = new Thread(() -> {
            while (true) {
                System.out.println("Daemon Thread is running");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        daemonThread.setDaemon(true); // 设置为守护线程

        userThread.start();
        daemonThread.start();
    }
}
  • 在这个示例中,userThread是一个用户线程,而daemonThread是一个守护线程。当用户线程执行完毕后,即使守护线程还在执行,程序也会退出。

线程调度策略?

(1) 抢占式调度策略

  • Java运行时系统的线程调度算法是抢占式的 (preemptive)。
  • Java运行时系统支持一种简单的固定优先级的调度算法。如果一个优先级比其他任何处于可运行状态的线程都高的线程进入就绪状态,那么运行时系统就会选择该线程运行。
  • 新的优先级较高的线程抢占(preempt)了其他线程。但是Java运行时系统并不抢占同优先级的线程。
  • 换句话说,Java运行时系统不是分时的(time-slice)。然而,基于Java Thread类的实现系统可能是支持分时的,因此编写代码时不要依赖分时。当系统中的处于就绪状态的线程都具有相同优先级时,线程调度程序采用一种简单的、非抢占式的轮转的调度顺序。

(2) 时间片轮转调度策略

  • 有些系统的线程调度采用时间片轮转(round-robin)调度策略。
  • 这种调度策略是从所有处于就绪状态的线程中选择优先级最高的线程分配一定的CPU时间运行。该时间过后再选择其他线程运行。
  • 只有当线程运行结束、放弃(yield)CPU或由于某种原因进入阻塞状态,低优先级的线程才有机会执行。如果有两个优先级相同的线程都在等待CPU,则调度程序以轮转的方式选择运行的线程。

什么是线程安全和线程不安全?

  • 线程安全(Thread-Safe)和线程不安全(Thread-Unsafe)是指在多线程环墋下对共享资源进行访问时的两种状态:

线程安全(Thread-Safe):

  • 定义:当多个线程同时访问某个共享资源时,如果不需要额外的同步措施或者采取了适当的同步措施,那么这个共享资源的操作就是线程安全的。
  • 特点:线程安全的操作可以保证在多线程环境下,对共享资源的访问不会导致数据不一致或出现意外结果。
  • 实现方式:常见的实现线程安全的方式包括使用同步关键字(synchronized)、使用并发容器(如ConcurrentHashMap、CopyOnWriteArrayList等)、使用Lock接口等。

线程不安全(Thread-Unsafe):

  • 定义:当多个线程同时访问某个共享资源时,如果没有适当的同步措施或者同步措施不正确,可能导致数据不一致或出现意外结果,这种情况就是线程不安全的。
  • 特点:线程不安全的操作可能导致数据竞争、数据错乱、数据丢失等问题,从而影响程序的正确性和稳定性。
  • 常见问题:常见的线程不安全问题包括竞态条件(Race Condition)、死锁(Deadlock)、数据不一致等。

Java 线程在运行的生命周期

  • Java线程在其运行的生命周期中,可以经历以下几种状态的变迁:

新建(New)

当使用 new 关键字创建一个 Thread 对象时,线程处于新建状态。此时,线程对象已经分配了内存空间,包含了线程的属性(如优先级、名称等)以及指向线程执行体(run() 方法)的引用,但线程尚未开始执行。

就绪(Runnable)

当调用线程对象的 start() 方法后,线程进入就绪状态。此时,线程已经完成了初始化,具备了运行条件,但还没有被操作系统选中分配CPU时间片。线程在就绪队列中等待,由Java虚拟机(JVM)的线程调度器根据调度策略选择合适的线程执行。

运行(Running)

当线程获得CPU时间片并开始执行时,线程进入运行状态。此时,线程的 run() 方法正在被处理器执行。同一时刻,单核处理器上只有一个线程处于运行状态,而在多核处理器上则可能有多个线程同时运行。

阻塞(Blocked)

线程因某种原因无法继续执行时,会进入阻塞状态。阻塞的原因多种多样,包括但不限于:

  • 等待同步锁:线程试图访问被其他线程持有的同步监视器(如 synchronized 代码块或方法),需等待锁释放。
  • 等待IO操作完成:进行输入输出操作时,线程可能因等待数据准备好或数据传输完成而阻塞。
  • 等待超时或通知:线程在 Object.wait()、Condition.await()、CountDownLatch.await()、Semaphore.acquire() 等方法调用中等待特定条件满足或被其他线程唤醒。
  • 调用 Thread.sleep() 或 Thread.join():线程主动让出CPU,进入休眠或等待其他线程结束。

等待(Waiting)

有时“阻塞”和“等待”状态会被区分开来,尤其是当提到线程池的管理状态时。在某些上下文中,“等待”状态特指线程处于 Object.wait()、Condition.await() 等方法调用中,不占用CPU且不计入任何锁的等待队列,需要被显式唤醒才能重新竞争锁。
定时等待(Timed Waiting)
类似于“等待”状态,但带有超时限制。线程会在指定的时间过后自动返回到就绪状态,无需其他线程显式唤醒。例如,调用 Thread.sleep() 时传入了超时时间、Object.wait(long timeout)、Condition.await(long time, TimeUnit unit) 等方法。

终止(Terminated)

线程执行完 run() 方法的全部代码,或者因异常导致提前终止,都会进入终止状态。终止后的线程不能再被启动,且线程对象可能被垃圾回收。
总结起来,Java线程在其运行的生命周期中,可能经历的新建、就绪、运行、阻塞、等待、定时等待和终止这些状态,它们之间可以相互转换,形成线程状态图。理解这些状态及其转换有助于更好地设计和调试多线程应用程序。

线程池之ThreadPoolExecutor使用

package executor;

import java.util.concurrent.*;

/**
 * @author guisong.zhang
 * @date 2024/4/21 23:31
 * @description 自定义线程池
 */
public class CustomThreadPool extends ThreadPoolExecutor {

    public CustomThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                            BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    // 可以在这里添加一些自定义的功能,比如在任务执行前后添加日志、监控等

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        // 任务执行前的操作
        System.out.println("Perform beforeExecute() logic");
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        // 任务执行后的操作
        System.out.println("Perform afterExecute() logic");
    }

    @Override
    protected void terminated() {
        super.terminated();
        // 线程池完全终止时的操作
        System.out.println("Perform terminated() logic");
    }

    public static void main(String[] args) {
        CustomThreadPool threadPool = new CustomThreadPool(
                4, // 核心线程数
                10, // 最大线程数
                60, // 空闲线程存活时间
                TimeUnit.SECONDS, // 时间单位
                new LinkedBlockingQueue<>(100), // 任务队列
                new CustomThreadFactory(), // 线程工厂
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );

        // 添加任务到线程池
        for (int i = 0; i < 20; i++) {
            threadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
                try {
                    Thread.sleep(2000); // 模拟任务执行时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        threadPool.shutdown(); // 关闭线程池
    }
}

class CustomThreadFactory implements ThreadFactory {
    private int threadId;
    private String namePrefix = "CustomThread-";

    public CustomThreadFactory() {
        this.threadId = 1;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, namePrefix + threadId);
        System.out.println("Thread created: " + t.getName());
        threadId++;
        return t;
    }
}


在这里插入图片描述

并行和并发有什么区别?

  • 在编程领域中,"并行"和"并发"是两个相关但不同的概念:
  • 并行(Parallelism):指的是系统同时执行多个任务,即多个任务在同一时刻同时进行。在计算机系统中,通常通过多核处理器或者多台计算机来实现并行执行,以提高系统的性能和效率。
  • 并发(Concurrency):指的是系统同时处理多个任务,这些任务可能在同一时间段内交替执行,但并不是同时进行。在并发编程中,任务之间可能会共享资源,需要通过同步机制来确保数据的一致性和正确性。
  • 简而言之,"并行"强调多个任务同时执行,而"并发"强调多个任务交替执行。

线程和进程的区别?

  • 在操作系统中,"线程"和"进程"是两个重要的概念,它们之间的区别如下:
  • 进程(Process):是程序的一次执行过程,是操作系统进行资源分配和调度的基本单位。每个进程都有独立的内存空间,包括代码、数据、堆栈等,进程之间相互独立,需要通过进程间通信来实现数据共享。
  • 线程(Thread):是进程中的一个执行单元,一个进程可以包含多个线程,线程共享进程的资源,包括内存空间、文件描述符等。线程之间可以直接访问共享的数据,因此线程间通信更加方便和高效。
  • 简而言之,进程是操作系统资源分配的基本单位,而线程是进程中的执行单元,线程共享进程的资源。

创建线程有哪几种方式?

①. 继承Thread类创建线程类

  • 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
  • 创建Thread子类的实例,即创建了线程对象。
  • 调用线程对象的start()方法来启动该线程。

②. 通过Runnable接口创建线程类

  • 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  • 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  • 调用线程对象的start()方法来启动该线程。

③. 通过Callable和Future创建线程

  • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
  • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

使用FutureTask对象作为Thread对象的target创建并启动新线程。
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

callable和runnable的区别

Runnable接口:

  • 方法定义:Runnable接口定义了一个无参数无返回值的run()方法。
    @FunctionalInterface
    public interface Runnable {
        void run();
    }
  • 异常处理:run()方法不能抛出受检查的异常(checked exceptions)。
  • 用途:当你的任务不需要返回结果,并且不需要抛出受检查的异常时,可以使用Runnable接口。
  • Runnable示例:
Runnable task = () -> {
    System.out.println("Running a task with Runnable");
};
new Thread(task).start();

Callable接口:

  • 方法定义:Callable接口定义了一个可以抛出异常的call()方法,并且这个方法有返回值。
    @FunctionalInterface
    public interface Callable<V> {
        V call() throws Exception;
    }
  • 异常处理:call()方法可以抛出异常,包括受检查的异常(checked exceptions)。
  • 用途:当你的任务需要返回执行结果,或者需要抛出受检查的异常时,可以使用Callable接口。
  • Callable示例:
Callable<Integer> task = () -> {
    return 123;
};
ExecutorService executor = Executors.newCachedThreadPool();
Future<Integer> future = executor.submit(task);
// 使用future.get()来获取返回值

使用场景:

  • Runnable:适用于简单的并发任务,不需要返回值,也不抛出受检查的异常。例如,启动一个新线程来执行一些后台任务,如日志记录、监控状态等。
  • Callable:适用于需要返回结果的复杂任务,或者那些可能需要处理异常的任务。Callable通常与Future一起使用,以便可以查询任务的状态、获取返回值或取消任务。

总结来说,Callable提供了比Runnable更丰富的功能,包括支持返回值和能够抛出异常,这使得Callable在执行复杂任务时更为灵活和强大。

什么是FutureTask?

  • FutureTask是Java并发包中的一个实用类,实现了RunnableFuture接口,该接口继承自Runnable和Future接口。FutureTask类可以包装Callable或Runnable对象,
  • 因此它既可以作为一个任务执行,也可以用来获取执行结果。

主要特点:

  1. 获取结果:FutureTask提供了一种异步执行机制,允许在任务完成后获取其执行结果或状态。
  2. 可取消:任务一旦开始执行,可以通过cancel()方法取消执行。
  3. 状态查询:可以查询任务是否已完成(isDone())或是否被取消(isCancelled())。
  4. 一次性:FutureTask的执行只能进行一次,任务执行完毕后,再次调用run()方法将不会执行任务。

使用场景:

  • 当需要执行一个计算密集型任务,并且想要在计算完成后获取结果时。
  • 当需要执行一个可能需要取消的长时间运行任务时。

代码示例

import java.util.concurrent.*;

public class FutureTaskExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 使用Callable创建任务
        Callable<Integer> callableTask = () -> {
            TimeUnit.SECONDS.sleep(2); // 模拟耗时操作
            return 123;
        };

        // 将Callable包装成FutureTask
        FutureTask<Integer> futureTask = new FutureTask<>(callableTask);

        // 启动线程执行任务
        Thread thread = new Thread(futureTask);
        thread.start();

        // 在需要结果之前可以做一些其他操作

        // 获取执行结果,如果任务还没完成,get()将阻塞直到任务完成
        Integer result = futureTask.get();
        System.out.println("Result: " + result);
    }
}
  • 在这个示例中,FutureTask包装了一个Callable任务,然后通过一个线程执行。主线程可以继续执行其他任务,并在适当的时候通过调用get()方法来获取执行结果,如果此时任务还没有完成,get()方法会阻塞直到任务完成。
  • FutureTask是并发编程中非常有用的一个工具类,它简化了线程间的协作和结果获取过程。

Java线程池中submit() 和 execute()方法有什么区别

  • submit()和execute()方法都是用于向线程池提交任务的方法,但它们之间存在一些关键的区别:

execute()方法:

  • 定义:execute()方法定义在Executor接口中,它接受一个Runnable对象作为参数。
  • 返回值:execute()方法没有返回值。
  • 异常处理:如果在执行任务时抛出了异常,那么异常会被Thread的UncaughtExceptionHandler捕获,如果没有设置UncaughtExceptionHandler,那么异常会被忽略,并且任务会失败。
  • 用途:适用于当你不关心任务结果,只想异步执行一个任务。
public interface Executor {
    void execute(Runnable command);
}

submit()方法:

  • 定义:submit()方法定义在ExecutorService接口中,是Executor接口的扩展。它可以接受Runnable或Callable对象作为参数。Callable任务允许有返回值。
  • 返回值:submit()方法返回一个Future对象,通过这个Future对象可以检查任务是否执行完成,并且可以获取Callable任务的返回值。对于Runnable任务,Future.get()会返回null。
  • 异常处理:如果任务在执行过程中抛出了异常,这个异常会被封装在返回的Future对象中。当调用Future.get()方法时,可以通过捕获ExecutionException来处理异常。
  • 用途:适用于当你需要获取异步任务的结果时。
public interface ExecutorService extends Executor {
    void shutdown();
    
    List<Runnable> shutdownNow();
    
    boolean isShutdown();
    
    boolean isTerminated();
    
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

    <T> Future<T> submit(Callable<T> task);

    <T> Future<T> submit(Runnable task, T result);

    Future<?> submit(Runnable task);

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
        
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)  throws InterruptedException, ExecutionException, TimeoutException;
}

总结:

  • execute()方法适用于简单的异步任务执行,不需要关心任务结果。
  • submit()方法提供了更灵活的功能,允许处理有返回值的任务,并且可以通过返回的Future对象来管理任务状态,包括取消任务和获取任务执行结果或异常。

选择哪一个方法主要取决于你的具体需求,是否需要关心任务的执行结果和如何处理任务执行中的异常。

线程安全在三个方面体现:

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
  • 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。

如何使线程按照我们想要的顺序执行

1. 使用join()方法

这是最直接的方法,通过在代码中适当位置调用线程的join()方法,可以让当前线程等待另一个线程执行完成后再继续执行。

public class OrderedExecution {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> System.out.println("Thread 1 is executing"));
        Thread thread2 = new Thread(() -> {
            try {
                thread1.join(); // 确保线程1先执行
                System.out.println("Thread 2 is executing");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread thread3 = new Thread(() -> {
            try {
                thread2.join(); // 确保线程2先执行
                System.out.println("Thread 3 is executing");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        thread1.start();
        thread2.start();
        thread3.start();

        // 主线程等待所有线程完成
        thread1.join();
        thread2.join();
        thread3.join();
    }
}

2. 使用CountDownLatch

CountDownLatch是一个同步辅助类,它允许一个或多个线程等待其他线程完成一系列操作后再继续执行。

import java.util.concurrent.CountDownLatch;

public class OrderedExecutionWithLatch {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);

        Thread thread1 = new Thread(() -> {
            System.out.println("Thread 1 is executing");
            latch.countDown(); // 完成计数减1
        });

        Thread thread2 = new Thread(() -> {
            try {
                latch.await(); // 等待计数器归零
                System.out.println("Thread 2 is executing");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread thread3 = new Thread(() -> {
            try {
                latch.await(); // 等待计数器归零
                System.out.println("Thread 3 is executing");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        thread1.start();
        thread2.start();
        thread3.start();

        thread1.join();
        thread2.join();
        thread3.join();
    }
}

3. 使用CyclicBarrier

CyclicBarrier是另一种同步工具,它允许一组线程相互等待,直到到达某个屏障点,然后所有线程一起继续执行。

4. 通过同步代码块或方法

利用对象锁(synchronized关键字)确保线程按顺序访问共享资源,但这种方法更适用于控制对资源的访问,而不是严格控制执行顺序。

  • 注意
    上述方法能够帮助控制线程的执行顺序,但实际应用中需根据具体需求选择合适的方法。
    线程的调度最终还是依赖于JVM和操作系统,因此在高度竞争的环境下,可能会出现轻微的顺序偏差。
    使用join()方法是最直观且简单的方式,尤其适合简单的线程顺序控制场景。

线程池中 阻塞队列是如何唤醒线程去获取队列中的任务

  • 在Java中,阻塞队列的阻塞与唤醒机制主要依赖于锁(Lock)和条件变量(Condition)。以 LinkedBlockingQueue 为例,这是一个常用的阻塞队列实现,我们可以详细探讨其内部如何实现线程的阻塞与唤醒。
  • 内部结构
    LinkedBlockingQueue 使用了一个内部锁(ReentrantLock)和两个条件变量(Condition):
    notEmpty: 当队列为空时,试图从队列取元素的线程将在这个条件上等待。
    notFull: 当队列满时,试图往队列添加元素的线程将在这个条件上等待。
  • 添加元素(put方法)
    当一个线程尝试添加一个元素到队列中时,它会执行以下步骤:
  1. 获取锁:线程首先获取锁,以确保在给定时间内只有一个线程可以修改队列。
  2. 检查队列状态:线程检查队列是否已满。如果队列已满,线程将在 notFull 条件上等待,直到其他线程从队列中取走元素并发出信号。
  3. 添加元素:一旦队列不满,线程将元素添加到队列的尾部。
  4. 发出信号:添加元素后,线程将调用 notEmpty.signal() 来通知可能正在 notEmpty 条件上等待的线程(这些线程是因为队列为空而阻塞的线程)。
  5. 释放锁:操作完成后,线程释放锁。
  • 取出元素(take方法)
    当一个线程尝试从队列中取出一个元素时,它会执行以下步骤:
  1. 获取锁:线程首先获取锁,确保在给定时间内只有一个线程可以访问队列。
  2. 检查队列状态:线程检查队列是否为空。如果队列为空,线程将在 notEmpty 条件上等待,直到其他线程向队列中添加新元素并发出信号。
  3. 取出元素:一旦队列不为空,线程从队列的头部取出一个元素。
  4. 发出信号:取出元素后,线程将调用 notFull.signal() 来通知可能正在 notFull 条件上等待的线程(这些线程是因为队列已满而阻塞的线程)。
  5. 释放锁:操作完成后,线程释放锁。
  • 总结
    这种使用锁和条件变量的机制允许线程在必要时阻塞,直到它们可以安全地执行操作。通过这种方式,LinkedBlockingQueue 确保了线程安全地访问队列,同时通过条件变量的合理使用,有效地管理了线程的休眠和唤醒,优化了资源的使用和线程的调度。

ScheduledThreadPoolExecutor是怎么实现指定的延时时间后延时执行任务的

ScheduledThreadPoolExecutor 是 Java 中 java.util.concurrent 包提供的一个用于延时或定期执行任务的类,它继承自 ThreadPoolExecutor 并实现了 ScheduledExecutorService 接口。

核心组件

  1. 延时任务队列
    ScheduledThreadPoolExecutor 使用一个优先级队列(DelayedWorkQueue)来存储和管理所有的延时任务。这个队列中的元素是 ScheduledFutureTask 类型,它实现了 RunnableScheduledFuture 接口,该接口扩展了 RunnableFuture 并添加了时间控制的功能。
  2. ScheduledFutureTask
    这个类是 ScheduledThreadPoolExecutor 的核心,它包含了任务的执行时间和周期信息。每个 ScheduledFutureTask 都有一个 time 属性,表示任务下一次应该执行的时间。任务的排序就是基于这个时间来的,时间最小的任务最先被执行。

工作流程

  1. 任务调度
    当你通过 schedule 方法提交一个任务时,会创建一个 ScheduledFutureTask 对象,并将其插入到延时队列中。这个方法需要指定任务的延时时间,这个时间会被用来计算 ScheduledFutureTask 的 time 属性。
  2. 任务执行
    ScheduledThreadPoolExecutor 的工作线程会不断地从延时队列中取出已到期的任务(即当前时间已经超过任务的 time 属性的任务)来执行。如果队列中最近的任务还没到执行时间,线程会等待直到它到期。
  3. 周期性任务处理
    对于周期性任务,ScheduledFutureTask 在每次执行完毕后会重新计算下一次执行的时间,并再次被加入到延时队列中等待执行。

源码角度的关键方法

  • scheduleAtFixedRate 和 scheduleWithFixedDelay 是用于提交周期性任务的两个方法,它们分别对应固定频率和固定延迟的周期性执行策略。
  • delayedExecute 方法用于将一个新的 ScheduledFutureTask 加入到延时队列并在必要时唤醒工作线程。
  • getTask 方法用于从延时队列中获取已到期的任务,如果没有到期的任务,工作线程会等待。
  • 通过这种方式,ScheduledThreadPoolExecutor 能够精确地控制任务的执行时间,无论是单次延时任务还是周期性任务。

周期性提交任务方式

scheduleAtFixedRate

        /**
         * scheduleAtFixedRate:
         * 这个方法保证了两次任务执行之间的间隔时间是固定的。
         * 即使前一次任务执行超时或者出现异常,下一次任务也会在固定的时间间隔后开始,不会因为前一次的延迟而改变。
         * 例如,如果你设置了5秒的间隔,即使上一次任务用了6秒,下一次任务仍然会在5秒后开始。
         */
        executor.scheduleAtFixedRate(() -> {
            System.out.println("scheduleAtFixedRate   --" + LocalDateTime.now() + "--" + Thread.currentThread().getName() + ":send heart beat");
            long startTime = System.currentTimeMillis(), nowTime = startTime;
            while (nowTime - startTime < 5000) {
                nowTime = System.currentTimeMillis();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("scheduleAtFixedRate   --" + LocalDateTime.now() + "--" + Thread.currentThread().getName() + ":heart beat end");
        }, 1, 2, TimeUnit.SECONDS);

scheduleWithFixedDelay

executor.scheduleWithFixedDelay(() -> {
            System.out.println("scheduleWithFixedDelay--" + LocalDateTime.now() + "--" + Thread.currentThread().getName() + ":send heart beat");
            long startTime = System.currentTimeMillis(), nowTime = startTime;
            while (nowTime - startTime < 5000) {
                nowTime = System.currentTimeMillis();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("scheduleWithFixedDelay--" + LocalDateTime.now() + "--" + Thread.currentThread().getName() + ":heart beat end");
        }, 1, 2, TimeUnit.SECONDS);

Timer周期性提交任务

Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println("timer1--" + LocalDateTime.now() + "--" + Thread.currentThread().getName() + ":send heart beat");
                throw new RuntimeException("timer异常--:send heart beat end");
            }
        }, 1000, 2000);

        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println("timer2--" + LocalDateTime.now() + "--" + Thread.currentThread().getName() + ":send heart beat");
            }
        }, 1000, 2000);

几种方式的差异

  • Timer中其中一个抛出异常后,其他的都不能正常执行。
  • ScheduledThreadPoolExecutor即使其中一个线程挂了也不会影响其他的线程执行

延迟队列ScheduledThreadPoolExecutor是如何实现按照时间排序执行的,任务如何排序的

比如以下代码

ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2, Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());

executor.scheduleAtFixedRate(() -> {
            System.out.println("scheduleAtFixedRate   --" + LocalDateTime.now() + "--" + Thread.currentThread().getName() + ":send heart beat");
            long startTime = System.currentTimeMillis(), nowTime = startTime;
            while (nowTime - startTime < 5000) {
                nowTime = System.currentTimeMillis();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("scheduleAtFixedRate   --" + LocalDateTime.now() + "--" + Thread.currentThread().getName() + ":heart beat end");
        }, 1, 2, TimeUnit.SECONDS);

        executor.scheduleAtFixedRate(() -> {
            System.out.println("scheduleAtFixedRate   --" + LocalDateTime.now() + "--" + Thread.currentThread().getName() + ":send heart beat");
            long startTime = System.currentTimeMillis(), nowTime = startTime;
            while (nowTime - startTime < 5000) {
                nowTime = System.currentTimeMillis();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("scheduleAtFixedRate   --" + LocalDateTime.now() + "--" + Thread.currentThread().getName() + ":heart beat end");
        }, 1, 10, TimeUnit.SECONDS);

        executor.scheduleAtFixedRate(() -> {
            System.out.println("scheduleAtFixedRate   --" + LocalDateTime.now() + "--" + Thread.currentThread().getName() + ":send heart beat");
            long startTime = System.currentTimeMillis(), nowTime = startTime;
            while (nowTime - startTime < 5000) {
                nowTime = System.currentTimeMillis();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("scheduleAtFixedRate   --" + LocalDateTime.now() + "--" + Thread.currentThread().getName() + ":heart beat end");
        }, 1, 5, TimeUnit.SECONDS);

如何排序保证时间间隔短的先执行的,如何排序。

实现原理

在这里插入图片描述

  • ScheduledThreadPoolExecutor 使用一个名为 DelayedWorkQueue 的特殊类型的优先级队列来管理和执行定时任务。这个队列是基于时间的优先级排序的,确保最早需要执行的任务能够最先被取出执行。下面详细解释这个队列的实现和任务的排序方式。
    数据结构
  • DelayedWorkQueue 是一个基于堆的数据结构,具体来说是一个最小堆。在这个堆中,元素的排序是根据任务的预定执行时间来确定的。堆的根节点(即堆顶元素)总是最小的元素,对于 DelayedWorkQueue 而言,这意味着堆顶的任务是最接近执行时间的任务。
  • 任务排序
    在 DelayedWorkQueue 中,每个任务都封装在一个 ScheduledFutureTask 对象中。ScheduledFutureTask 实现了 Delayed 接口,这个接口要求实现 getDelay(TimeUnit unit) 方法,该方法返回任务距离其执行时间还剩余的时间。队列使用这个延迟时间来对任务进行排序。
  • 具体实现
  1. 插入任务:当一个新的任务被提交到 ScheduledThreadPoolExecutor 时,它会被封装成一个 ScheduledFutureTask,并根据其预定执行时间插入到 DelayedWorkQueue。插入操作是通过堆的上浮(percolate up)操作完成的,以保持堆的性质。
  2. 取出任务:工作线程从队列中取出任务时,会从堆顶取出最小元素,即最早需要执行的任务。完成取出操作后,堆会进行下沉(percolate down)操作,重新调整以保持堆的性质。
  3. 时间检查:工作线程在尝试取出任务时,如果队列的堆顶任务还未到执行时间,线程会等待直到该任务到达可执行时间。这是通过 getDelay 方法和条件变量等待实现的。

二:线程代码Git地址

三:线程源码相关流程图

四:线程池的执行流程

在这里插入图片描述

五:Executor框架

  • Executor接口是线程池框架中最基础的部分,定义了一个用于执行Runnable的execute方法

继承与实现

在这里插入图片描述

  • 从图中可以看出Executor下有一个重要子接口ExecutorService,其中定义了线程池的具体行为
    1,execute(Runnable command):履行Ruannable类型的任务,
    2,submit(task):可用来提交Callable或Runnable任务,并返回代表此任务的Future对象
    3,shutdown():在完成已提交的任务后封闭办事,不再接管新任务,
    4,shutdownNow():停止所有正在履行的任务并封闭办事。
    5,isTerminated():测试是否所有任务都履行完毕了。
    6,isShutdown():测试是否该ExecutorService已被关闭。

线程池重点属性

ctl

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING,0));
private static final int COUNT_BITS = Integer.SIZE ­ 3;
private static final int CAPACITY = (1 << COUNT_BITS) ­ 1;
  • ctl 是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它包含两部分的信息: 线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),这里可以看到,
  • 使用了Integer类型来保存,高3位保存runState,低29位保存workerCount。
  • COUNT_BITS 就是29,
  • CAPACITY就是1左移29位减1(29个1),这个常量表示workerCount的上限值,大约是5亿。
ctl相关方法
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
  • runStateOf:获取运行状态;
  • workerCountOf:获取活动线程数;
  • ctlOf:获取运行状态和活动线程数的值。

线程池存在5种状态

RUNNING =1 << COUNT_BITS; //高3位为111
SHUTDOWN = 0 << COUNT_BITS; //高3位为000
STOP = 1 << COUNT_BITS; //高3位为001
TIDYING = 2 << COUNT_BITS; //高3位为010
TERMINATED = 3 << COUNT_BITS; //高3位为011

在这里插入图片描述
在这里插入图片描述

1、RUNNING

  • (1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
  • (2) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0

2、 SHUTDOWN

  • (1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
  • (2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。

3、STOP

  • (1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
  • (2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP

4、TIDYING

  • (1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
  • (2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。

5、 TERMINATED

  • (1) 状态说明:线程池彻底终止,就变成TERMINATED状态。
  • (2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING ->TERMINATED
进入TERMINATED的条件如下:
  • 线程池不是RUNNING状态;
  • 线程池状态不是TIDYING状态或TERMINATED状态;
  • 如果线程池状态是SHUTDOWN并且workerQueue为空;
  • workerCount为0;
  • 设置TIDYING状态成功
    在这里插入图片描述

线程池的具体实现

  • ThreadPoolExecutor 默认线程池
  • ScheduledThreadPoolExecutor 定时线程池

ThreadPoolExecutor

  • 线程池的创建
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 1,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());
  • 任务提交
public void execute() //提交任务无返回值
public Future<?> submit() //任务执行完成后有返回值

参数解释

corePoolSize
  • 线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
maximumPoolSize
  • 线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;
keepAliveTime
  • 线程池维护线程所允许的空闲时间。当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;
unit
  • keepAliveTime的单位;
workQueue
  • 用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了如下阻塞队列:
    • 1、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
    • 2、LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;
    • 3、SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;
    • 4、priorityBlockingQuene:具有优先级的无界阻塞队列;
threadFactory
  • 它是ThreadFactory类型的变量,用来创建新线程。默认使用Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。
handler
  • 线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
    1、AbortPolicy:直接抛出异常,默认策略;
    2、CallerRunsPolicy:用调用者所在的线程来执行任务;
    3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
    4、DiscardPolicy:直接丢弃任务;

  • 上面的4种策略都是ThreadPoolExecutor的内部类。

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

线程池监控

public long getTaskCount() //线程池已执行与未执行的任务总数
public long getCompletedTaskCount() //已完成的任务数
public int getPoolSize() //线程池当前的线程数
public int getActiveCount() //线程池中正在执行任务的线程数量

线程池原理

在这里插入图片描述

源码分析部分文字说明,详情见流程图

execute方法

简单来说,在执行execute()方法时如果状态一直是RUNNING时,的执行过程如下:

  1. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务;
  2. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中;
  3. 如 果 workerCount >= corePoolSize && workerCount <maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务;
  4. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常
  • 这里要注意一下addWorker(null, false);,也就是创建一个线程,但并没有传入任务,因为任务已经被添加到workQueue中了,所以worker在执行的时候,会直接从workQueue中获取任务。
  • 所以,在workerCountOf(recheck) == 0时执行addWorker(null, false);也是为了保证线程池在RUNNING状态下必须要有一个线程来执行任务。

execute方法执行流程如下:

在这里插入图片描述

addWorker方法

  • addWorker方法的主要工作是在线程池中创建一个新的线程并执行,
  • firstTask参数 用于指定新增的线程执行的第一个任务,
  • core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,
  • false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize
  • addWorker是线程池管理中的一个关键方法,其主要职责是在遵守线程池配置规则的前提下尝试向线程池中添加一个新的工作线程。以下是该函数功能的详细分解:
  • 状态检查与决策:
    首先,函数通过循环读取并检查线程池的控制状态(ctl),判断线程池是否处于可以接受新任务的状态。如果线程池已经关闭或正在关闭且队列非空或首任务不为空,则直接返回false。
    根据core参数决定是检查核心池大小(corePoolSize)还是最大池大小(maximumPoolSize),以决定是否可以添加更多线程。
  • 线程计数与安全增加:
    内部循环确保在尝试增加工作线程计数之前,检查当前线程数是否已达到预设的上限(核心或最大)。使用compareAndIncrementWorkerCount原子操作尝试增加工作线程计数,以保证线程安全,如果增加失败则重新尝试。
  • 工作线程创建与初始化:
    成功增加线程计数后,尝试创建新的Worker对象,该对象封装了Runnable任务(firstTask)和实际的工作线程。如果线程创建成功(即w.thread != null),则进入下一步。
  • 同步与状态验证:
    获取主锁(mainLock)以确保线程安全地修改线程池状态。在持有锁的情况下,再次检查线程池状态,确保在添加新线程前线程池没有被关闭或正在关闭(除非是关闭状态且首任务为null,允许清理队列)。
  • 工作线程加入池与启动:
    如果一切条件满足,将新创建的Worker添加到工作线程集合中,并更新最大池大小记录。设置workerAdded为true表示成功添加。
    释放锁后,尝试启动新线程,如果启动成功,则设置workerStarted为true。
  • 失败处理:
    在finally块中,如果线程未能启动(!workerStarted),调用addWorkerFailed清理失败的Worker资源,避免资源泄露。
  • 返回结果:
    最终,函数根据workerStarted的值返回true或false,指示是否成功添加并启动了一个新的工作线程。
  • 综上所述,addWorker方法实现了复杂的逻辑来动态调整线程池大小,同时确保了线程池状态的一致性和操作的安全性。

Worker类

  • 线程池中的每一个线程被封装成一个Worker对象,ThreadPool维护的其实就是一组Worker对象,请参见JDK源码。
  • Worker类继承了AQS,并实现了Runnable接口,注意其中的firstTask和thread属性:firstTask用它来保存传入的任务;thread是在调用构造方法时通过ThreadFactory来创建的线程,是用来处理任务的线程。
  • 在调用构造方法时 ,需要把任务传入 ,这 里 通 过getThreadFactory().newThread(this);来新建一个线程,newThread方法传入的参数是this,因为Worker本身继承了Runnable接口,也就是一个线程,所以一个Worker对象在启动的时候会调用Worker类中的run方法。
Worker继承了AQS,使用AQS来实现独占锁的功能。为什么不使用ReentrantLock来实现呢?

可以看到tryAcquire方法,它是不允许重入的,而ReentrantLock是允许重入的:

  1. lock方法一旦获取了独占锁,表示当前线程正在执行任务中;
  2. 如果正在执行任务,则不应该中断线程;
  3. 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断;
  4. 线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;
  5. 之所以设置为不可重入,是因为我们不希望任务在调用像setCorePoolSize这样的线程池控制方法时重新获取锁。如果使用ReentrantLock,它是可重入的,这样如果在任务中调用了如setCorePoolSize这类线程池控制的方法,会中断正在运行的线程。

所以,Worker继承自AQS,用于判断线程是否空闲以及是否可以被中断。

  • 此外,在构造方法中执行了setState(-1);,把state变量设置为-1,为什么这么做呢?是因为AQS中默认的state是0,如果刚创建了一个Worker对象,还没有执行任务时,这时就不应该被中断。
protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
  • tryAcquire方法是根据state是否是0来判断的,所以,setState(-1);将state设置为-1是为了禁止在执行任务前对线程进行中断。
  • 正因为如此,在runWorker方法中会先调用Worker对象的unlock方法将state设置为0

runWorker方法

  1. while循环不断地通过getTask()方法获取任务;
  2. getTask()方法从阻塞队列中取任务;
  3. 如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态;
  4. 调用task.run()执行任务;
  5. 如果task为null则跳出循环,执行processWorkerExit()方法;
  6. runWorker方法执行完毕,也代表着Worker中的run方法执行完毕,销毁线程。
  • 这里的beforeExecute方法和afterExecute方法在ThreadPoolExecutor类中是空的,留给子类来实现
  • completedAbruptly变量来表示在执行任务过程中是否出现了异常,在processWorkerExit方法中会对该变量的值进行判断。

getTask方法

getTask方法用来从阻塞队列中取任务

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

processWorkerExit方法

  • 该函数是用于处理工作线程退出时的清理和记录工作。它被工作线程调用,用于从工作线程集中移除线程,并在适当的情况下终止池或替换工作线程。
  • 如果completedAbruptly为true,则说明工作线程由于用户异常而突然退出,此时需要调整workerCount。
  • 函数首先通过decrementWorkerCount()方法减少workerCount的值。
  • 然后,函数获取主锁并进行一系列操作,包括将完成的任务数加到completedTaskCount上,并从workers集中移除当前工作线程。
  • 接着,函数尝试终止线程池。
  • 如果线程池的状态小于STOP,则会进一步判断是否需要替换工作线程。
  • 如果不是突然退出,并且工作线程数小于等于核心线程数,或者任务队列不为空,则不需要替换工作线程。
  • 最后,如果需要替换工作线程,则调用addWorker(null, false)方法添加一个新的工作线程。
  • processWorkerExit执行完之后,工作线程被销毁,以上就是整个工作线程的生命周期,
  • 从execute方法开始,Worker使用ThreadFactory创建新的工作线程,runWorker通过getTask获取任务,然后执行任务,如果getTask返回null,进入processWorkerExit方法,整个线程结束,如图所示:
    在这里插入图片描述

课程总结

  • 分析了线程的创建,任务的提交,状态的转换以及线程池的关闭;
  • 这 里 通 过 execute 方 法 来 展 开 线 程 池 的 工 作 流 程 , execute 方 法 通 过corePoolSize,maximumPoolSize以及阻塞队列的大小来判断决定传入的任务应该被立即执行,还是应该添加到阻塞队列中,还是应该拒绝任务。
  • 介绍了线程池关闭时的过程,也分析了shutdown方法与getTask方法存在竞态条件;
  • 在获取任务时,要通过线程池的状态来判断应该结束工作线程还是阻塞线程等待新的任务,也解释了为什么关闭线程池时要中断工作线程以及为什么每一个worker都需要lock。

六:ScheduledThreadPoolExecutor

定时线程池类的类结构图

在这里插入图片描述

它用来处理延时任务或定时任务。

在这里插入图片描述

  • 它接收SchduledFutureTask类型的任务,是线程池调度任务的最小单位,有三种提交任务的方式:
  1. schedule
  2. scheduledAtFixedRate
  3. scheduledWithFixedDelay
  • 它采用DelayQueue存储等待的任务
  1. DelayQueue内部封装了一个PriorityQueue,它会根据time的先后时间排序,若time相同则根据sequenceNumber排序;
  2. DelayQueue也是一个无界队列;

SchduledFutureTask

SchduledFutureTask接收的参数(成员变量):

private long time:任务开始的时间
private final long sequenceNumber;:任务的序号
private final long period:任务执行的时间间隔

工作线程的执行过程:

  • 工作线程会从DelayQueue取已经到期的任务去执行;
  • 执行结束后重新设置任务的到期时间,再次放回DelayQueue
  • ScheduledThreadPoolExecutor会把待执行的任务放到工作队列DelayQueue中,DelayQueue封装了一个PriorityQueue,PriorityQueue会对队列中的ScheduledFutureTask进行排序,
  • 首先按照time排序,time小的排在前面,time大的排在后面;
  • 如果time相同,按照sequenceNumber排序,sequenceNumber小的排在前面,sequenceNumber大的排在后面,
  • 换句话说,如果两个task的执行时间相同,优先执行先提交的task

SchduledFutureTask之run方法实现

run方法是调度task的核心,task的执行实际上是run方法的执行。

  1. 如果当前线程池运行状态不可以执行任务,取消该任务,然后直接返回,否则执行步骤2;
  2. 如果不是周期性任务,调用FutureTask中的run方法执行,会设置执行结果,然后直接返回,否则执行步骤3;
  3. 如果是周期性任务,调用FutureTask中的runAndReset方法执行,不会设置执行结果,然后直接返回,否则执行步骤4和步骤5;
  4. 计算下次执行该任务的具体时间;
  5. 重复执行任务

reExecutePeriodic方法

该方法和delayedExecute方法类似,不同的是:

  1. 由于调用reExecutePeriodic方法时已经执行过一次周期性任务了,所以不会reject当前任务;
  2. 传入的任务一定是周期性任务。

线程池任务的提交

  • 首先是schedule方法,该方法是指任务在指定延迟时间到达后触发,只会执行一次。
  • 任务提交方法:delayedExecute

DelayedWorkQueue

  • ScheduledThreadPoolExecutor之所以要自己实现阻塞的工作队列,是因为ScheduledThreadPoolExecutor要求的工作队列有些特殊。
  • DelayedWorkQueue是一个基于堆的数据结构,类似于DelayQueue和PriorityQueue。在执行定时任务的时候,每个任务的执行时间都不同,所以DelayedWorkQueue的工作就是按照执行时间的升序来排列,
  • 执行时间距离当前时间越近的任务在队列的前面(注意:这里的顺序并不是绝对的,堆中的排序只保证了子节点的下次执行时间要比父节点的下次执行时间要大,而叶子节点之间并不一定是顺序的,下文中会说明)。
  • 堆结构如下图:
    在这里插入图片描述
  • 可见,DelayedWorkQueue是一个基于最小堆结构的队列。堆结构可以使用数组表示,可以转换成如下的数组:
    在这里插入图片描述
  • 在这种结构中,可以发现有如下特性:
  • 假设,索引值从0开始,子节点的索引值为k,父节点的索引值为p,则:
    1. 一个节点的左子节点的索引为:k = p * 2 + 1;
    2. 一个节点的右子节点的索引为:k = (p + 1) * 2;
    3. 一个节点的父节点的索引为:p = (k - 1) / 2。

为什么要使用DelayedWorkQueue呢?

  • 定时任务执行时需要取出最近要执行的任务,所以任务在队列中每次出队时一定要是当前队列中执行时间最靠前的,所以自然要使用优先级队列。
  • DelayedWorkQueue是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的,由于它是基于堆结构的队列,堆结构在执行插入和删除操作时的最坏时间复杂度是 O(logN)。

DelayedWorkQueue属性

// 队列初始容量
private static final int INITIAL_CAPACITY = 16;
// 根据初始容量创建RunnableScheduledFuture类型的数组
private RunnableScheduledFuture<?>[] queue =
new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
private final ReentrantLock lock = new ReentrantLock();
private int size = 0;
// leader线程
private Thread leader = null;
// 当较新的任务在队列的头部可用时,或者新线程可能需要成为leader,则通过该条件发出信号
private final Condition available = lock.newCondition();
  • 注意这里的leader,它是Leader-Follower模式的变体,用于减少不必要的定时等待。什么意思呢?对于多线程的网络模型来说:
  • 所有线程会有三种身份中的一种:leader和follower,以及一个干活中的状态:
  • proccesser。它的基本原则就是,永远最多只有一个leader。而所有follower都在等待成为leader。
  • 线程池启动时会自动产生一个Leader负责等待网络IO事件,当有一个事件产生时,Leader线程首先通知一个Follower线程将其提拔为新的Leader,然后自己就去干活了,去处理这个网络事件,处理完毕后加入Follower线程等待队列,等待下次成为Leader。
  • 这种方法可以增强CPU高速缓存相似性,及消除动态内存分配和线程间的数据交换。

offer方法

  • 该函数是一个并发容器类的添加元素方法。它将传入的Runnable对象转换为RunnableScheduledFuture<?>类型,然后通过加锁确保线程安全。
  • 在获取锁后,函数会检查队列是否已满,如果已满则扩容。接着将元素加入队列,并根据队列状态进行相应的操作,最后释放锁并返回true表示添加成功。
  • 整个过程使用了CAS操作和锁机制来保证并发性能和线程安全。

任务排序sift方法

  • 代码很好理解,就是循环的根据key节点与它的父节点来判断,如果key节点的执行时间小于父节点,则将两个节点交换,使执行时间靠前的节点排列在队列的前面。
  • 假设新入队的节点的延迟时间(调用getDelay()方法获得)是5,执行过程如下:
    1. 先将新的节点添加到数组的尾部,这时新节点的索引k为7:
      在这里插入图片描述

    2. 计算新父节点的索引:parent = (k - 1) >>> 1,parent = 3,那么queue[3]的时间间隔值为8,因为 5 < 8 ,将执行queue[7] = queue[3]:
      在这里插入图片描述

    3. 这时将k设置为3,继续循环,再次计算parent为1,queue[1]的时间间隔为3,因为 5 > 3 ,这时退出循环,最终k为3:
      在这里插入图片描述

  • 可见,每次新增节点时,只是根据父节点来判断,而不会影响兄弟节点。

take方法

  • take方法是什么时候调用的呢?在ThreadPoolExecutor中,介绍了getTask方法,工作线程会循环地从workQueue中取任务。
  • 但定时任务却不同,因为如果一旦getTask方法取出了任务就开始执行了,而这时可能还没有到执行的时间,所以在take方法中,要保证只有在到指定的执行时间的时候任务才可以被取走。
  • 再来说一下leader的作用,这里的leader是为了减少不必要的定时等待,当一个线程成为leader时,它只等待下一个节点的时间间隔,但其它线程无限期等待。 leader线程必须在从take()或poll()返回之前signal其它线程,除非其他线程成为了leader。
  • 举例来说,如果没有leader,那么在执行take时,都要执行available.awaitNanos(delay),假设当前线程执行了该段代码,这时还没有signal,第二个线程也执行了该段代码,则第二个线程也要被阻塞。
  • 多个这时执行该段代码是没有作用的,因为只能有一个线程会从take中返回queue[0](因为有lock),其他线程这时再返回for循环执行时取的queue[0],已经不是之前的queue[0]了,然后又要继续阻塞。
  • 所以,为了不让多个线程频繁的做无用的定时等待,这里增加了leader,如果leader不为空,则说明队列中第一个节点已经在等待出队,这时其它的线程会一直阻塞,减少了无用的阻塞(注意,在finally中调用了signal()来唤醒一个线程,而不是signalAll())。

poll 方法

  • 该函数是一个带有超时时间的轮询函数,用于从队列中获取第一个可用的任务。具体功能如下:
  • 尝试获取锁,如果锁被其他线程持有,则会阻塞当前线程,直到获取锁为止。
  • 循环遍历队列,查找第一个非空的任务。
  • 如果找到了非空任务且延迟时间为0或负数,表示任务可以立即执行,将其从队列中移除并返回。
  • 如果超时时间已过,返回null。
  • 如果当前任务的延迟时间大于超时时间,释放锁并等待任务的延迟时间结束。
  • 如果当前线程被中断,抛出InterruptedException异常。
  • 在等待期间,如果有其他线程尝试添加任务到队列中,则会提前结束等待。
  • 最后,释放锁并返回结果。

finishPoll方法

  • 该函数是一个私有函数,用于执行poll和take操作的公共记录工作。它通过将队列中的第一个元素替换为最后一个元素,并将其向下筛选来完成操作。
  • 函数中首先将队列大小减1,并将要删除的任务f的索引设置为-1,然后判断队列大小是否为0,如果不为0,则调用siftDown函数将元素x向下筛选。
  • 最后返回任务f。

siftDown方法

  • 该函数是一个私有方法,用于将添加到队列顶部的元素向下移动到其堆排序的位置。
  • 该方法在调用时需要持有锁。 方法内部通过while循环,不断将当前元素与它的子节点进行比较,如果当前元素大于子节点中的较小值,则将较小值替换为当前元素,并继续向下移动。
  • 直到当前元素小于等于子节点中的较小值,或者当前元素已经位于队列的底部时,结束循环。
  • 最后将当前元素放置在正确的位置上,并更新索引。 其中,size表示队列的大小,queue是一个存储元素的数组,setIndex是一个用于更新元素在数组中索引的方法。

remove方法

  • 该函数是一个从队列中移除元素的函数,它的参数是一个Object类型的x。
  • 函数首先获取一个ReentrantLock类型的锁,然后尝试移除元素x。如果找到了x,就将它在队列中的位置标记为-1,然后更新队列的大小,并将x所在位置的元素置为null。
  • 接着,函数会判断是否需要将替代元素(replacement)下沉或上移来保持队列的有序性。
  • 最后,函数返回一个布尔值,表示是否成功移除了x,并在finally块中释放锁。

总结

  • 与Timer执行定时任务的比较,相比Timer,ScheduedThreadPoolExecutor有什么优点;
  • ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,所以它也是一个线程池,也有coorPoolSize和workQueue,ScheduledThreadPoolExecutor特殊的地方在于,自己实现了优先工作队列DelayedWorkQueue;
  • ScheduedThreadPoolExecutor实现了ScheduledExecutorService,所以就有了任务调度的方法,如schedule,scheduleAtFixedRate和scheduleWithFixedDelay,同时注意他们之间的区别;
  • 内部类ScheduledFutureTask继承自FutureTask,实现了任务的异步执行并且可以获取返回结果。同时也实现了Delayed接口,可以通过getDelay方法获取将要执行的时间间隔;
  • 周期任务的执行其实是调用了FutureTask类中的runAndReset方法,每次执行完不设置结果和状态。
  • 详细分析了DelayedWorkQueue的数据结构,它是一个基于最小堆结构的优先队列,并且每次出队时能够保证取出的任务是当前队列中下次执行时间最小的任务。
  • 同时注意一下优先队列中堆的顺序,堆中的顺序并不是绝对的,但要保证子节点的值要比父节点的值要大,这样就不会影响出队的顺序。

总体来说,ScheduedThreadPoolExecutor的重点是要理解下次执行时间的计算,以及优先队列的出队、入队和删除的过程,这两个是理解ScheduedThreadPoolExecutor的关键。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值