多线程Java任务执行DB插入时发生死锁问题排查
问题描述
运行新实现的任务时遭遇死锁,任务处理数千条记录后停滞:主线程因RejectedExecutionHandler进入等待状态,所有工作线程均阻塞。线程转储显示工作线程在newRecordDao.insertContent(newRecord);处等待主线程释放锁,而非数据库层面死锁。
Job 核心代码
final BlockingQueue<Runnable> BLOCKING_QUEUE = new ArrayBlockingQueue<>(10); final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(10, 10, 60, TimeUnit.HOURS, BLOCKING_QUEUE, new BlockWhenQueueFull()); protected void doExecute() { records = service.getBatchRecordsToProcess(batchSize); // 主线程从DB加载500条记录 for (Record record: records) { EXECUTOR.execute(() -> { // 从线程池取线程处理记录 service.processRecord(record); }); } } } class BlockWhenQueueFull implements RejectedExecutionHandler { public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { try { Thread.sleep(200); // 若所有线程繁忙,主线程休眠200ms后重试 } catch (InterruptedException interruptedException) { LOGGER.debug(interruptedException.getMessage(), interruptedException); } executor.execute(r); } }
Service 核心代码
public List<Record> getBatchRecordsToProcess(int batchSize) { return dao.getRecordsToProcess(batchSize); } public void processRecord(Record record) { processContent(record); writeToFs(record); markAsProcessed(record); } public void processContent(Record record) { // 业务逻辑 NewRecord newRecord = new NewRecord(record); dao.insertNewRecord(newRecord); } private synchronized void writeToFs(Record record) throws IOException { // 在文件系统创建文件夹和文件并写入内容 } private void markAsProcessed(Record record) { dao.markRecordAsProcessed(record); }
DAO 核心代码
@Transactional(readOnly = true) public List<Record> getRecordsToProcess(int batchSize) { return jdbcTemplate.query(SELECT_BATCH, new Object[]{batchSize}, new RecordMapper()); } @Transactional public void insertNewRecord(NewRecord newRecord) { jdbcTemplate.update( INSERT_NEW_RECORD, newRecord.getDate(), // 参数值 ); } @Transactional public void markRecordAsProcessed(Record record) { jdbcTemplate.update(UPDATE_PROCESSED, record.getId()); }
线程转储信息
"pool-2-thread-10@8925" prio=5 tid=0x44 nid=NA waiting for monitor entry java.lang.Thread.State: BLOCKED waiting for main@1 to release lock on <0x27c9> (a java.util.concurrent.ConcurrentHashMap) ... at com.dao.NewRecordDao$$EnhancerBySpringCGLIB$$ec63bd65.insertContent(<generated>:-1) "main@1" prio=5 tid=0x1 nid=NA sleeping java.lang.Thread.State: TIMED_WAITING at java.lang.Thread.sleep(Thread.java:-1) at com.job.BlockWhenQueueFull.rejectedExecution(Job.java:130) *指向Thread.sleep(200); at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823) at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369) at com.job.BlockWhenQueueFull.rejectedExecution(Job.java:134) *指向executor.execute(r);
疑问及解答
疑问1:多线程对Oracle表并发插入(随机主键、READ COMMITTED隔离级)是否可能死锁?
从线程转储来看,当前阻塞是Java层面的锁竞争,并非Oracle数据库死锁。单纯针对随机主键的并发插入,在READ COMMITTED隔离级下,Oracle会为每个插入的行加行级锁,由于主键是随机顺序,不会出现多个线程互相等待对方锁资源的循环等待场景,理论上不会触发数据库死锁。
当前问题的根源在于:主线程触发拒绝策略后进入Thread.sleep(200),此时主线程持有了ConcurrentHashMap的锁(该锁大概率来自Spring事务管理的内部资源映射,比如事务同步管理器的线程绑定资源),而工作线程执行insertNewRecord时需要获取这个锁,导致所有工作线程阻塞;同时线程池队列已满、工作线程全忙,主线程不断重试提交任务,陷入循环sleep,最终整个任务停滞。
疑问2:写入文件系统(tmpfs/xfs)的方法是否需要加synchronized?
是否需要同步取决于写入的资源:
- 如果每个
record写入独立的文件/文件夹,不需要加synchronized——文件系统本身可以处理不同文件的并发写入,不会出现资源竞争。 - 如果多个线程写入同一个文件/共享路径(比如追加到同一个日志文件),则需要加
synchronized或使用其他线程安全的写入方式,否则会出现内容错乱、文件损坏的问题。
当前线程转储未显示文件写入相关的锁竞争,因此该方法不是本次死锁的诱因。
内容的提问来源于stack exchange,提问作者Petr H
相关产品推荐
相关产品推荐

