news 2026/2/6 21:07:02

同程旅行开的薪资太低,果断拒了!

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
同程旅行开的薪资太低,果断拒了!

在酒旅领域,大多数人首先想到的是携程与飞猪,其中携程更是占据着最大的市场份额。然而,在这两大巨头之外,同程旅行(前身为同程艺龙)依然稳稳地占据着一席之地。

我对同程的印象,还停留在六年前校招季的初次接触,当时感觉这家公司整体还是不错的。然而,随着市场的风云变幻,我一度以为它已在激烈的竞争中逐渐沉寂。

看到有球友面试同程,加上前段时间同程放出了还不错的业绩报告,我才知道这家公司目前依然发展的还不错。

这家公司的技术面试一般由笔试+技术面试两轮+HR 面组成,整体难度一般,但对学历相对包容,二本和双非也可投递试试。

根据前几届的同学反馈来看,同程会要求提前实习且可能会卡转正。另外,校招薪资整体处于行业中游水平。以 24 届为例,苏州 Java 岗位的薪酬范围大致在(12-14k) * 15 薪

同程旅行的校招在前几天也已经开始了,这里给大家分享一位球友的同程校招一面面经,大家感受一下难度如何。

概览:

HashMap 是否线程安全?不安全的话用什么?

HashMap不是线程安全的。在多线程环境下对HashMap进行并发写操作,可能会导致两种主要问题:

  1. 数据丢失:并发put操作可能导致一个线程的写入被另一个线程覆盖。

  2. 无限循环:在 JDK 7 及以前的版本中,并发扩容时,由于头插法可能导致链表形成环,从而在get操作时引发无限循环,CPU 飙升至 100%。

ConcurrentHashMapHashMap的线程安全版本。这意味着它可以保证多个线程同时对它进行读写操作时,不会出现数据不一致的情况,也不会导致 JDK1.7 及之前版本的HashMap多线程操作导致死循环问题。但是,这并不意味着它可以保证所有的复合操作都是原子性的,一定不要搞混了!

ConcurrentHashMap 是如何保证线程安全的?

一句话总结:JDK 1.7 采用Segment分段锁来保证安全,Segment是继承自ReentrantLock。JDK1.8 放弃了Segment分段锁的设计,采用Node + CAS + synchronized保证线程安全,锁粒度更细,synchronized只锁定当前链表或红黑二叉树的首节点。

下面是详细介绍。

JDK1.8 之前

Java7 ConcurrentHashMap 存储结构

首先将数据分为一段一段(这个“段”就是Segment)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成

Segment继承了ReentrantLock,所以Segment是一种可重入锁,扮演锁的角色。HashEntry用于存储键值对数据。

static class Segment<K,V> extends ReentrantLock implements Serializable { }

一个ConcurrentHashMap里包含一个Segment数组,Segment的个数一旦初始化就不能改变Segment数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。

Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment的锁。也就是说,对同一Segment的并发写入会被阻塞,不同Segment的写入是可以并发执行的。

JDK1.8 之后

Java8 ConcurrentHashMap 存储结构

Java 8 几乎完全重写了ConcurrentHashMap,代码量从原来 Java 7 中的 1000 多行,变成了现在的 6000 多行。

ConcurrentHashMap取消了Segment分段锁,采用Node + CAS + synchronized来保证并发安全。数据结构跟HashMap1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。

Java 8 中,锁粒度更细,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。

AQS, ReentrantLock, synchronized 的区别

synchronized是 Java 关键字,由 JVM 层面实现。简单易用,编译器会自动加锁和释放锁。默认是非公平锁,且是悲观锁。功能相对简单,无法中断、无法设置超时。

ReentrantLock是 JDK 层面实现的(也就是 API 层面),需要lock()unlock()方法配合try/finally语句块来完成。默认非公平,但可配置为公平锁。功能非常强大,支持可中断获取锁、可超时获取锁、可尝试获取锁,并且可以绑定多个Condition实现精准唤醒。

AQS (AbstractQueuedSynchronizer,抽象队列同步器)是从 JDK1.5 开始提供的 Java 并发核心组件。

AQS 解决了开发者在实现同步器时的复杂性问题。它提供了一个通用框架,用于实现各种同步器,例如可重入锁ReentrantLock)、信号量Semaphore)和倒计时器CountDownLatch)。通过封装底层的线程同步机制,AQS 将复杂的线程管理逻辑隐藏起来,使开发者只需专注于具体的同步逻辑。简单来说,AQS 是一个抽象类,为同步器提供了通用的执行框架。它定义了资源获取和释放的通用流程,而具体的资源获取逻辑则由具体同步器通过重写模板方法来实现。 因此,可以将 AQS 看作是同步器的基础“底座”,而同步器则是基于 AQS 实现的具体“应用”

ThreadLocal 能说多少说多少

ThreadLocal提供了一种线程内部的局部变量机制,它可以在同一个线程的执行周期内,让不同的方法都能方便地访问到同一个变量,而不需要通过参数层层传递。

最终的变量是放在了当前线程的ThreadLocalMap中,并不是存在ThreadLocal上,ThreadLocal可以理解为只是ThreadLocalMap的封装,传递了变量值。ThrealLocal类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。

ThreadLocal数据结构如下图所示:

ThreadLocal 数据结构

在线程池中使用ThreadLocal时,要特别小心内存泄漏。当ThreadLocal实例失去强引用后,其对应的 value 仍然存在于ThreadLocalMap中,因为Entry对象强引用了它。如果线程持续存活(例如线程池中的线程),ThreadLocalMap也会一直存在,导致 key 为null的 entry 无法被垃圾回收,即会造成内存泄漏。

如何避免内存泄漏的发生?

  1. 在使用完ThreadLocal后,务必调用remove()方法。 这是最安全和最推荐的做法。remove()方法会从ThreadLocalMap中显式地移除对应的 entry,彻底解决内存泄漏的风险。 即使将ThreadLocal定义为static final,也强烈建议在每次使用后调用remove()

  2. 在线程池等线程复用的场景下,使用try-finally块可以确保即使发生异常,remove()方法也一定会被执行。

主线程的 ThreadLocal 访问子线程的变量,怎么处理?

由于ThreadLocal的变量值存放在Thread里,而父子线程属于不同的Thread的。因此在异步场景下,父子线程的ThreadLocal值无法进行传递。

如果想要在异步场景下传递ThreadLocal值,有两种解决方案:

  • InheritableThreadLocalInheritableThreadLocal是 JDK1.2 提供的工具,继承自ThreadLocal。使用InheritableThreadLocal时,会在创建子线程时,令子线程继承父线程中的ThreadLocal值,但是无法支持线程池场景下的ThreadLocal值传递。

  • TransmittableThreadLocalTransmittableThreadLocal(简称 TTL) 是阿里巴巴开源的工具类,继承并加强了InheritableThreadLocal类,可以在线程池的场景下支持ThreadLocal值传递。项目地址:https://github.com/alibaba/transmittable-thread-local。

MySQL 的索引数据结构

在 MySQL 中,MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构。

B+树的优势:

  • 矮胖的树形结构:B+树是多路平衡查找树。它的非叶子节点只存储键(索引),不存储数据,这使得每个节点可以容纳成百上千个键。因此,即使存储上亿条数据,B+树的高度通常也只有 3-4 层,极大地减少了磁盘 I/O 次数。

  • 所有数据都在叶子节点:所有数据记录都存储在叶子节点上,这使得查询性能非常稳定,任何查询都需要从根节点走到叶子节点。

  • 有序的叶子节点链表:B+树的叶子节点之间通过一个双向链表连接,并且是有序的。这个特性使得它在进行范围查询时极其高效,只需要定位到范围的起始点,然后沿着链表顺序遍历即可。

  • 更适合磁盘存储:B+树的节点大小被设计为与磁盘页(Page)的大小相近,可以充分利用磁盘的预读特性,一次 I/O 就能加载一个完整的节点到内存中。

时间复杂度怎么计算?

时间复杂度是用来衡量一个算法执行时间随数据规模(n)增长而变化的趋势,它描述的是一种增长率,而不是精确的执行时间。我们通常使用大 O 表示法来表示。

一个算法的时间复杂度,通常由嵌套最深、执行次数最多的那段代码决定。总复杂度等于量级最大的那段代码的复杂度,例如 O(n) + O(n^2) = O(n^2)。嵌套代码的复杂度等于内外代码复杂度的乘积。一个循环嵌套另一个循环,就是 O(n * n) = O(n^2)。

RAG 技术

RAG,全称是检索增强生成 (Retrieval-Augmented Generation)。它是一种将外部知识库与大语言模型 (LLM) 相结合的技术框架,旨在解决大模型存在的这些痛点:

  1. 知识的实时性问题:大模型的知识是截止到它训练时的,无法获取最新的信息(比如今天的热点新闻)。

  2. 幻觉问题:当被问到其知识范围外或专业性极强的问题时,大模型可能会编造答案。

  3. 私有领域知识问题:大模型不知道你公司的内部文档、产品手册等私有知识。

RAG 过程分为两个不同阶段:索引检索

在索引阶段,文档会进行预处理,以便在检索阶段实现高效搜索。该阶段通常包括以下步骤:

  1. 输入文档:文档是需要被处理的内容来源,可能是文本文件、PDF、网页、数据库记录等。

  2. 清理文档:对文档进行去噪处理,移除无用内容(如 HTML 标签、特殊字符)。

  3. 增强文档:利用附加数据和元数据(如时间戳、分类标签)为文档片段提供更多上下文信息。

  4. 文档拆分:通过文本分割器(Text Splitter)将文档拆分为较小的文本片段(Segments),以适应检索和生成的上下文长度限制(例如 GPT 的 token 限制)。

  5. 生成嵌入:通过嵌入模型(如 Sentence-BERT 或 OpenAI Embedding)将每个片段转换为向量表示,以捕捉其语义信息。

  6. 存储到向量数据库:将生成的嵌入和对应的元数据存储在嵌入存储库(如 Faiss、Annoy、Pinecone、Weaviate 或本地向量数据库)。

索引过程通常是离线完成的,例如通过定时任务(如每周末更新文档)进行重新索引。对于动态需求,例如用户上传文档的场景,索引可以在线完成,并集成到主应用程序中。

索引阶段的简化流程图如下(图源 langchain4j 官方文档):

检索通常在线进行的,当用户提交一个问题时,系统会使用已索引的文档来回答问题。该阶段通常包括以下步骤:

  1. 接收请求:接收用户的自然语言查询(Query),例如一个问题或任务描述。

  2. 查询向量化:使用嵌入模型(Embedding Model)将用户查询转换为语义向量表示(Query Embedding),以捕捉查询的语义信息。

  3. 信息检索 (R):在嵌入存储(Embedding Store)中,通过语义相似性搜索找到与查询向量最相关的文档片段(Relevant Segments)。

  4. 生成增强 (A):将检索到的相关片段(Relevant Segments)和原始查询作为上下文输入给 LLM,并使用合适的提示词引导 LLM 基于检索到的信息回答问题。

  5. 输出生成 (G):LLM 生成最终答案。

  6. 结果反馈(可选):如果用户对生成的结果不满意,可以允许用户提供反馈,通过调整提示词或检索方式优化生成效果。在某些实现中,支持多轮交互,进一步完善回答。

检索阶段的简化流程图如下

用 SpringAI 做了那些事

Spring AI 为整个 RAG 流程提供了接近一站式的解决方案。从文档加载、切分,到向量化、存储、检索,再到与大模型的交互,都有非常好的抽象和封装,让我可以不用关心底层具体模型的 API 差异,专注于业务逻辑的实现,开发效率大大提升。

另外,Spring AI 的 Function Calling 机制,优雅地打通了 AI 的自然语言理解能力和我们后端系统的业务能力,让 AI 真正成为了我们业务系统的一个智能入口。

SQL 优化你的思路和出发点

回答这个问题的核心是先提到开启慢查询日志和使用 EXPLAIN 进行执行计划分析。

慢查询日志捕获那些执行时间超过阈值的 SQL 语句,这是发现问题的起点。拿到慢 SQL 后,用 EXPLAIN 关键字分析这条 SQL 的执行计划,分析原因。

基于 EXPLAIN 的分析结果,进行针对性优化。比较常见的 SQL 优化手段如下:

  1. 索引优化(最常用)

  2. 避免SELECT *

  3. 深度分页优化

  4. 尽量避免多表做 join

  5. 选择合适的字段类型

  6. ......

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/4 18:02:17

专业学术论文查重工具推荐:9款高效软件与实用技巧盘点

核心工具对比速览 工具名称 核心功能 处理时间 适配检测平台 特色优势 aibiye 降AIGC查重 20分钟 知网/格子达/维普 保留学术术语的AI痕迹弱化 aicheck AIGC检测降重 即时 主流学术平台 实时检测反馈精准降重 askpaper 学术AI优化 15-30分钟 高校常用系统 专…

作者头像 李华
网站建设 2026/2/3 13:03:17

告别3天调试:AI驱动的黑苹果配置革命

告别3天调试&#xff1a;AI驱动的黑苹果配置革命 【免费下载链接】OpCore-Simplify A tool designed to simplify the creation of OpenCore EFI 项目地址: https://gitcode.com/GitHub_Trending/op/OpCore-Simplify 副标题&#xff1a;从硬件扫描到EFI生成&#xff0c;…

作者头像 李华
网站建设 2026/2/4 5:39:57

ERNIE 4.5黑科技:2比特量化单GPU驾驭300B大模型

ERNIE 4.5黑科技&#xff1a;2比特量化单GPU驾驭300B大模型 【免费下载链接】ERNIE-4.5-300B-A47B-2Bits-Paddle 项目地址: https://ai.gitcode.com/hf_mirrors/baidu/ERNIE-4.5-300B-A47B-2Bits-Paddle 百度ERNIE 4.5推出2比特量化版本&#xff08;ERNIE-4.5-300B-A47…

作者头像 李华
网站建设 2026/2/4 17:43:07

游游的二进制树【牛客tracker 每日一题】

游游的二进制树 时间限制&#xff1a;1秒 空间限制&#xff1a;256M 网页链接 牛客tracker 牛客tracker & 每日一题&#xff0c;完成每日打卡&#xff0c;即可获得牛币。获得相应数量的牛币&#xff0c;能在【牛币兑换中心】&#xff0c;换取相应奖品&#xff01;助力每…

作者头像 李华