You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

高负载下获取PESSIMISTIC_READ锁时触发Eclipselink空指针异常

高负载下获取PESSIMISTIC_READ锁偶发NullPointerException的排查与解决

我来帮你分析下这个高负载场景下偶发空指针异常的问题,结合你的代码和报错堆栈,咱们一步步拆解:

问题复现场景

你当前的代码逻辑是先查询账号实体,再尝试给实体加PESSIMISTIC_READ锁,代码如下:

EntityManager em = SMEntityManagerFactory.getEM();
AccountDao accountDao = new AccountDao(em);
AccountEntity entity = accountDao.findEntityByName(this.getUserName(),this.getUserDomain());
em.lock(entity, LockModeType.PESSIMISTIC_READ);

对应的报错堆栈:

java.lang.NullPointerException
at org.eclipse.persistence.internal.jpa.EntityManagerImpl.executeQuery(EntityManagerImpl.java:920)
at org.eclipse.persistence.internal.jpa.EntityManagerImpl.lock(EntityManagerImpl.java:1903)
at org.eclipse.persistence.internal.jpa.EntityManagerImpl.lock(EntityManagerImpl.java:1846)
at sun.reflect.GeneratedMethodAccessor2006.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)

核心原因分析

从堆栈和代码逻辑来看,最直接的触发点是accountDao.findEntityByName返回了null,而EclipseLink的EntityManager.lock方法在处理null实体时没有做判空校验,直接进入后续查询逻辑导致了NPE。

为什么高负载下才会偶发?大概率是并发场景导致的间隙问题:

  1. 你的线程A执行查询时,账号实体还存在;
  2. 同时线程B删除了该账号实体;
  3. 线程A拿到查询结果后(此时已经是null,或者查询结果在内存中已经失效),调用em.lock就触发了空指针。

另外还要排查下EntityManager的线程安全性:如果SMEntityManagerFactory.getEM()返回的是同一个实例被多线程共享,EclipseLink的EntityManager默认是非线程安全的,这种情况下也会出现各种不可预期的并发问题,包括NPE。

解决方案

针对这个问题,咱们可以从三个层面来解决:

1. 增加非空校验(最直接的防御手段)

在调用em.lock之前必须判空,避免传入null实体:

EntityManager em = SMEntityManagerFactory.getEM();
AccountDao accountDao = new AccountDao(em);
AccountEntity entity = accountDao.findEntityByName(this.getUserName(),this.getUserDomain());
// 新增非空校验
if (entity != null) {
    em.lock(entity, LockModeType.PESSIMISTIC_READ);
} else {
    // 根据业务场景处理,比如抛出业务异常或者返回错误提示
    throw new IllegalStateException("账号不存在,无法获取悲观锁");
}

2. 原子化查询加锁(消除并发间隙)

把查询和加锁合并成一个原子操作,避免先查询后加锁的时间窗口被其他线程修改数据。修改AccountDao的查询方法,直接在查询时指定锁模式:

// 在AccountDao中实现带悲观读锁的查询
public AccountEntity findEntityByNameWithLock(String userName, String userDomain) {
    TypedQuery<AccountEntity> query = em.createQuery(
        "SELECT a FROM AccountEntity a WHERE a.userName = :userName AND a.userDomain = :userDomain",
        AccountEntity.class
    );
    query.setParameter("userName", userName);
    query.setParameter("userDomain", userDomain);
    // 查询时直接加PESSIMISTIC_READ锁
    query.setLockMode(LockModeType.PESSIMISTIC_READ);
    try {
        return query.getSingleResult();
    } catch (NoResultException e) {
        return null;
    }
}

之后业务代码直接调用这个带锁的查询方法,既保证了查询到实体时已经持有锁,也避免了后续加锁的空指针问题。

3. 确保EntityManager的线程安全性

检查SMEntityManagerFactory.getEM()的实现:

  • 如果是Java SE环境,确保每个线程获取独立的EntityManager实例(EntityManager是轻量级对象,创建开销很低);
  • 如果是Java EE/Spring环境,使用容器管理的EntityManager(比如Spring的@PersistenceContext注入),容器会自动处理线程安全问题。

总结

这个偶发NPE的核心是并发场景下实体可能为空,加上先查询后加锁的间隙导致的。通过非空校验、原子化查询加锁、保证EntityManager线程安全这三个步骤,就能彻底解决这个问题。

内容的提问来源于stack exchange,提问作者RupakSah

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 03:54:19