如何在Serializable类中正确使用lock变量同步并避免NPE
问题分析与解决方案
问题根源
1. 空指针异常(NPE)的直接原因
lock是transient修饰的变量,序列化时会被忽略,反序列化后默认值为null。虽然重写了readObject方法重新初始化lock,但反序列化过程中,readObject的执行时机晚于对象实例的创建——如果在readObject完成前,有线程调用beforeInvocation方法,就会访问到null的lock,触发NPE。
2. IDE警告的原因
synchronization on a non-final variable的警告是因为非final的锁变量可能在同步块执行过程中被替换,导致不同线程使用不同的锁对象,完全失去同步的作用,破坏线程安全。
修复方案
步骤1:确保锁对象不可变且初始化安全
将lock声明为final transient,保证锁对象一旦初始化就不会被替换,同时避免序列化:
private final transient Object lock;
然后在构造器中完成初始化:
public OpenSessionBackgroundProcess(String name, ActionInvocation invocation, int threadPriority, EntityManagerFactory entityManagerFactory) { super(name, invocation, threadPriority); this.entityManagerFactory = entityManagerFactory; this.lock = new Object(); // 初始化final锁 initializationComplete = true; synchronized (lock) { lock.notify(); } }
步骤2:修复反序列化时final transient变量的初始化
因为final transient变量不会被自动反序列化,需要通过反射在readObject中初始化:
private void readObject(ObjectInputStream serialized) throws IOException, ClassNotFoundException { serialized.defaultReadObject(); // 通过反射设置final的lock变量 try { Field lockField = OpenSessionBackgroundProcess.class.getDeclaredField("lock"); lockField.setAccessible(true); lockField.set(this, new Object()); } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException("Failed to initialize lock during deserialization", e); } }
步骤3:保证共享变量的可见性
initializationComplete是多线程共享的变量,添加volatile修饰,确保线程间的可见性,避免出现线程一直等待的情况:
protected volatile boolean initializationComplete;
完整修复后的代码
import com.opensymphony.xwork2.ActionInvocation; import org.apache.struts2.interceptor.BackgroundProcess; import org.springframework.orm.jpa.EntityManagerFactoryUtils; import org.springframework.orm.jpa.EntityManagerHolder; import org.springframework.transaction.support.TransactionSynchronizationManager; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.lang.reflect.Field; public class OpenSessionBackgroundProcess extends BackgroundProcess implements Serializable { private static final long serialVersionUID = 3884464561311686443L; private final transient EntityManagerFactory entityManagerFactory; // 添加volatile保证多线程可见性 protected volatile boolean initializationComplete; // 声明为final transient,确保锁对象不可变 private final transient Object lock; public OpenSessionBackgroundProcess(String name, ActionInvocation invocation, int threadPriority, EntityManagerFactory entityManagerFactory) { super(name, invocation, threadPriority); this.entityManagerFactory = entityManagerFactory; this.lock = new Object(); initializationComplete = true; synchronized (lock) { lock.notify(); } } protected void beforeInvocation() throws Exception { while (!initializationComplete) { try { synchronized (lock) { lock.wait(100); } } catch (InterruptedException e) { // 忽略中断,继续等待 } } EntityManager em = entityManagerFactory.createEntityManager(); TransactionSynchronizationManager.bindResource(entityManagerFactory, new EntityManagerHolder(em)); super.beforeInvocation(); } protected void afterInvocation() throws Exception { super.afterInvocation(); EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager.unbindResource(entityManagerFactory); EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager()); } private void readObject(ObjectInputStream serialized) throws IOException, ClassNotFoundException { serialized.defaultReadObject(); // 通过反射初始化final的lock变量 try { Field lockField = OpenSessionBackgroundProcess.class.getDeclaredField("lock"); lockField.setAccessible(true); lockField.set(this, new Object()); } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException("Failed to initialize lock during deserialization", e); } } }
额外说明
final锁变量确保了同步块始终使用同一个锁对象,避免了锁对象被替换的风险,解决了IDE的警告。volatile修饰initializationComplete,保证了线程间的可见性,避免线程因看不到最新值而无限等待。- 反射初始化final变量是Java中处理final transient变量反序列化的常用手段,确保反序列化后的对象拥有有效的锁对象。
内容的提问来源于stack exchange,提问作者Burton
相关产品推荐
相关产品推荐

