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

多线程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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 09:40:32