同花顺开始谈薪了,有点紧张。。。

Connor o易交易所 2025-11-10 4 0

图解学习网站:

大家好,我是小林。

这几天有校招同学跟我说,他秋招顺利扛住了同花顺所有技术面,准备到 HR 面了,但是有点紧张,不知道该谈薪的时候,报多少合适。

同花顺开始谈薪了,有点紧张。。。

我也网上搜集了一下,确实同花顺后端开发岗位的薪资情况只有 2023 年的,怎么都找不到 2024 年,难道同花顺去年秋招没有怎么招开发?

还真是,我翻看了同花顺去年的秋招发现是没有招技术开发岗位的:

同花顺开始谈薪了,有点紧张。。。

2024年同花顺秋招岗位

但是今年秋招不一样了,我发现同花顺是有开设开发岗位的招聘了,我看了一下官方的招聘列表,Java 后端开发的需求还不少。

同花顺开始谈薪了,有点紧张。。。

展开全文

今年同花顺秋招招聘规模是比去年大不少的,我感觉这个也是跟大 A 有点关系,最近几个月大 A 有一点慢牛的感觉,行情整体比去年好很多,小林自己的账户也从持续 3 年的绿色,终于翻红了。

所以合理的怀疑,同花顺作为炒股的软件,有新的市场增长需求了,自然就开始放量招生了。

26 届同学可以抓只这一波机会,开冲同花顺,去不去再说,offer先拿了

同花顺薪资的话,根据 23 年的情况校招是开了 20x15 薪资,整体还是有 30w 年薪的,虽然比不上一线大厂,但是也算大厂薪资范畴的了。

同花顺的校招面试难度,我也找了份真实面经给大家看看。

这是往届的 Java 校招一面,共面试了 40 分钟, 虽然不是这一两年的面经,但是也是可以作为一个参考点,八股和算法都是跟互联网公司拷打差不多。

同花顺开始谈薪了,有点紧张。。。

同花顺(Java 一面)1. 重载和重写的区别是什么?

重载是指在同一个类中定义多个同名方法,而重写是指子类重新定义父类中的方法。

重载(Overloading)指的是在同一个类中,可以有多个同名方法,它们具有不同的参数列表(参数类型、参数个数或参数顺序不同),编译器根据调用时的参数类型来决定调用哪个方法。

重写(Overriding)指的是子类可以重新定义父类中的方法,方法名、参数列表和返回类型必须与父类中的方法一致,通过@override注解来明确表示这是对父类方法的重写。

重载(Overloading)指的是在同一个类中,可以有多个同名方法,它们具有不同的参数列表(参数类型、参数个数或参数顺序不同),编译器根据调用时的参数类型来决定调用哪个方法。

重写(Overriding)指的是子类可以重新定义父类中的方法,方法名、参数列表和返回类型必须与父类中的方法一致,通过@override注解来明确表示这是对父类方法的重写。

简单说,final是用于限制程序结构的修饰符,finally是用于保证资源释放的异常处理块。

final用于修饰类、方法或变量,表达 "不可变" 的语义:修饰类时,该类不能被继承;修饰方法时,该方法不能被重写;修饰变量时,该变量一旦被赋值就不能再修改(对于基本类型是值不可变,对于引用类型是引用地址不可变,但对象内容可改)。

finally则是异常处理机制的一部分,只能用于try-catch-finally结构中,它定义的代码块总会被执行(除非虚拟机在执行前退出),无论try块中的代码是否正常执行、是否抛出异常、或者try/catch块中是否有return语句。通常用finally来释放资源,比如关闭文件流、数据库连接等,确保资源不会因异常而泄漏。

final用于修饰类、方法或变量,表达 "不可变" 的语义:修饰类时,该类不能被继承;修饰方法时,该方法不能被重写;修饰变量时,该变量一旦被赋值就不能再修改(对于基本类型是值不可变,对于引用类型是引用地址不可变,但对象内容可改)。

finally则是异常处理机制的一部分,只能用于try-catch-finally结构中,它定义的代码块总会被执行(除非虚拟机在执行前退出),无论try块中的代码是否正常执行、是否抛出异常、或者try/catch块中是否有return语句。通常用finally来释放资源,比如关闭文件流、数据库连接等,确保资源不会因异常而泄漏。

sleep和wait都能让线程暂停执行,但它们的设计目的、使用场景和底层机制有显著区别。

简单来说,sleep是 “定时休眠”,不释放锁,用于控制线程执行节奏;wait是 “等待唤醒”,必须配合锁使用,用于线程间协作,释放锁让其他线程有机会执行。

sleep是Thread类的静态方法,它的作用是让当前线程暂停指定的时间(毫秒或纳秒),期间线程会让出 CPU 执行权,但不会释放锁资源(如果持有锁的话)。时间结束后,线程会自动恢复运行状态,等待 CPU 调度。它通常用于 “让当前线程执行一段固定时间的暂停”,比如模拟网络延迟、控制任务执行节奏等,使用时不需要依赖同步锁,直接通过Thread.sleep(time)调用。

wait是Object类的方法,必须在同步代码块(synchronized)或同步方法中使用,调用时会让当前线程释放所持有的对象锁,并进入该对象的等待队列,直到被其他线程通过notify或notifyAll唤醒,或等待超时后才会恢复。它的核心用途是实现线程间的协作,比如生产者 - 消费者模型中,当队列满时生产者线程等待,队列空时消费者线程等待,通过唤醒机制实现线程间的通信与同步。

sleep是Thread类的静态方法,它的作用是让当前线程暂停指定的时间(毫秒或纳秒),期间线程会让出 CPU 执行权,但不会释放锁资源(如果持有锁的话)。时间结束后,线程会自动恢复运行状态,等待 CPU 调度。它通常用于 “让当前线程执行一段固定时间的暂停”,比如模拟网络延迟、控制任务执行节奏等,使用时不需要依赖同步锁,直接通过Thread.sleep(time)调用。

wait是Object类的方法,必须在同步代码块(synchronized)或同步方法中使用,调用时会让当前线程释放所持有的对象锁,并进入该对象的等待队列,直到被其他线程通过notify或notifyAll唤醒,或等待超时后才会恢复。它的核心用途是实现线程间的协作,比如生产者 - 消费者模型中,当队列满时生产者线程等待,队列空时消费者线程等待,通过唤醒机制实现线程间的通信与同步。

HashMap put流程如下图:

同花顺开始谈薪了,有点紧张。。。

HashMap HashMap的put方法用于向HashMap中添加键值对,当调用HashMap的put方法时,会按照以下详细流程执行(JDK8 1.8版本):

第一步:根据要添加的键的哈希码计算在数组中的位置(索引)。

第一步:根据要添加的键的哈希码计算在数组中的位置(索引)。

第二步:检查该位置是否为空(即没有键值对存在)

第二步:检查该位置是否为空(即没有键值对存在)

如果为空,则直接在该位置创建一个新的Entry对象来存储键值对。将要添加的键值对作为该Entry的键和值,并保存在数组的对应位置。将HashMap的修改次数(modCount)加1,以便在进行迭代时发现并发修改。

如果为空,则直接在该位置创建一个新的Entry对象来存储键值对。将要添加的键值对作为该Entry的键和值,并保存在数组的对应位置。将HashMap的修改次数(modCount)加1,以便在进行迭代时发现并发修改。

第三步:如果该位置已经存在其他键值对,检查该位置的第一个键值对的哈希码和键是否与要添加的键值对相同?

第三步:如果该位置已经存在其他键值对,检查该位置的第一个键值对的哈希码和键是否与要添加的键值对相同?

如果相同,则表示找到了相同的键,直接将新的值替换旧的值,完成更新操作。

如果相同,则表示找到了相同的键,直接将新的值替换旧的值,完成更新操作。

第四步:如果第一个键值对的哈希码和键不相同,则需要遍历链表或红黑树来查找是否有相同的键:

第四步:如果第一个键值对的哈希码和键不相同,则需要遍历链表或红黑树来查找是否有相同的键:

如果键值对集合是链表结构,从链表的头部开始逐个比较键的哈希码和equals方法,直到找到相同的键或达到链表末尾。

如果找到了相同的键,则使用新的值取代旧的值,即更新键对应的值。

如果没有找到相同的键,则将新的键值对添加到链表的头部。

如果找到了相同的键,则使用新的值取代旧的值,即更新键对应的值。

如果没有找到相同的键,则将新的键值对添加到链表的头部。

如果键值对集合是红黑树结构,在红黑树中使用哈希码和equals方法进行查找。根据键的哈希码,定位到红黑树中的某个节点,然后逐个比较键,直到找到相同的键或达到红黑树末尾。

如果找到了相同的键,则使用新的值取代旧的值,即更新键对应的值。

如果没有找到相同的键,则将新的键值对添加到红黑树中。

如果找到了相同的键,则使用新的值取代旧的值,即更新键对应的值。

如果没有找到相同的键,则将新的键值对添加到红黑树中。

第五步:检查链表长度是否达到阈值(默认为8):

第五步:检查链表长度是否达到阈值(默认为8):

如果链表长度超过阈值,且HashMap的数组长度大于等于64,则会将链表转换为红黑树,以提高查询效率。

如果链表长度超过阈值,且HashMap的数组长度大于等于64,则会将链表转换为红黑树,以提高查询效率。

第六步:检查负载因子是否超过阈值(默认为0.75):

第六步:检查负载因子是否超过阈值(默认为0.75):

如果键值对的数量(size)与数组的长度的比值大于阈值,则需要进行扩容操作。

如果键值对的数量(size)与数组的长度的比值大于阈值,则需要进行扩容操作。

第七步:扩容操作:

第七步:扩容操作:

创建一个新的两倍大小的数组。

将旧数组中的键值对重新计算哈希码并分配到新数组中的位置。

更新HashMap的数组引用和阈值参数。

创建一个新的两倍大小的数组。

将旧数组中的键值对重新计算哈希码并分配到新数组中的位置。

更新HashMap的数组引用和阈值参数。

第八步:完成添加操作。

第八步:完成添加操作。

此外,HashMap是非线程安全的,如果在多线程环境下使用,需要采取额外的同步措施或使用线程安全的ConcurrentHashMap。

5. HashMap 扩容机制是怎样的?

hashMap默认的负载因子是0.75,即如果hashmap中的元素个数超过了总容量75%,则会触发扩容,扩容分为两个步骤:

第1步是对哈希表长度的扩展(2倍)

第2步是将旧哈希表中的数据放到新的哈希表中。

第1步是对哈希表长度的扩展(2倍)

第2步是将旧哈希表中的数据放到新的哈希表中。

因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。

如我们从16扩展为32时,具体的变化如下所示:

因此元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。可以看看下图为16扩充为32的resize示意图:

同花顺开始谈薪了,有点紧张。。。

这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。

6. 深拷贝和浅拷贝区别是什么?

同花顺开始谈薪了,有点紧张。。。

浅拷贝是指只复制对象本身和其内部的值类型字段,但不会复制对象内部的引用类型字段。换句话说,浅拷贝只是创建一个新的对象,然后将原对象的字段值复制到新对象中,但如果原对象内部有引用类型的字段,只是将引用复制到新对象中,两个对象指向的是同一个引用对象。

深拷贝是指在复制对象的同时,将对象内部的所有引用类型字段的内容也复制一份,而不是共享引用。换句话说,深拷贝会递归复制对象内部所有引用类型的字段,生成一个全新的对象以及其内部的所有对象。

浅拷贝是指只复制对象本身和其内部的值类型字段,但不会复制对象内部的引用类型字段。换句话说,浅拷贝只是创建一个新的对象,然后将原对象的字段值复制到新对象中,但如果原对象内部有引用类型的字段,只是将引用复制到新对象中,两个对象指向的是同一个引用对象。

深拷贝是指在复制对象的同时,将对象内部的所有引用类型字段的内容也复制一份,而不是共享引用。换句话说,深拷贝会递归复制对象内部所有引用类型的字段,生成一个全新的对象以及其内部的所有对象。

强引用是最常见的引用类型,我们日常编程中创建的对象默认都是强引用(如 Object obj = new Object)。只要存在强引用,垃圾回收器就不会回收被引用的对象,即使内存不足时抛出 OutOfMemoryError也不会回收。

软引用通过 SoftReference类实现(如 SoftReference<Object> softRef = new SoftReference<>(new Object))。当内存充足时,软引用的对象不会被回收;当内存不足时,垃圾回收器会回收这些对象,这一特性使其适合用于实现缓存(如图片缓存),在内存紧张时释放缓存内容以避免 OOM。

弱引用通过 WeakReference类实现(如 WeakReference<Object> weakRef = new WeakReference<>(new Object))。它的生命周期比软引用更短,无论内存是否充足,只要垃圾回收器开始工作,就会回收被弱引用关联的对象。弱引用常用于需要关联但不影响对象回收的场景,比如 WeakHashMap中,当键不再被强引用时,对应的键值对会被自动移除。

虚引用也叫幽灵引用,通过 PhantomReference类实现,它必须与引用队列(ReferenceQueue)配合使用。虚引用不会影响对象的生命周期,甚至无法通过虚引用获取对象实例,其唯一作用是在对象被垃圾回收时,会将虚引用加入关联的队列,以此通知程序该对象已被回收,可用于管理直接内存(如跟踪 DirectByteBuffer的回收,避免内存泄漏)。

强引用是最常见的引用类型,我们日常编程中创建的对象默认都是强引用(如 Object obj = new Object)。只要存在强引用,垃圾回收器就不会回收被引用的对象,即使内存不足时抛出 OutOfMemoryError也不会回收。

软引用通过 SoftReference类实现(如 SoftReference<Object> softRef = new SoftReference<>(new Object))。当内存充足时,软引用的对象不会被回收;当内存不足时,垃圾回收器会回收这些对象,这一特性使其适合用于实现缓存(如图片缓存),在内存紧张时释放缓存内容以避免 OOM。

弱引用通过 WeakReference类实现(如 WeakReference<Object> weakRef = new WeakReference<>(new Object))。它的生命周期比软引用更短,无论内存是否充足,只要垃圾回收器开始工作,就会回收被弱引用关联的对象。弱引用常用于需要关联但不影响对象回收的场景,比如 WeakHashMap中,当键不再被强引用时,对应的键值对会被自动移除。

虚引用也叫幽灵引用,通过 PhantomReference类实现,它必须与引用队列(ReferenceQueue)配合使用。虚引用不会影响对象的生命周期,甚至无法通过虚引用获取对象实例,其唯一作用是在对象被垃圾回收时,会将虚引用加入关联的队列,以此通知程序该对象已被回收,可用于管理直接内存(如跟踪 DirectByteBuffer的回收,避免内存泄漏)。

同花顺开始谈薪了,有点紧张。。。

分请求报文和响应报文来说明。

请求报文:

请求行:包含请求方法、请求目标(URL或URI)和HTTP协议版本。

请求头部:包含关于请求的附加信息,如Host、User-Agent、Content-Type等。

空行:请求头部和请求体之间用空行分隔。

请求体:可选,包含请求的数据,通常用于POST请求等需要传输数据的情况。

请求行:包含请求方法、请求目标(URL或URI)和HTTP协议版本。

请求头部:包含关于请求的附加信息,如Host、User-Agent、Content-Type等。

空行:请求头部和请求体之间用空行分隔。

请求体:可选,包含请求的数据,通常用于POST请求等需要传输数据的情况。

响应报文:

状态行:包含HTTP协议版本、状态码和状态信息。

响应头部:包含关于响应的附加信息,如Content-Type、Content-Length等。

空行:响应头部和响应体之间用空行分隔。

响应体:包含响应的数据,通常是服务器返回的HTML、JSON等内容。

状态行:包含HTTP协议版本、状态码和状态信息。

响应头部:包含关于响应的附加信息,如Content-Type、Content-Length等。

空行:响应头部和响应体之间用空行分隔。

响应体:包含响应的数据,通常是服务器返回的HTML、JSON等内容。

同花顺开始谈薪了,有点紧张。。。

大类

1xx 类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。

2xx 类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。

3xx 类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向。

4xx 类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。

5xx 类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。

1xx 类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。

2xx 类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。

3xx 类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向。

4xx 类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。

5xx 类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。

其中常见的具体状态码有:

200:请求成功;

301:永久重定向;302:临时重定向;

404:无法找到此页面;405:请求的方法类型不支持;

500:服务器内部出错。

200:请求成功;

301:永久重定向;302:临时重定向;

404:无法找到此页面;405:请求的方法类型不支持;

500:服务器内部出错。

206什么时候用?

206什么时候用?

当客户端仅需要获取资源的一部分(而非完整资源)时,服务器返回 206 状态码,同时返回对应的部分数据。

触发场景比如有:

下载大文件(如视频、压缩包)时,若中途中断,客户端可通过请求头指定 “需要的字节范围”,服务器返回 206 和对应片段,实现 “续传”。

在线播放视频时,客户端先请求 “开头的 100KB 数据” 用于加载,后续再请求 “下一段数据”,避免一次性加载完整大文件(节省带宽和时间)。

客户端主动将大文件拆分为多个片段(如每片 1MB),分多次请求不同片段,服务器每次返回 206 和对应片段,最后客户端合并片段。

下载大文件(如视频、压缩包)时,若中途中断,客户端可通过请求头指定 “需要的字节范围”,服务器返回 206 和对应片段,实现 “续传”。

在线播放视频时,客户端先请求 “开头的 100KB 数据” 用于加载,后续再请求 “下一段数据”,避免一次性加载完整大文件(节省带宽和时间)。

客户端主动将大文件拆分为多个片段(如每片 1MB),分多次请求不同片段,服务器每次返回 206 和对应片段,最后客户端合并片段。

304什么时候用?

304什么时候用?

客户端之前已请求过该资源,本次请求时服务器判断 「资源自上次请求后未发生变化」,因此不返回资源体(仅返回响应头),客户端直接使用本地缓存的资源 ,目的减少重复数据传输,提升性能。

触发场景:

静态资源缓存:网站的 CSS、JS、图片等静态资源(内容很少变化),客户端首次请求后缓存到本地,后续再次请求时,服务器返回 304,客户端直接用缓存,无需重新下载。

页面二次访问:用户刷新页面时,若页面依赖的资源(如 logo 图片)未修改,服务器返回 304,加快页面加载速度。

静态资源缓存:网站的 CSS、JS、图片等静态资源(内容很少变化),客户端首次请求后缓存到本地,后续再次请求时,服务器返回 304,客户端直接用缓存,无需重新下载。

页面二次访问:用户刷新页面时,若页面依赖的资源(如 logo 图片)未修改,服务器返回 304,加快页面加载速度。

简单来说,当用户第一次访问某个网页时,浏览器需要从服务器下载所有所需资源;但当用户再次访问该网页(或刷新页面)时,若资源未发生变化,浏览器可直接使用本地存储的缓存资源,无需重新从服务器下载 ,这就是 HTTP 缓存的核心价值。

响应头控制,客户端(浏览器)根据响应头的规则执行缓存逻辑。核心分为两大策略:强缓存和协商缓存,两者通常配合使用。

强缓存由浏览器自主判断是否使用本地缓存,无需与服务器通信,它通过响应头中的Cache-Control(如max-age指定缓存有效期)或Expires(指定缓存过期时间点)来确定资源是否仍在有效期内,若未过期则直接从本地读取缓存资源,完全跳过服务器请求。

如果强缓存失效,才会进入协商缓存流程,此时浏览器会携带资源的标识信息(如If-Modified-Since对应资源的Last-Modified时间,或If-None-Match对应资源的ETag唯一标识)向服务器发起请求,服务器通过这些标识判断资源是否有更新,若资源未变则返回 304 状态码,告知浏览器继续使用本地缓存,若资源已更新则返回 200 状态码和新资源,同时更新缓存标识和缓存规则。

11. 线程池参数有哪些?原理是什么?

线程在正常执行或者异常中断时会被销毁,如果频繁的创建很多线程,不仅会消耗系统资源,还会降低系统的稳定性,一不小心把系统搞崩了。

所以,线程池是为了减少频繁的创建线程和销毁线程带来的性能损耗,线程池的工作原理如下图:

同花顺开始谈薪了,有点紧张。。。

当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。首先,所有任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:

首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。

如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。

如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。

如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。

如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。

如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。

如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。

如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。

如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

ThreadPoolExecutor的核心构造函数确实有这7个参数:

同花顺开始谈薪了,有点紧张。。。

corePoolSize: 是线程池的核心线程数量。默认情况下,只要线程数不超过这个值,即使这些线程处于空闲状态,也不会被销毁,始终保持在线程池中。

maximumPoolSize: 限制了线程池能创建的最大线程总数(包括核心线程和非核心线程),当 corePoolSize已满 并且 尝试将新任务加入阻塞队列失败(即队列已满)并且 当前线程数 < maximumPoolSize,就会创建新线程执行此任务,但是当 corePoolSize满 并且 队列满 并且 线程数已达 maximumPoolSize并且 又有新任务提交时,就会触发拒绝策略。

keepAliveTime: 用于控制非核心线程的存活时间:当线程池中线程数超过 corePoolSize 时,那些空闲时间超过这个值的非核心线程会被销毁,避免资源浪费。

unit:是 keepAliveTime 的时间单位,比如秒、毫秒等。

workQueue:是工作队列,用于存放暂时无法被执行的任务。当没有空闲线程时,新任务会先进入队列等待,直到有线程空闲后从队列中获取任务执行。

threadFactory:是线程工厂,用于创建新线程,可自定义线程的名称、优先级等属性,方便线程管理和问题排查。

handler: 是拒绝策略,当线程池无法处理新任务时(队列已满且线程数达到 maximumPoolSize),就会通过拒绝策略决定如何处理这个任务,比如直接丢弃、让提交任务的线程自己执行等。

corePoolSize: 是线程池的核心线程数量。默认情况下,只要线程数不超过这个值,即使这些线程处于空闲状态,也不会被销毁,始终保持在线程池中。

maximumPoolSize: 限制了线程池能创建的最大线程总数(包括核心线程和非核心线程),当 corePoolSize已满 并且 尝试将新任务加入阻塞队列失败(即队列已满)并且 当前线程数 < maximumPoolSize,就会创建新线程执行此任务,但是当 corePoolSize满 并且 队列满 并且 线程数已达 maximumPoolSize并且 又有新任务提交时,就会触发拒绝策略。

keepAliveTime: 用于控制非核心线程的存活时间:当线程池中线程数超过 corePoolSize 时,那些空闲时间超过这个值的非核心线程会被销毁,避免资源浪费。

unit:是 keepAliveTime 的时间单位,比如秒、毫秒等。

workQueue:是工作队列,用于存放暂时无法被执行的任务。当没有空闲线程时,新任务会先进入队列等待,直到有线程空闲后从队列中获取任务执行。

threadFactory:是线程工厂,用于创建新线程,可自定义线程的名称、优先级等属性,方便线程管理和问题排查。

handler: 是拒绝策略,当线程池无法处理新任务时(队列已满且线程数达到 maximumPoolSize),就会通过拒绝策略决定如何处理这个任务,比如直接丢弃、让提交任务的线程自己执行等。

常见的垃圾回收机制(算法和收集器)可从回收算法和具体收集器实 两方面理解。

垃圾回收算法

垃圾回收算法

标记 - 清除算法:垃圾回收算法里,标记 - 清除算法分两步走。先遍历所有对象,把还在被引用的存活对象做上标记,之后再把没标记的垃圾对象清理掉。它实现起来简单,但问题是会留下很多内存碎片,可能导致后面大对象找不到连续的内存空间。

标记 - 复制算法:标记 - 复制算法是把内存分成两块一样大的区域,每次只用其中一块。标记完存活对象后,就把它们复制到另一块没用到的区域,然后把当前这块的所有对象都清掉。这样做解决了内存碎片的问题,复制过程中还能自动整理内存,不过内存利用率只有一半,更适合存活对象少的场景,比如新生代。

标记 - 整理算法:标记 - 整理算法综合了前两种的优点。标记完存活对象后,不直接删垃圾,而是把所有存活对象挪到内存的一端,再把边界外的垃圾全清掉。这样既没有内存碎片,也不用浪费一半内存,只是移动对象会增加些开销,适合存活对象多的场景,像老年代。

分代收集算法:分代收集算法是现在主流虚拟机比如 HotSpot 采用的思路。它根据对象存活时间把内存分成不同区域,像新生代和老年代,不同区域用不同的算法。新生代里的对象存活时间短、数量多,就用标记 - 复制算法,效率高;老年代的对象存活时间长、数量少,就用标记 - 清除或标记 - 整理算法,减少内存碎片。

标记 - 清除算法:垃圾回收算法里,标记 - 清除算法分两步走。先遍历所有对象,把还在被引用的存活对象做上标记,之后再把没标记的垃圾对象清理掉。它实现起来简单,但问题是会留下很多内存碎片,可能导致后面大对象找不到连续的内存空间。

标记 - 复制算法:标记 - 复制算法是把内存分成两块一样大的区域,每次只用其中一块。标记完存活对象后,就把它们复制到另一块没用到的区域,然后把当前这块的所有对象都清掉。这样做解决了内存碎片的问题,复制过程中还能自动整理内存,不过内存利用率只有一半,更适合存活对象少的场景,比如新生代。

标记 - 整理算法:标记 - 整理算法综合了前两种的优点。标记完存活对象后,不直接删垃圾,而是把所有存活对象挪到内存的一端,再把边界外的垃圾全清掉。这样既没有内存碎片,也不用浪费一半内存,只是移动对象会增加些开销,适合存活对象多的场景,像老年代。

分代收集算法:分代收集算法是现在主流虚拟机比如 HotSpot 采用的思路。它根据对象存活时间把内存分成不同区域,像新生代和老年代,不同区域用不同的算法。新生代里的对象存活时间短、数量多,就用标记 - 复制算法,效率高;老年代的对象存活时间长、数量少,就用标记 - 清除或标记 - 整理算法,减少内存碎片。

常见垃圾收集器(基于 HotSpot 虚拟机)

常见垃圾收集器(基于 HotSpot 虚拟机)

Serial GC:Serial GC 是串行收集器,单线程执行垃圾回收,回收的时候会暂停所有用户线程。它简单高效,适合单核 CPU 或者内存小的应用比如客户端程序,但在多核环境下性能不太好。

Parallel GC:Parallel GC 是并行收集器,多线程执行垃圾回收,还是会暂停用户线程,但回收速度更快,更看重吞吐量,也就是用户代码运行时间和总时间的比例。适合后台计算这类对响应时间要求不高的场景。

CMS收集器:CMS 收集器以低延迟为目标,大部分阶段都能和用户线程一起运行,只在初始标记和重新标记的时候短暂暂停用户线程。它基于标记 - 清除算法,可能产生内存碎片,适合 Web 服务这种对响应时间敏感的应用,不过比较耗 CPU。

G1收集器:G1 收集器适合大内存场景,把内存分成多个大小相同的区域,优先回收垃圾最多的区域。它结合了分代思想和区域化管理,兼顾吞吐量和延迟,还能动态调整停顿时间,是 JDK9 及以上版本的默认收集器。

ZGC:ZGC 是新一代的低延迟收集器,几乎能做到毫秒甚至微秒级的停顿,适合百 GB 级这种超大堆内存的场景。它通过并发处理和更高效的内存布局,实现了高吞吐量和低延迟,是高性能应用的不错选择。

Serial GC:Serial GC 是串行收集器,单线程执行垃圾回收,回收的时候会暂停所有用户线程。它简单高效,适合单核 CPU 或者内存小的应用比如客户端程序,但在多核环境下性能不太好。

Parallel GC:Parallel GC 是并行收集器,多线程执行垃圾回收,还是会暂停用户线程,但回收速度更快,更看重吞吐量,也就是用户代码运行时间和总时间的比例。适合后台计算这类对响应时间要求不高的场景。

CMS收集器:CMS 收集器以低延迟为目标,大部分阶段都能和用户线程一起运行,只在初始标记和重新标记的时候短暂暂停用户线程。它基于标记 - 清除算法,可能产生内存碎片,适合 Web 服务这种对响应时间敏感的应用,不过比较耗 CPU。

G1收集器:G1 收集器适合大内存场景,把内存分成多个大小相同的区域,优先回收垃圾最多的区域。它结合了分代思想和区域化管理,兼顾吞吐量和延迟,还能动态调整停顿时间,是 JDK9 及以上版本的默认收集器。

ZGC:ZGC 是新一代的低延迟收集器,几乎能做到毫秒甚至微秒级的停顿,适合百 GB 级这种超大堆内存的场景。它通过并发处理和更高效的内存布局,实现了高吞吐量和低延迟,是高性能应用的不错选择。

最大公约数

最大公约数

评论