公平锁和非公平锁的区别?

典型回答 非公平锁:多个线程不按照申请锁的顺序去获得锁,而是直接去尝试获取锁,获取不到,再进入队列等待,如果能获取到,就直接获取到锁。 公平锁:多个线程按照申请锁的顺序去获得锁,所有线程都在队列里排队,这样就保证了队列中的第一个先得到锁。 两种锁分别适合不同的场景中,存在着各自的优缺点,对于公平锁来说,他的优点是所有的线程都能得到资源,不会饿死在队列中。但是他存在着吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大的缺点。 而对于非公平锁来说,他可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必去唤醒所有线程,会减少唤起线程的数量。但是他可能会导致队列中排队的线程一直获取不到锁或者长时间获取不到锁,活活饿死的情况。 扩展知识 ReentrantLock 分为公平锁和非公平锁,可以通过构造方法来指定具体类型: 1 2 3 4 //默认非公平锁 public ReentrantLock() { sync = new NonfairSync(); } 1 2 3 4 //公平锁 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } 默认一般使用非公平锁,它的效率和吞吐量都比公平锁高的多。 reentrantLock的非公平锁实现 非公平锁的lock的核心逻辑在NonfairSync中,具体代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /** * Sync object for non-fair locks */ static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } ...

March 22, 2026 · 2 min · santu

到底啥是内存屏障?到底怎么加的?

典型回答 (无关紧要的话:这个问题也困扰了我好久,之前一直没深究,毕竟这东西离开发太远了,但是最近在volatile的介绍中有很多人问这个东西到底是咋加的,还有的兄弟说我介绍的根本不对,那我就抽空研究了一下,和大家一起学习。但是本文的内容太底层了,我个人觉得了解即可。。。) 为了保证volatile变量的可见性和禁止指令重排序,Java会在生成的字节码中插入内存屏障来实现。 我们所说的内存屏障是一种CPU指令,它可以防止CPU及编译器对指令序列进行重排序,从而保证在代码执行过程中,对内存的读写操作按照程序员的意愿来进行。 关于内存屏障,推荐看一下,Doug Lea写的这篇文章:https://gee.cs.oswego.edu/dl/jmm/cookbook.html 四种屏障 volatile变量的内存屏障是通过一组指令来实现的,包括LoadLoad、LoadStore、StoreStore和StoreLoad。这些指令用于保证在volatile变量的读取和写入操作中,相邻指令之间的顺序不会被改变。 LoadLoad:Load1; LoadLoad; Load2,确保 Load1 加载数据先于 Load2 和所有后续加载指令。 LoadStore:Load1; LoadStore; Store2,确保 Load1 的数据加载先于 Store2 及其后续存储指令将数据刷新到主内存。 StoreStore:<font style="color:rgb(55, 65, 81);">Store1; StoreStore; Store2</font>,确保 Store1 的数据在Store2 及所有后续存储指令相关的数据之前对其他处理器可见(即刷新到内存中) StoreLoad:Store1; StoreLoad; Load2,确保 Store1 的数据在 Load2 及其后续加载指令访问的数据之前对其他处理器可见(即刷新到主内存) 这里怎么记呢,就看操作顺序就行了,先load再load,中间加的就是LoadLoad,先Load再Store,中间加的就是LoadStore 但是,Doug Lea也说过,《很难找到一个” 最佳” 的位置使得最大限度地减少执行屏障的总数》,并且在不同的操作系统上,具体的添加屏障的情况也不一致,有些操作系统可以天然保证一些操作不会被重排序。 那么,如果严格一点的话,最完整的情况应该是这样加内存屏障,对于volatile变量来说: 在每一个volatile的写(store)之前,加入一个StoreStore屏障和一个LoadStore屏障 在每一个volatile的写(store)之后,加入一个StoreLoad屏障和一个StoreStore屏障 在每一个volatile的读(load)之后,加一个LoadLoad屏障和LoadStrore屏障 即: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 StoreStore LoadStore store StoreLoad StroeStore ----- load LoadLoad LoadStrore ...

March 22, 2026 · 2 min · santu

如何理解AQS?

典型回答 AbstractQueuedSynchronizer (抽象队列同步器,以下简称 AQS)出现在 JDK 1.5 中。AQS 是很多同步器的基础框架,比如 ReentrantLock、CountDownLatch 和 Semaphore 等都是基于 AQS 实现的。除此之外,我们还可以基于 AQS,定制出我们所需要的同步器。 1 2 3 4 public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { } 在AQS内部,维护了一个FIFO队列和一个volatile的int类型的state变量。在state=1的时候表示当前对象锁已经被占有了,state变量的值修改的动作通过CAS来完成。 ✅有了CAS为啥还需要volatile? FIFO队列用来实现多线程的排队工作,当线程加锁失败时,该线程会被封装成一个Node节点来置于队列尾部。 当持有锁的线程释放锁时,AQS会将等待队列中的第一个线程唤醒,并让其重新尝试获取锁。 上图展示的是一个非公平锁,如果是公平锁则第一步只进行判断队列中是否有前序节点,如果有的话,直接入队列,不会进行第一次的CAS。 同步状态——state AQS使用一个volatile int类型的成员变量来表示同步状态,在state=1的时候表示当前对象锁已经被占有了。它提供了三个基本方法来操作同步状态:getState(), setState(int newState), 和 compareAndSetState(int expect, int update)。这些方法允许在不同的同步实现中自定义资源的共享和独占方式。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 同步状态 private volatile int state; // 获取状态 protected final int getState() { return state; } // 设置状态 protected final void setState(int newState) { state = newState; } // CAS更新状态 protected final boolean compareAndSetState(int expect, int update) { // See below for intrinsics setup to support this return unsafe.compareAndSwapInt(this, stateOffset, expect, update); } FIFO队列——Node AQS内部通过一个内部类——Node,AQS就是借助他来实现同步队列的功能的。 ...

March 22, 2026 · 2 min · santu

有了synchronized为什么还需要volatile_

典型回答 synchronized其实是一种加锁机制,那么既然是锁,天然就具备以下几个缺点: 1、有性能损耗:虽然在JDK 1.6中对synchronized做了很多优化,如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等。但是他毕竟还是一种锁。所以,无论是使用同步方法还是同步代码块,在同步操作之前还是要进行加锁,同步操作之后需要进行解锁,这个加锁、解锁的过程是要有性能损耗的。 2、产生阻塞:无论是同步方法还是同步代码块,无论是ACC_SYNCHRONIZED还是monitorenter、monitorexit都是基于Monitor实现的。基于Monitor对象,当多个线程同时访问一段同步代码时,首先会进入Entry Set,当有一个线程获取到对象的锁之后,才能进行The Owner区域,其他线程还会继续在Entry Set等待。并且当某个线程调用了wait方法后,会释放锁并进入Wait Set等待。所以,synchronize实现的锁本质上是一种阻塞锁。 除了前面我们提到的volatile比synchronized性能好以外,volatile其实还有一个很好的附加功能,那就是禁止指令重排。因为volatile借助了内存屏障来帮助其解决可见性和有序性问题,而内存屏障的使用还为其带来了一个禁止指令重排的附加功能,所以在有些场景中是可以避免发生指令重排的问题的。 扩展知识 锁的性能损耗 虽然在JDK 1.6中对synchronized做了很多优化,如如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等,但是他毕竟还是一种锁。 以上这几种优化,都是尽量想办法避免对Monitor进行加锁,但是,并不是所有情况都可以优化的,况且就算是经过优化,优化的过程也是有一定的耗时的。 所以,无论是使用同步方法还是同步代码块,在同步操作之前还是要进行加锁,同步操作之后需要进行解锁,这个加锁、解锁的过程是要有性能损耗的。 关于二者的性能对比,由于虚拟机对锁实行的许多消除和优化,使得我们很难量化这两者之间的性能差距,但是我们可以确定的一个基本原则是:volatile变量的读操作的性能和普通变量几乎无差别,但是写操作由于需要插入内存屏障所以会慢一些,即便如此,volatile在大多数场景下也比锁的开销要低。 锁产生阻塞 无论是同步方法还是同步代码块,无论是ACC_SYNCHRONIZED还是monitorenter、monitorexit都是基于Monitor实现的。 基于Monitor对象,当多个线程同时访问一段同步代码时,首先会进入Entry Set,当有一个线程获取到对象的锁之后,才能进行The Owner区域,其他线程还会继续在Entry Set等待。并且当某个线程调用了wait方法后,会释放锁并进入Wait Set等待。 所以,synchronize实现的锁本质上是一种阻塞锁,也就是说多个线程要排队访问同一个共享对象。 而volatile是Java虚拟机提供的一种轻量级同步机制,他是基于内存屏障实现的。说到底,他并不是锁,所以他不会有synchronized带来的阻塞和性能损耗的问题。 volatile的附加功能 除了前面我们提到的volatile比synchronized性能好以外,volatile其实还有一个很好的附加功能,那就是禁止指令重排。 我们先来举一个例子,看一下如果只使用synchronized而不使用volatile会发生什么问题,就拿我们比较熟悉的单例模式来看。 我们通过双重校验锁的方式实现一个单例,这里不使用volatile关键字: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Singleton { private static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } } 以上代码,我们通过使用synchronized对Singleton.class进行加锁,可以保证同一时间只有一个线程可以执行到同步代码块中的内容,也就是说singleton = new Singleton()这个操作只会执行一次,这就是实现了一个单例。 ...

March 22, 2026 · 1 min · santu

线程数设定成多少更合适?

典型回答 线程数设定是个高频出现的题目,这个题目对于优秀的面试官希望得到的答案是面试者的思考过程,考察其分析线程数设定的思考方向是否全面(主要包括对操作系统线程的理解和实际设置经验)。比较差的面试官才会认为固定的公式是正确的。 首先看一下影响线程数的因素可能有哪些? 影响线程数的因素 线程数的设定需要根据应用程序的需求和运行环境来决定,没有一个固定的最佳值。以下是一些影响线程数设定的关键因素: CPU 核数 多核处理器:理想的线程数肯定取决于处理器的核心数。在理想情况下,每个核心运行一个线程是最高效的。 超线程技术(如Intel的Hyper-Threading):目前,很多CPU都采用了超线程技术,也就是利用特殊的硬件指令,把两个逻辑内核模拟成两个物理芯片,让单个处理器都能使用线程级并行计算。所以我们经常可以看到"4核8线程的CPU",也就是物理内核有4个,逻辑内核有8个。如果CPU支持超线程技术,可以为每个核心分配更多的线程,因为超线程可以提高CPU资源的利用率。 应用类型 CPU密集型:对于CPU密集型的任务(如计算密集型任务),线程数最好设置为核心数的1到1.5倍,因为这些任务主要消耗CPU资源。 I/O密集型:如果任务涉及大量的等待或阻塞(如数据库操作、文件操作、网络操作等),则可以配置更多的线程,比如2倍,因为线程在等待时CPU可以切换去处理其他任务。 JVM和系统资源 内存限制:每个线程都会占用一定的内存(如栈空间)。如果创建过多线程,可能会消耗大量内存,甚至导致内存溢出。 操作系统限制:操作系统对进程可创建的线程数通常有限制,过多的线程可能导致系统性能下降。 其他考虑 RT要求:如果系统对响应时间有严格要求,可能需要更多线程来减少处理延迟。 任务特性:不同的任务可能对线程数的需求不同。长时间运行的任务与短时任务,同步任务与异步任务,都需要考虑不同的线程配置。 有没有公式 根据我们前面提到的这些影响因素来看,主要和CPU核心数、以及应用类型等有关。简单一点的话,可以根据应用类型来套用以下公式(但是,实际应用起来,也不要死守着公式不放,公式只是可以当作参考): 如果是CPU密集型应用,则线程池大小设置为N+1 如果是IO密集型应用,则线程池大小设置为2N+1 上面的N为CPU总核数 但是,上面的公式中,前提要求是知道你的应用是IO密集型还是CPU密集型,那么,到底怎么样算IO密集,怎么样又算CPU密集呢?一个应用就真的能明确的定位出来是CPU密集还是IO密集吗? 所以,还有另外一个公式: 等待时间是指线程在执行过程中花费在等待外部操作完成的时间。这些外部操作通常包括I/O操作(如读写文件、数据库操作、网络请求等)和其他资源的同步等待(如等待锁的释放)。在等待时间内,线程通常不占用CPU资源,因为它在等待某个事件或资源可用。 计算时间是指线程实际进行计算处理的时间,即线程在CPU上执行操作的时间。计算时间通常指的是CPU密集型操作,如数学计算、数据处理等。 在这个公式中,“等待时间 / 计算时间"的比例是一个关键因素,它帮助决定合适的线程数量以平衡CPU利用和等待效率: I/O密集型任务:对于I/O密集型任务,等待时间通常远大于计算时间,这意味着可以分配更多的线程。当一个线程在等待时(如等待网络响应),CPU可以切换到另一个线程进行计算,从而提高CPU利用率。 CPU密集型任务:对于CPU密集型任务,计算时间通常占主导地位。在这种情况下,增加线程数可能不会提高性能,因为大部分时间都在进行CPU计算,线程之间的上下文切换可能导致性能下降。 不建议直接套公式 上面给出了公式,但是这个公式建议大家可以参考,而不是直接套用。 一方面我们讲过了,线程数设置时还需要考虑JVM、机器资源等,还需要考虑超线程等技术,另外有时候我们看到的CPU核数也不一定就是真实的。 很多时候,我们的应用部署在云服务器上面,有时候给我们分配的机器显示的是8核的,但是你要知道你实际上使用的只是虚拟机而已,并不是物理机,实际上大多数情况下不能发挥出8核的作用来。 可以在刚上线的时候,先根据公式大致的设置一个数值,然后再根据你自己的实际业务情况,以及不断的压测结果,再不断调整,最终达到一个相对合理的值。 在说明压测的时候,要说清楚可接受的响应耗时是多少,大于这个阈值即是错误,错误率多少可接受。这样给出的线程池参数才合理。 ✅什么是压测,怎么做压测? 扩展知识 核心线程数已经占满了,如果来了新的任务会怎么处理? 先尝试把任务放进工作队列(workQueue) 只要工作队列没满,新任务就会在队列中排队等待,不会创建新线程,也不会被拒绝。 只有当队列也满了,才会考虑创建超过核心线程数的线程(最多到 maximumPoolSize)。 如果连 maximumPoolSize 也达到了,且队列满,才会拒绝任务。

March 22, 2026 · 1 min · santu

线程有几种状态,状态之间的流转是怎样的?

典型回答 Java中线程的状态分为6种: 1.初始(NEW):新创建了一个线程对象,但还没有调用start()方法。 2.运行(RUNNABLE):Java线程中将就绪(READY)和运行中(RUNNING)两种状态笼统的称为“运行”。 就绪(READY):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中并分配cpu使用权 。 运行中(RUNNING):就绪(READY)的线程获得了cpu 时间片,开始执行程序代码。 3.阻塞(BLOCKED):表示线程阻塞于锁(关于锁,在后面章节会介绍)。 4.等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。 5.超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。 终止(TERMINATED):表示该线程已经执行完毕。 状态流转如图: 拓展知识 WAITING和TIMED_WAIT的区别? WAITING是等待状态,在Java中,调用wait方法时,线程会进入到WAITING状态,而TIMED_WAITING是超时等待状态,当线程执行sleep方法时,线程会进入TIMED_WAIT状态。 处于WAITING和TIMED_WAIT的线程,都是会让出CPU的,这时候其他线程就可以获得CPU时间片开始执行。但是他们在对象的锁释放上面并不一样,如果加了锁,sleep方法不会释放对象上的锁,而wait方法是会释放锁的。 因为Java锁的目标是对象,所以wait、notify和notifyAll针对的目标都是对象,所以把他们定义在Object类中。而sleep不需要释放锁,所以他是Thread类中的一个方法。 为什么线程没有RUNNING状态 对于现在的分时操作系统来说,在单CPU情况下,所有的线程其实都是串行执行的。但是为了让我们看起来像是在并发执行,人们把CPU的执行分成很多个小的时间片。 哪个线程得到时间片,那个线程就执行,时间片到了之后,就要释放出CPU,再重新进行争抢时间片。 只要把时间片划分的足够细,那么多个程序虽然在不断的串行执行,但是看起来也像是在同时执行一样。 那么,CPU的时间片其实是很短的,一般也就是10-20毫秒左右。 那么,也就是说,在一秒钟之内,同一个线程可能一部分时间处于READY状态、一部分时间处于RUNNING状态。 那么如果,明确的给线程定义出RUNNING状态的话,有一个很大的问题,就是这个状态其实是不准的。 因为当我们看到线程是RUNNING状态的时候,很有可能他已经丢失了CPU时间片了。 对于线程的状态,我们只需要知道,他当前有没有在"正在参与执行"就行了,何为"参与执行"? 就是他的状态是可执行的,只要获得时间片,就能立即执行。 那这不就是RUNNABLE吗? 所以,Java就没有给线程定义RUNNING状态,而是定义了一个RUNNABLE状态。

March 22, 2026 · 1 min · santu

线程池中使用ThreadLocal会有哪些潜在风险?

典型回答 ✅ThreadLocal为什么会导致内存泄漏?如何解决的? 其实这个问题就是上面这个问题的变形。(请仔细看上面这篇文章之后再学习本文,不然可能看不懂。) 如果在线程池中使用ThreadLocal,线程就要复用,就不会被销毁,ThreadLocal 变量不会自动清理,容易造成内存泄漏! 因为ThreadLocal 绑定在线程的 ThreadLocalMap 里。如上图的引用链。如果线程一直被复用,那么Thread Ref就会一直在,那么他关联的Thread对象,ThreadLocalMap和其中的Value就会一直在,无法被回收。 随着线程不断服用,不断的往ThreadLocalMap中加东西,就会导致Value越来越多。最终导致OOM。 ThreadLocalMap底层使用数组来保存元素,使用“线性探测法”来解决hash冲突的,在每次调用ThreadLocal的get、set、remove等方法的时候,内部会实际调用ThreadLocalMap的get、set、remove等操作。 而ThreadLocalMap的每次get、set、remove,都会清理key为null,但是value还存在的Entry。 所以,当我们在一个ThreadLocal用完之后,手动调用一下remove,就可以在下一次GC的时候,把Entry清理掉。 (装B时刻:现在是2025年9月19日,JDK25在两天前发布了,新出了一个ScopedValue,可以解决这个内存泄漏的问题,你如果面试的时候主动提一句,会让面试官觉得你很有技术热情。很加分!!!) ✅JDK25的ScopedValue是什么?为什么可以替代ThreadLocal?

March 22, 2026 · 1 min · santu

线程池有哪些核心参数?

典型回答 ✅什么是线程池,如何实现的? 这个问题其实在上面的文章中介绍过,但是我发现最近问这个还挺多的, 有的时候搜索不一定能找到准确的内容,所以单独写一篇,详细展开再介绍下。 以下这个是Java中ThreadPoolExecutor的构造函数,那么看他都有哪些参数就行了。 参数 含义 说明 corePoolSize 核心线程数 核心线程会一直存在(即使空闲),除非设置了 allowCoreThreadTimeOut(true)。当任务到来时,线程池先创建核心线程来执行任务。可以类比正式员工数量,常驻。 maximumPoolSize 最大线程数 线程池中允许存在的最大线程数。任务太多且队列已满时,会创建非核心线程,直到达到此上限。公司最多雇佣员工数量。 workQueue 任务队列 存放等待执行任务的队列。不同类型的队列影响线程池的行为。再多的人都处理不过来了,需要等着,在这个地方等。 keepAliveTime 非核心线程存活时间 超过核心线程数的空闲线程,超过该时间后会被销毁。就是外包人员等了多久,如果还没有活干,解雇了。 unit 时间单位 TimeUnit.SECONDS, MILLISECONDS等,表示 keepAliveTime的单位。 threadFactory 线程工厂 用于创建线程,一般用于给线程命名或设置为守护线程。 handler 拒绝策略 当任务太多无法处理时的处理方式(如抛异常、丢弃、调用者执行等)。 线程池创建方法 ✅为什么不建议通过Executors构建线程池 核心线程数&最大线程数 ✅线程数设定成多少更合适? 拒绝策略 ✅线程池的拒绝策略有哪些? 任务队列类型 队列类型 说明 特点 LinkedBlockingQueue 无界链表队列(默认容量 Integer.MAX_VALUE) 常用于执行大量任务,容易导致 OOM。 ArrayBlockingQueue 有界数组队列 可限制任务数量,防止 OOM,推荐使用。 SynchronousQueue 不存储任务的“直接移交”队列 每个插入操作必须等待对应的取出操作。用于“任务即时执行”。 PriorityBlockingQueue 优先级队列 根据任务优先级执行。 线程池调优 ✅如何进行线程池调优?

March 22, 2026 · 1 min · santu

能不能谈谈你对线程安全的理解?

典型回答 线程安全是指某个函数在并发环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。 简单来说,就是多个线程同时访问共享变量的时候,得到的结果和我们预期的一样,就是线程安全。所以有四个关键词:并发、多线程、共享变量、正确完成。这里所谓的正确完成,其实就是要满足所谓的原子性、有序性和可见性。 知识扩展 并发与并行 ✅什么是并发,什么是并行? 进程和线程 理解了并发和并行之间的关系和区别后,我们再回到前面介绍的多任务分时操作系统,看看CPU是如何进行进程调度的。 为了看起来像是“同时干多件事”,分时操作系统是把CPU的时间划分成长短基本相同的”时间片”,通过操作系统的管理,把这些时间片依次轮流地分配给各个用户的各个任务使用。 在多任务处理系统中,CPU需要处理所有程序的操作,当用户来回切换它们时,需要记录这些程序执行到哪里。在操作系统中,CPU切换到另一个进程需要保存当前进程的状态并恢复另一个进程的状态:当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。上下文切换就是这样一个过程,他允许CPU记录并恢复各种正在运行程序的状态,使它能够完成切换操作。 在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称作“切换帧”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。 对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。 而在多个进程之间切换的时候,需要进行上下文切换。但是上下文切换势必会耗费一些资源。于是人们考虑,能不能在一个进程中增加一些“子任务”,这样减少上下文切换的成本。比如我们使用Word的时候,它可以同时进行打字、拼写检查、字数统计等,这些子任务之间共用同一个进程资源,但是他们之间的切换不需要进行上下文切换。 在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。 随着时间的慢慢发展,人们进一步的切分了进程和线程之间的职责。把进程当做资源分配的基本单元,把线程当做执行的基本单元,同一个进程的多个线程之间共享资源 拿我们比较熟悉的Java语言来说,Java程序是运行在JVM上面的,每一个JVM其实就是一个进程。所有的资源分配都是基于JVM进程来的。而在这个JVM进程中,又可以创建出很多线程,多个线程之间共享JVM资源,并且多个线程可以并发执行。 但是,需要注意的是,Java中,在JDK21 出来虚拟线程之前,线程在操作系统层面也是基于轻量级进程实现的, 本质上还是存在操作系统级别的上下文切换的。JDK 21的虚拟线程是一种用户态线程,其切换不需要操作系统的参与,因此可以避免操作系统级别上下文切换,但是仍然需要在JVM层面做一些保存和恢复线程的状态,但是也成本低得多 线程的特点 在多线程操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。 轻型实体 线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。 线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。TCB包括以下信息: (1)线程状态。 (2)当线程不运行时,被保存的现场资源。 (3)一组执行堆栈。 (4)存放每个线程的局部变量主存区。 (5)访问同一个进程中的主存和其它资源。 用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。 独立调度和分派的基本单位。 在多线程操作系统中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。 可并发执行 在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。 共享进程资源 在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。 共享变量 所谓共享变量,指的是多个线程都可以操作的变量。 前面我们提到过,进程是分配资源的基本单位,线程是执行的基本单位。所以,多个线程之间是可以共享一部分进程中的数据的。在JVM中,Java堆和方法区的区域是多个线程共享的数据区域。也就是说,多个线程可以操作保存在堆或者方法区中的同一个数据。那么,换句话说,保存在堆和方法区中的变量就是Java中的共享变量。 那么,Java中哪些变量是存放在堆中,哪些变量是存放在方法区中,又有哪些变量是存放在栈中的呢? 类变量、成员变量和局部变量 Java中共有三种变量,分别是类变量、实例变量和局部变量。他们分别存放在JVM的方法区(元空间)、堆内存和栈内存中。 这里有关于成员变量、局部变量,以及成员变量中类变量和实例变量的定义。 https://web.archive.org/web/20220412010257/http://www.dickbaldwin.com/java/Java020.htm 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 /** * @author Hollis */ public class Variables { /** * 类变量 */ private static int a; /** * 成员变量 */ private int b; /** * 局部变量 * @param c */ public void test(int c){ int d; } } 上面定义的三个变量中,变量a就是类变量,变量b就是成员变量,而变量c和d是局部变量。 ...

March 22, 2026 · 1 min · santu

什么是多线程中的上下文切换?

典型回答 上下文切换是指 CPU 从一个线程转到另一个线程时,需要保存当前线程的上下文状态,恢复另一个线程的上下文状态,以便于下一次恢复执行该线程时能够正确地运行。 在多线程编程中,上下文切换是一种常见的操作,上下文切换通常是指在一个 CPU 上,由于多个线程共享 CPU 时间片,当一个线程的时间片用完后,需要切换到另一个线程运行。此时需要保存当前线程的状态信息,包括程序计数器、寄存器、栈指针等,以便下次继续执行该线程时能够恢复到正确的执行状态。同时,需要将切换到的线程的状态信息恢复,以便于该线程能够正确运行。 在多线程中,上下文切换的开销比直接用单线程大,因为在多线程中,需要保存和恢复更多的上下文信息。过多的上下文切换会降低系统的运行效率,因此需要尽可能减少上下文切换的次数。 扩展知识 减少上下文切换 频繁的上下文切换会导致CPU时间的浪费,因此在多线程编程时需要尽可能地避免它。以下是一些避免频繁上下文切换的方法: 减少线程数:可以通过合理的线程池管理来减少线程的创建和销毁,线程数不是越多越好,合理的线程数可以避免线程过多导致上下文切换。 使用无锁并发编程:无锁并发编程可以避免线程因等待锁而进入阻塞状态,从而减少上下文切换的发生。 使用CAS算法:CAS算法可以避免线程的阻塞和唤醒操作,从而减少上下文切换。 使用协程(JDK 19的虚拟线程):协程是一种用户态线程,其切换不需要操作系统的参与,因此可以避免上下文切换。(避免的是操作系统级别的上下文切换,但是仍然需要在JVM层面做一些保存和恢复线程的状态,但是也成本低得多) 合理地使用锁:在使用锁的过程中,需要避免过多地使用同步块或同步方法,尽量缩小同步块或同步方法的范围,从而减少线程的等待时间,避免上下文切换的发生。

March 22, 2026 · 1 min · santu

留言给博主