高负载下获取PESSIMISTIC_READ锁时触发Eclipselink空指针异常
我来帮你分析下这个高负载场景下偶发空指针异常的问题,结合你的代码和报错堆栈,咱们一步步拆解:
问题复现场景
你当前的代码逻辑是先查询账号实体,再尝试给实体加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。
为什么高负载下才会偶发?大概率是并发场景导致的间隙问题:
- 你的线程A执行查询时,账号实体还存在;
- 同时线程B删除了该账号实体;
- 线程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

