Why MyBatis SqlSession Crashes with Multiple Threads and How Spring Fixes It
This article examines the ClassCastException that occurs when 100 threads concurrently query MyBatis using a shared SqlSession, analyzes the root cause in DefaultSqlSession and BaseExecutor, and explains how Spring Boot’s MyBatis integration employs SqlSessionTemplate, ThreadLocal binding, and transaction management to guarantee thread‑safe SqlSession usage.
1. Environment Preparation
Configuration files and Java classes required for the demo.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/testjpa?serverTimezone=GMT%2B8"/>
<property name="username" value="root"/>
<property name="password" value="123123"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mappers/UsersMapper.xml"/>
</mappers>
</configuration> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pack.mapper.UsersMapper">
<select id="selectList" resultType="com.pack.domain.Users">
select * from t_users
</select>
</mapper> package com.pack.mapper;
import java.util.List;
import com.pack.domain.Users;
public interface UsersMapper {
List<Users> selectList();
} public class Users {
private String id;
private String username;
private String password;
} public class UsersMapperTest {
private static final int MAX = 100;
private SqlSessionFactory sqlSessionFactory;
private Thread[] threads = new Thread[MAX];
private CountDownLatch cdl = new CountDownLatch(MAX);
@Before
public void init() throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
@Test
public void testSelectList() throws Exception {
SqlSession session = sqlSessionFactory.openSession();
UsersMapper mapper = session.getMapper(UsersMapper.class);
for (int i = 0; i < MAX; i++) {
threads[i] = new Thread(() -> {
try {
cdl.await();
System.out.println(mapper.selectList());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
for (int i = 0; i < MAX; i++) {
threads[i].start();
cdl.countDown();
}
System.in.read();
}
}2. Error Analysis
Running the test with 100 concurrent threads throws a ClassCastException because the shared SqlSession returns an ExecutionPlaceholder object from the local cache, which cannot be cast to List. The exception originates from DefaultSqlSession.selectList (line 153) and propagates through BaseExecutor.query.
### Cause: java.lang.ClassCastException: org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.List
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:153)
...
at com.sun.proxy.$Proxy8.selectList(Unknown Source)
at test.UsersMapperTest.lambda$0(UsersMapperTest.java:39)
at java.lang.Thread.run(Thread.java:745)The problem occurs when thread 1 stores ExecutionPlaceholder in the cache (position 2) and thread 2 reads the same cache entry (position 1) and attempts to cast it to List. The fix is to give each thread its own SqlSession instance.
3. Default SqlSession Implementation
public class DefaultSqlSessionFactory implements SqlSessionFactory {
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
Environment environment = configuration.getEnvironment();
TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
}
/**
* The default implementation for SqlSession.
* Note that this class is not Thread‑Safe.
*/
public class DefaultSqlSession implements SqlSession {
// ... not thread‑safe implementation
}4. How Spring Handles SqlSession
4.1 Dependency
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency> @SpringBootApplication
@MapperScan({"com.pack.mapper"})
public class SpringBootTransactionalApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootTransactionalApplication.class, args);
}
}
@Import(MapperScannerRegistrar.class)
public @interface MapperScan { }4.2 Auto‑Configuration
public class MybatisAutoConfiguration implements InitializingBean {
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
// ... set data source, etc.
return factory.getObject();
}
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
if (executorType != null) {
return new SqlSessionTemplate(sqlSessionFactory, executorType);
} else {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
}4.3 Mapper Scanning
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes mapperScanAttrs = AnnotationAttributes.fromMap(
importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
if (mapperScanAttrs != null) {
registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
generateBaseBeanName(importingClassMetadata, 0));
}
}
// ... builds MapperScannerConfigurer with base packages
}
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
private Class<? extends MapperFactoryBean> mapperFactoryBeanClass = MapperFactoryBean.class;
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// configure scanner (addToConfig, annotationClass, markerInterface, etc.)
scanner.registerFilters();
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
// ... processes found mapper interfaces and registers them as MapperFactoryBean definitions
}4.4 Mapper Instantiation
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
}
public abstract class SqlSessionDaoSupport extends DaoSupport {
private SqlSessionTemplate sqlSessionTemplate;
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
}
}
public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
this.sqlSessionTemplate = sqlSessionTemplate;
}
public SqlSession getSqlSession() {
return this.sqlSessionTemplate;
}
}4.5 Thread‑Safe SqlSession (SqlSessionTemplate)
public class SqlSessionTemplate implements SqlSession, DisposableBean {
private final SqlSession sqlSessionProxy;
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
SqlSessionFactory.class.getClassLoader(), new Class[]{SqlSession.class}, new SqlSessionInterceptor());
}
public <T> T selectOne(String statement) {
return this.sqlSessionProxy.selectOne(statement);
}
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
return result;
} finally {
// transaction handling is delegated to Spring
}
}
}
} public final class SqlSessionUtils {
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
Environment environment = sessionFactory.getConfiguration().getEnvironment();
if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
SqlSessionHolder holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
TransactionSynchronizationManager.bindResource(sessionFactory, holder);
TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
holder.setSynchronizedWithTransaction(true);
holder.requested();
}
}
}
}4.6 Transaction Management
public class SpringManagedTransactionFactory implements TransactionFactory {
@Override
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
return new SpringManagedTransaction(dataSource);
}
@Override
public Transaction newTransaction(Connection conn) {
throw new UnsupportedOperationException("New Spring transactions require a DataSource");
}
@Override
public void setProperties(Properties props) { }
}
public class SpringManagedTransaction implements Transaction {
private final DataSource dataSource;
private Connection connection;
private boolean isConnectionTransactional;
private boolean autoCommit;
public SpringManagedTransaction(DataSource dataSource) {
notNull(dataSource, "No DataSource specified");
this.dataSource = dataSource;
}
@Override
public Connection getConnection() throws SQLException {
if (this.connection == null) {
openConnection();
}
return this.connection;
}
private void openConnection() throws SQLException {
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
}
@Override
public void commit() throws SQLException {
if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
this.connection.commit();
}
}
@Override
public void rollback() throws SQLException {
if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
this.connection.rollback();
}
}
@Override
public void close() throws SQLException {
DataSourceUtils.releaseConnection(this.connection, this.dataSource);
}
@Override
public Integer getTimeout() throws SQLException {
ConnectionHolder holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (holder != null && holder.hasTimeout()) {
return holder.getTimeToLiveInSeconds();
}
return null;
}
}
public abstract class DataSourceUtils {
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
try {
return doGetConnection(dataSource);
} catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex);
}
}
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
conHolder.setConnection(fetchConnection(dataSource));
}
return conHolder.getConnection();
}
Connection con = fetchConnection(dataSource);
if (TransactionSynchronizationManager.isSynchronizationActive()) {
ConnectionHolder holderToUse = conHolder != null ? conHolder : new ConnectionHolder(con);
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(new ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
return con;
}
// fetchConnection and other helper methods omitted for brevity
}In summary, Spring guarantees thread‑safety of MyBatis SqlSession by wrapping it in SqlSessionTemplate, which obtains a session from SqlSessionUtils. The utility stores the session in a ThreadLocal via TransactionSynchronizationManager, ensuring each thread works with its own session while participating in Spring‑managed transactions.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
