线程是如何被调度的?

典型回答 对于单CPU的计算机来说,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。 所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。 前面关于线程状态的介绍中,我们知道,线程的运行状态中包含两种子状态,即就绪(READY)和运行中(RUNNING)。 而一个线程想要从就绪状态变成运行中状态,这个过程需要系统调度,即给线程分配CPU的使用权,获得CPU使用权的线程才会从就绪状态变成运行状态。 给多个线程按照特定的机制分配CPU的使用权的过程就叫做线程调度。 进程是分配资源的基本单元,线程是CPU调度的基本单元。这里所说的调度指的就是给其分配CPU时间片,让其执行任务。 Linux线程调度 在Linux中,线程是由进程来实现,线程就是轻量级进程( lightweight process ),因此在Linux中,线程的调度是按照进程的调度方式来进行调度的,也就是说线程是调度单元。 Linux这样实现的线程的好处的之一是:线程调度直接使用进程调度就可以了,没必要再搞一个进程内的线程调度器。在Linux中,调度器是基于线程的调度策略(scheduling policy)和静态调度优先级(static scheduling priority)来决定那个线程来运行。 在Linux中,主要有三种调度策略。分别是: SCHED_OTHER 分时调度策略,(默认的) SCHED_FIFO 实时调度策略,先到先服务 SCHED_RR 实时调度策略,时间片轮转 Windows线程调度 Windows 采用基于优先级的、抢占调度算法来调度线程。 用于处理调度的 Windows 内核部分称为调度程序,Windows 调度程序确保具有最高优先级的线程总是在运行的。由于调度程序选择运行的线程会一直运行,直到被更高优先级的线程所抢占,或终止,或时间片已到,或调用阻塞系统调用(如 I/O)。如果在低优先级线程运行时,更高优先级的实时线程变成就绪,那么低优先级线程就被抢占。这种抢占使得实时线程在需要使用 CPU 时优先得到使用。 Java线程调度 可以看到,不同的操作系统,有不同的线程调度策略。但是,作为一个Java开发人员来说,我们日常开发过程中一般很少关注操作系统层面的东西。 主要是因为Java程序都是运行在Java虚拟机上面的,而虚拟机帮我们屏蔽了操作系统的差异,所以我们说Java是一个跨平台语言。 在操作系统中,一个Java程序其实就是一个进程。所以,我们说Java是单进程、多线程的! 前面关于线程的实现也介绍过,Thread类与大部分的Java API有显著的差别,它的所有关键方法都是声明为Native的,也就是说,他需要根据不同的操作系统有不同的实现。 在Java的多线程程序中,为保证所有线程的执行能按照一定的规则执行,JVM实现了一个线程调度器,它定义了线程调度模型,对于CPU运算的分配都进行了规定,按照这些特定的机制为多个线程分配CPU的使用权。 主要有两种调度模型:协同式线程调度和抢占式调度模型。 协同式线程调度 协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。协同式多线程的最大好处是实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以没有什么线程同步的问题。 抢占式调度模型 抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的问题。 系统会让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。 Java虚拟机采用抢占式调度模型。 虽然Java线程调度是系统自动完成的,但是我们还是可以“建议”系统给某些线程多分配一点执行时间,另外的一些线程则可以少分配一点——这项操作可以通过设置线程优先级来完成。Java语言一共设置了10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在两个线程同时处于Ready状态时,优先级越高的线程越容易被系统选择执行。 不过,线程优先级并不是太靠谱,原因是Java的线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统,虽然现在很多操作系统都提供线程优先级的概念,但是并不见得能与Java线程的优先级一一对应。

March 22, 2026 · 1 min · santu

线程池的拒绝策略有哪些?

典型回答 在Java中,线程池的拒绝策略决定了在任务队列已满的情况下,如何处理新提交的任务。当线程池达到最大容量,并且任务队列也已满时,拒绝策略就会起作用。Java提供了四种内置的拒绝策略,它们分别是: AbortPolicy - 这是默认的拒绝策略,当线程池无法接受新任务时,会抛出RejectedExecutionException异常。这意味着新任务会被立即拒绝,不会加入到任务队列中,也不会执行。通常情况下都是使用这种拒绝策略。 DiscardPolicy - 这个策略在任务队列已满时,会丢弃新的任务而且不会抛出异常。新任务提交后会被默默地丢弃,不会有任何提示或执行。这个策略一般用于日志记录、统计等不是非常关键的任务。 DiscardOldestPolicy - 这个策略也会丢弃任务,但它会先尝试将任务队列中最早的任务删除,然后再尝试提交新任务。如果任务队列已满,且线程池中的线程都在工作,可能会导致一些任务被丢弃。这个策略对于一些实时性要求较高的场景比较合适。 CallerRunsPolicy - 这个策略将任务回退给调用线程,而不会抛出异常。调用线程会尝试执行任务。这个策略可以降低任务提交速度,适用于任务提交者能够承受任务执行的压力,但希望有一种缓冲机制的情况。 1 2 3 4 5 6 7 8 ThreadPoolExecutor executor = new ThreadPoolExecutor( 10, // corePoolSize 10, // maximumPoolSize 0L, // keepAliveTime TimeUnit.MILLISECONDS, // timeUnit new LinkedBlockingQueue<>(10), new ThreadPoolExecutor.AbortPolicy() // 拒绝策略 ); 一般来说,默认的拒绝策略还是比较常用的,因为大多数情况下我们不太会让任务多到线程池中放不下,要不然就提升执行速度,要不然就提升队列长度了。 需要拒绝的情况一般是特殊情况比较多,所以在实际工作中基本就是拒绝并抛异常的方式比较多。

March 22, 2026 · 1 min · santu

Java是如何判断一个线程是否存活的?

典型回答 在Java中,我们自己想要判断线程是否存活,可以通过Thread下的isAlive()方法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Test{ public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { System.out.println("t1 begin"); try { Thread.sleep(1000); } catch (InterruptedException e) { } System.out.println("t1 end"); }); t1.start(); System.out.println("t1.isAlive()="+t1.isAlive()); t1.join(); System.out.println("t1.isAlive()="+t1.isAlive()); } } 运行结果: ...

March 22, 2026 · 2 min · santu

什么是可重入锁,怎么实现可重入锁?

典型回答 可重入锁是一种多线程同步机制,允许同一线程多次获取同一个锁而不会导致死锁。这意味着一个线程可以在持有锁的情况下再次请求并获得相同的锁,而不会被自己阻塞。可重入锁有助于避免死锁和提高代码的可维护性,因为它允许在一个线程中嵌套地调用锁定的方法。 如我们常用的synchronized和reentrantLock都是比较典型的可重入锁。也就是说,在一个线程调用synchronized方法的同时,可以在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class ReentrantLockTest { public static void main(String[] args) { ReentrantLockTest reentrantLockTest = new ReentrantLockTest(); reentrantLockTest.method1(); } public synchronized void method1(){ method2(); System.out.println("invoke method1"); } public synchronized void method2() { System.out.println("invoke method2"); } } 如以上代码,即可输出: ...

March 22, 2026 · 1 min · santu

如何实现主线程捕获子线程异常

典型回答 ✅为什么不能在try-catch中捕获子线程的异常? 我们说过,没办法直接在主线程的try-catch中捕获子线程的异常。但是,有的时候子线程中会开启一些IO链接,网络资源等,那么,如何在抛出异常的时候进行处理呢? 有几个方案可以实现: 使用Future 如果想要在主线程能够捕获子线程的异常,可以考虑使用Callable和Future,它们允许主线程获取子线程的执行结果和异常。这样,主线程可以检查子线程是否抛出了异常,并在必要时处理它。以下是一个示例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import java.util.concurrent.*; public class Main { public static void main(String[] args) { ExecutorService executor = Executors.newSingleThreadExecutor(); Future<Integer> future = executor.submit(() -> { // 子线程抛出异常 throw new RuntimeException("子线程异常"); }); try { Integer result = future.get(); System.out.println("子线程结果: " + result); } catch (ExecutionException e) { Throwable cause = e.getCause(); System.out.println("捕获到子线程异常")); } executor.shutdown(); } } 以上代码输出结果: ...

March 22, 2026 · 2 min · santu

Java线程出现异常,进程为啥不会退出?

典型回答 Java线程出现异常,如果这里的异常是我们认知中的Exception的话,JVM进程其实是不会退出的。 因为Java本身就是支持多线程的,每个Java线程都是相对独立的执行单元,每个线程是独立的执行上下文,异常只会影响抛出异常的线程。所以当一个线程抛出异常时,只会影响到该线程本身。其他线程将继续执行,不受异常的影响。 而且,在Java中,我们是可以自己主动的通过异常处理机制来捕获和处理异常的。如果在线程的代码中使用try-catch块来捕获异常,并在catch块中处理异常,那么异常不会传播到线程的外部,也不会影响整个进程的执行。 即使有的异常我们并没有捕获,Java也认为这些异常并不是特别严重(因为严重的话就不是异常,而是ERROR了),所以JVM并不会因为一个线程的异常就直接把JVM进程直接退出。 扩展知识 多线程异常处理 ✅为什么不能在try-catch中捕获子线程的异常? OOM与JVM退出 ✅Java发生了OOM一定会导致JVM 退出吗?

March 22, 2026 · 1 min · santu

为什么不能在try-catch中捕获子线程的异常_

典型回答 在Java中,主线程不能直接捕获子线程抛出的异常的!主要是因为子线程和主线程是独立的执行单元,它们的执行是并发的,因此主线程无法捕获子线程的异常。子线程的异常通常由子线程自己处理或通过适当的异常处理机制处理。 线程隔离,这也是Java保证线程出现异常不会影响整个进程的一个主要原理。 Java线程出现异常,进程为啥不会退出? 那么也就是说,以下代码是无法生效的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Main { public static void main(String[] args) { Thread childThread = new Thread(() -> { try { // 子线程抛出异常 int result = 10 / 0; } catch (ArithmeticException e) { System.out.println("子线程捕获到异常"); throw e; } }); try { // 主线程等待子线程执行完成 childThread.start(); } catch (InterruptedException e) { System.out.println("主线程捕获到异常"); } System.out.println("主线程继续执行"); } } 以上代码输出结果: ...

March 22, 2026 · 1 min · santu

什么是happens-before原则?

典型回答 在关于JMM的介绍中,我们知道,JMM 是一种规范,它提供了一系列的机制来保证跨线程的内存可见性、有序性和原子性。 我们之前介绍过很多保证可见性的关键字,如volatile和synchronized等,其实,volatile和synchronized为啥可以保证可见性,也正是因为他们遵守了一个重要的happens-before原则(后文会介绍,Monitor Lock 和Volatile Variable 是happens - before中重要的两个原则)。 我们之前还介绍过一个原则,叫做as-if-serial,他意思指:不管怎么重排序,单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。 ✅synchronized是如何保证原子性、可见性、有序性的? 这个as-if-serial语义是针对单线程的,但是如果在多线程情况下呢?有没有什么原则可以保证有序性呢?这就需要我们的happens-before原则了。 happens-before原则是一种用于描述多线程程序中操作执行顺序的规则。它是Java内存模型(Java Memory Model,JMM)的一部分:如果一个操作 A “happen-before” 另一个操作 B,那么 A 的结果对 B 是可见的。这个概念是理解线程间内存可见性的关键。 举一个例子,如以下代码: 1 2 3 4 5 6 7 8 9 10 11 public class ThreadStartExample { private int startValue = 10; public void startNewThread() { startValue +=1; new Thread(() -> { int localValue = startValue; }).start(); } } 有两个线程,一个写startValue,一个读startValue,但是我们并没有用synchronized加锁,也没有用volatile修饰,那么,JVM是如何保证,在主线程中修改startValue的操作在子线程中是可见的呢? ...

March 22, 2026 · 3 min · santu

happens-before和as-if-serial有啥区别和联系?

典型回答 happens-before 原则和 as-if-serial 语义是JMM中的重要概念,它们都与程序执行的顺序性和一致性有关,但它们的焦点和适用范围有所不同。 Happens-before 原则 ✅什么是happens-before原则? happens-before 原则是 Java 内存模型(JMM)的一部分,用于确定多线程环境中操作之间的顺序关系。它确保在一个线程中的操作在“逻辑上”先于另一个线程中的操作时,第一个线程的操作结果对第二个线程是可见的。 **happens-before原则主要用于多线程环境,确保线程间的内存可见性和有序性。**如在一个线程中对 volatile 变量的写操作 happens-before 另一个线程对同一 volatile 变量的读操作。 As-if-serial 语义 ✅synchronized是如何保证原子性、可见性、有序性的? As-if-serial 语义是一种保证单线程程序的行为就像代码按顺序执行一样的概念。它允许编译器和处理器进行优化(如指令重排),但优化不能改变程序执行的最终结果。 **as-if-serial语义保证了单线程中,指令重排是有一定的限制的,而只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的。**当然,实际上还是有重排的,只不过我们无须关心这种重排的干扰。 所以由于synchronized修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性。

March 22, 2026 · 1 min · santu

ThreadLocal的应用场景有哪些?

典型回答 ✅什么是ThreadLocal,如何实现的? ThreadLocal 提供了一种线程级别的数据存储机制。每个线程都拥有自己独立的 ThreadLocal 副本,这意味着每个线程都可以独立地、安全地操作这些变量,而不会影响其他线程。 ThreadLocal其实在工作中应该是非常常见的,以下是一些比较典型的使用场景: 用户身份信息存储: 在很多应用中,都需要做登录鉴权,一旦鉴权通过之后,就可以把用户信息存储在ThreadLocal中,这样在后续的所有流程中,需要获取用户信息的,直接取ThreadLocal中获取就行了。非常的方便。 线程安全:ThreadLocal可以用来定义一些需要并发安全处理的成员变量,比如SimpleDateFormat,由于 SimpleDateFormat 不是线程安全的,可以使用 ThreadLocal 为每个线程创建一个独立的 SimpleDateFormat 实例,从而避免线程安全问题。 日志上下文存储:在Log4j等日志框架中,经常使用ThreadLocal来存储与当前线程相关的日志上下文。这允许开发者在日志消息中包含特定于线程的信息,如用户ID或事务ID,这对于调试和监控是非常有用的。 **traceId存储:**和上面存储日志上下文类似,在分布式链路追踪中,需要存储本次请求的traceId,通常也都是基于ThreadLocal存储的。 **数据库Session:**很多ORM框架,如Hibernate、Mybatis,都是使用ThreadLocal来存储和管理数据库会话的。这样可以确保每个线程都有自己的会话实例,避免了在多线程环境中出现的线程安全问题。 PageHelper分页:PageHelper是MyBatis中提供的分页插件,主要是用来做物理分页的。我们在代码中设置的分页参数信息,页码和页大小等信息都会存储在ThreadLocal中,方便在执行分页时读取这些数据。 其实,看了这么多,主要就是俩作用: 1、解决并发问题,这个不需要多说了。 2、在线程中传递数据,在同一个线程执行过程中,ThreadLocal的数据一直在,所以我们可以在前面把数据放到ThreadLocal中,然后再后面的时候再取出来用,就可以避免要把这些数据一直通过参数传递。

March 22, 2026 · 1 min · santu

留言给博主