2025面试经验-京东零售-Java开发

ES深度分页

解决方案

1. 滚动搜索(Scroll API)
  • 原理:滚动搜索通过创建一个 “快照” 来避免深度分页问题。在第一次查询时,ES 会创建一个滚动上下文,并返回一个滚动 ID,后续的查询使用这个滚动 ID 来获取下一页的结果,直到滚动上下文过期或没有更多结果为止。
  • 示例(Python)

收起

python

from elasticsearch import Elasticsearch

# 连接到 Elasticsearch
es = Elasticsearch([{'host': 'localhost', 'port': 9200}])

# 定义索引名称
index_name = 'your_index'

# 每页文档数量
page_size = 10

# 第一次查询,创建滚动上下文
result = es.search(
    index=index_name,
    body={"query": {"match_all": {}}},
    scroll='1m',  # 滚动上下文有效期为 1 分钟
    size=page_size
)

# 获取滚动 ID
scroll_id = result['_scroll_id']

# 处理第一页结果
for hit in result['hits']['hits']:
    print(hit['_source'])

# 循环获取后续页结果
while len(result['hits']['hits']):
    result = es.scroll(scroll_id=scroll_id, scroll='1m')
    scroll_id = result['_scroll_id']
    for hit in result['hits']['hits']:
        print(hit['_source'])

# 清除滚动上下文
es.clear_scroll(scroll_id=scroll_id)

  • 适用场景:适用于需要一次性获取大量数据的场景,如数据导出、批量处理等。但滚动搜索不适合实时查询,因为滚动上下文是一个快照,在滚动过程中对索引的更新不会反映在结果中。
2. Search After
  • 原理search_after 参数通过记录上一页最后一条文档的排序值,在后续查询中从该排序值之后继续获取结果,避免了跳过大量记录的操作,从而提高了性能。
  • 示例(Python)

收起

python

from elasticsearch import Elasticsearch

# 连接到 Elasticsearch
es = Elasticsearch([{'host': 'localhost', 'port': 9200}])

# 定义索引名称
index_name = 'your_index'

# 每页文档数量
page_size = 10

# 第一次查询
query = {
    "query": {
        "match_all": {}
    },
    "size": page_size,
    "sort": [{"_id": "asc"}]  # 按 _id 升序排序
}
result = es.search(index=index_name, body=query)

# 处理第一页结果
for hit in result['hits']['hits']:
    print(hit['_source'])

# 获取最后一条文档的排序值
last_sort_value = result['hits']['hits'][-1]['sort']

# 循环获取后续页结果
while len(result['hits']['hits']) == page_size:
    query = {
        "query": {
            "match_all": {}
        },
        "size": page_size,
        "sort": [{"_id": "asc"}],
        "search_after": last_sort_value
    }
    result = es.search(index=index_name, body=query)
    for hit in result['hits']['hits']:
        print(hit['_source'])
    if len(result['hits']['hits']):
        last_sort_value = result['hits']['hits'][-1]['sort']

  • 适用场景:适用于实时分页查询,支持按任意字段排序。但 search_after 只能向后翻页,不支持向前翻页。
总结

在处理 ES 深度分页时,需要根据具体的业务场景选择合适的解决方案。如果需要一次性获取大量数据,可以使用滚动搜索;如果是实时分页查询,建议使用 search_after

Synchronized实现原理

一、Synchronized 的基本作用

synchronized 是 Java 中实现线程同步的关键字,用于确保多个线程对共享资源的原子性、可见性和有序性访问。它通过以下两种方式使用:

  1. 修饰代码块:锁定指定对象。
  2. 修饰方法:锁定当前实例对象(实例方法)或类对象(静态方法)。


二、底层实现机制
1. 字节码层面

代码块同步:通过 monitorenter 和 monitorexit 指令实现。

  • java复制

public void syncBlock() {
    synchronized (this) {
        // 临界区代码
    }
}

编译后的字节码:

复制

monitorenter   // 获取锁
...            // 临界区代码
monitorexit    // 释放锁

方法同步:通过方法访问标志 ACC_SYNCHRONIZED 标记。

  • java复制

public synchronized void syncMethod() {
    // 临界区代码
}

方法调用时:JVM 会隐式获取锁(无需显式字节码指令)。


三、锁的存储结构(对象头)

每个 Java 对象在内存中分为三部分:

  1. 对象头(Header)
  2. 实例数据(Instance Data)
  3. 对齐填充(Padding)

对象头结构:

  • Mark Word(64位系统占8字节):
    • 存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等)。
    • 锁状态不同时,Mark Word 的内容会动态变化。

锁状态

存储内容

无锁

哈希码、分代年龄、偏向模式标志(0)

偏向锁

线程ID、Epoch、分代年龄、偏向模式标志(1)

轻量级锁

指向栈中锁记录(Lock Record)的指针

重量级锁

指向互斥量(Mutex)的指针

GC 标记

空(用于垃圾回收)

  • Klass Pointer:指向类元数据的指针(开启压缩指针后占4字节)。


四、锁升级过程(优化机制)

Java 6 后引入锁升级机制,根据竞争情况动态调整锁状态,减少性能开销:

  1. 偏向锁(Biased Locking)
    • 适用场景:单线程访问,无实际竞争。
    • 实现原理:
      • 对象头 Mark Word 中存储线程ID。
      • 后续同一线程进入同步代码时,无需 CAS 操作,直接检查线程ID是否匹配。
    • 优势:消除无竞争时的同步开销。
    • 触发条件:默认开启(JVM 参数 -XX:+UseBiasedLocking)。
  1. 轻量级锁(Lightweight Locking)
    • 适用场景:多线程交替执行,竞争不激烈。
    • 实现原理:
      • 线程在栈帧中创建锁记录(Lock Record),存储对象 Mark Word 的拷贝。
      • 通过 CAS 操作尝试将对象头替换为指向锁记录的指针。
      • 成功:线程获得锁。
      • 失败:锁已占用,升级为重量级锁。
    • 优势:避免线程阻塞,通过自旋(CAS)减少上下文切换。
  1. 重量级锁(Heavyweight Locking)
    • 适用场景:多线程竞争激烈。
    • 实现原理:
      • 依赖操作系统提供的**互斥量(Mutex)**实现线程阻塞。
      • 未获取锁的线程进入阻塞队列,由操作系统调度唤醒。
    • 劣势:涉及用户态到内核态的切换,性能开销较大。

锁升级流程:

复制

无锁 → 偏向锁 → 轻量级锁 → 重量级锁


五、锁的优化技术
  1. 自旋锁(Spin Lock)
    • 原理:线程在竞争锁失败后,不立即阻塞,而是循环(自旋)尝试获取锁。
    • 适用场景:锁占用时间短,减少线程切换的开销。
    • JVM 参数:-XX:+UseSpinning(Java 6 后默认开启)。
  1. 适应性自旋锁(Adaptive Spinning)
    • 原理:根据历史自旋成功率动态调整自旋次数。
    • 若上次自旋成功获取锁,则允许更长的自旋时间。
  1. 锁消除(Lock Elimination)
    • 原理:JIT 编译器通过逃逸分析,移除不可能存在共享资源竞争的锁。

示例:

    • java复制

public String concat(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1).append(s2);
    return sb.toString();
}

      • StringBuffer 的 append() 方法使用 synchronized,但此处 sb 未逃逸出方法,JVM 会自动消除锁。
  1. 锁粗化(Lock Coarsening)
    • 原理:将多个连续的加锁-解锁操作合并为一个更大范围的锁。

示例:

    • java复制

for (int i = 0; i < 100; i++) {
    synchronized (this) {
        // 操作共享资源
    }
}

      • JVM 可能将锁范围扩大到整个循环,减少锁的获取/释放次数。


六、重量级锁的实现细节(Mutex)
  • 操作系统依赖:在 Linux 中,重量级锁通过 pthread_mutex_t 实现。
  • 阻塞与唤醒:
    • 未获取锁的线程进入等待队列(Contention List),由操作系统挂起。
    • 锁释放时,唤醒队列中的线程,触发上下文切换。


七、实战示例与验证
1. 查看对象头信息(JOL 工具)

使用 Java Object Layout(JOL)工具分析对象头变化:

java

复制

// 添加依赖
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.16</version>
</dependency>

// 示例代码
public class LockExample {
    public static void main(String[] args) {
        Object obj = new Object();
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        
        synchronized (obj) {
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }
    }
}

输出:

复制

无锁状态:01 00 00 00 (偏向锁标志位 0)
加锁后: 05 90 e1 1a (偏向锁标志位 1,线程ID 存储)

2. 性能对比测试

java

复制

// 测试不同锁状态下的性能差异
public class LockBenchmark {
    private static final int THREADS = 4;
    private static final int CYCLES = 1000000;

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        ExecutorService executor = Executors.newFixedThreadPool(THREADS);
        
        for (int i = 0; i < THREADS; i++) {
            executor.submit(() -> {
                for (int j = 0; j < CYCLES; j++) {
                    synchronized (LockBenchmark.class) {
                        // 模拟临界区操作
                    }
                }
            });
        }
        
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.HOURS);
        System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
    }
}

结果分析:

  • 无竞争时(单线程),偏向锁耗时最低。
  • 高并发时,重量级锁更稳定(但耗时较长)。


八、常见问题与解决方案

问题

原因

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值