首页 > 数据库 >java.sql.Connection 线程安全吗?

java.sql.Connection 线程安全吗?

时间:2022-12-20 14:59:10浏览次数:59  
标签:事务 java Connection 线程 SQL 执行 连接

首先看一段代码:

try (Connection connection = DriverManager.getConnection("jdbc:mysql://remote01:3306/test");) {
	ExecutorService executor = ThreadUtil.newExecutor();
	for (int i = 0; i < 1000; i++) {
		int finalI = i;
		// 使用线程池,并发执行下面语句
		executor.submit(() -> {
			try {
			  // 通过连接创建 Statement
				PreparedStatement statement = connection.prepareStatement("select * from user limit ?,100");
				statement.setInt(1, finalI * 100);
				// 执行查询操作
				statement.execute();
				// 获取返回结果
				ResultSet resultSet = statement.getResultSet();
				while (resultSet.next()) {
					System.out.println(resultSet.getString("name"));
				}
			} catch (SQLException e) {
				e.printStackTrace();
			}
		});
	}
} catch (SQLException e) {
	e.printStackTrace();
}

上面这段代码中,出现了线程池中多个线程共用一个 SQL 连接 的现象。一般我们会使用一些连接池来管理 SQL 连接,而这些连接池通常会为每个线程单独分配一个连接,来确保线程安全的问题。也就是说,如果出现了多个线程共用一个 Sql 连接,正常理解来说会出现线程安全问题,那么真的是这样的吗?

ExecutorService executor = ThreadUtil.newExecutor();
for (int i = 0; i < 1000; i++) {
    // 使用线程池,并发执行下面语句
    final int finalI = i;
    executor.submit(() -> {
        List<Map<String, Object>> resultSet = jdbcTemplate.queryForList("select * from user limit ?,100", finalI * 100);
        // 获取返回结果
        resultSet.forEach(res -> System.out.println(res.get("name")));
    });
}

上面这段代码使用了 jdbcTemplate 来执行 SQL 语句,这种情况下SQL 的连接管理就统一交由连接池去做了,jdbcTempalte 会在执行SQL前从连接池中拿到一个SQL 连接去执行SQL,连接池会根据当前线程名称为我们分配一个固定的SQL连接,确保一个线程对应一个SQL连接,而 jdbcTemplate 会在SQL执行完成后会将SQL连接放回至连接池中以备稍后使用,而不是直接关掉。从而提高SQL连接的利用率。

结论

非事务场景

Connection 没有开启事务的情况下,如单纯的 Select 查询语句,或开启了 AUTO_COMMIT,是线程安全的。但是由于内部使用了 (synchronized),同步锁,在多线程访问一个 Connection 时,会导致执行请求的串行化。

事务场景

如果在开启事务的情况下,会导致多个线程共用一个事务,也就是说如果其中一个线程对事务进行了提交或者回滚操作,会同时影响其他线程执行的SQL。

上代码

从结论可以得到,SQL连接虽然总体上非线程安全,但是在非事务场景中还是保证了线程安全的,那么 Connection 是怎么保证这一点的呢?

通常我们使用 Connection 来执行查询,通过Connection获取一个 Statement 对象 ,然后调用Statement中的 execute 方法来执行真正的 sql。我们可以通过解析 Statement实现中的 execute 方法来得到 SQL 执行的具体逻辑。

// com.mysql.jdbc.StatementImpl#execute(java.lang.String, boolean)
private boolean execute(String sql, boolean returnGeneratedKeys) throws SQLException {
	MySQLConnection locallyScopedConn = checkClosed();
	// 使用同步代码块对单次查询加锁
	synchronized (locallyScopedConn.getConnectionMutex()) {
		// 重置并且检查当前连接的状态
		this.retrieveGeneratedKeys = returnGeneratedKeys;
		lastQueryIsOnDupKeyUpdate = false;
		if (returnGeneratedKeys)
			lastQueryIsOnDupKeyUpdate = containsOnDuplicateKeyInString(sql);
		resetCancelledState();
		checkNullOrEmptyQuery(sql);
		checkClosed();
		char firstNonWsChar = StringUtils.firstAlphaCharUc(sql, findStartOfStatement(sql));
		boolean isSelect = true;
		if (firstNonWsChar != 'S') {
			// 检查状态
		}
		boolean doStreaming = createStreamingResultSet();
		try {
			// 执行查询,并且处理返回结果
			...
		} finally {
			this.statementExecuting.set(false);
		}
	}
}

上面是 mysql 驱动中 Statement 执行具体 SQL 的逻辑实现,可以看到在执行开始,就通过当前连接设置了一个同步代码块。这样的话,当前 Connection 所创建的所有 Statement 同时执行 execute 方法时,实际是串行执行的。

// 提交多线程并发执行任务
executor.submit(() -> {
	try {
	  // 每个线程执行时会单独创建一个 Statement
		PreparedStatement statement = connection.prepareStatement("select * from user limit ?,100");
		statement.setInt(1, finalI * 100);
		// 执行该方法时,由于 execute 方法中的同步代码块
		// 实际查询在并发场景中是串行执行的
		statement.execute();
		ResultSet resultSet = statement.getResultSet();
		while (resultSet.next()) {
			System.out.println(resultSet.getString("name"));
		}
	} catch (SQLException e) {
		e.printStackTrace();
	}
});
}

现在我们了解了,单个连接创建的多个SQL查询,实际是通过代码块的方式实现了线程安全,也就是在单个连接在多线程并发执行查询的场景下,不同的查询之间是互不干扰的。但由于同步代码块的存在,多个并发查询实际是串行执行的,所以使用单个连接并发查询会导致最终查询的效果还是串行的,并没有因为使用并发而提高效率。

事务场景

当然上面的分析是基于非事务场景下,查询之间是没有联系的。或者我们开启了自动提交(AUTO_COMMIT),这样每个SQL之间的操作也是互不关联的。但是在事务场景中,如果多个线程共用一个 SQL 连接去执行 SQL 的话,会导致多个线程共用同一个事务,如果某个线程回滚了一个事务,则会导致会将其他线程在事务期间执行的SQL一起给回滚了,而其他线程却对此一无所知,最终导致数据不一致。

image

如上图,线程A 开启了一个事务并执行相应的SQL操作。此时线程B共用同一个连接执行SQL的话,线程B是不知道它此时正处于线程A的事务当中,这样当线程A在请求回滚事务时,MySQL 会将线程B 在事务过程中提交的SQL一起给回滚掉。但所有的操作线程B 是无感知的。

参考资料

JDBC中Connection解惑

Connection实例是线程安全的吗

JDBC中的connection详解

标签:事务,java,Connection,线程,SQL,执行,连接
From: https://www.cnblogs.com/ghimi/p/16994159.html

相关文章

  • Java IO
    JavaIO原理:作者:哟哟的小豆豆链接:https://juejin.cn/post/6844903839439519758来源:稀土掘金类结构及继承关系图常见面试题什么是流?字符串分解---------------......
  • java中的抽象类
    本文主要讲述java中的抽象类,以及具体的细节老韩的抽象类介绍 ......
  • open for java 创建圆弧
    //徐华进2022/12/20///*==================================================================================================Copyright(c)2004UGS......
  • 线程状态演示
    线程状态演示操作系统层面有5中状态Java中有6种状态NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATEDThread源码中就有显示,打印出来演示@Slf4jp......
  • Java序列化与反序列化三连问:是什么?为什么要?如何做?
    Java序列化与反序列化是什么?Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程:序列化:对象序列化的最主要的用处就是在......
  • 为什么Java线程没有Running状态?
    Java虚拟机层面所暴露给我们的状态,与操作系统底层的线程状态是两个不同层面的事。具体而言,这里说的Java线程状态均来自于Thread类下的State这一内部枚举类中所定义的......
  • 再谈Python多线程--避免GIL对性能的影响
    在Python中使用多线程,如果你对GIL本身没有一定的了解;那么很有可能你只是写出了正确的多线程代码,而并没有达到多线程的目的,甚至截然相反的效果。下面介绍了Python中GIL的作用......
  • 解析Java框架中entity层,mapper层,service层,controller各层作用
    转载自:https://blog.csdn.net/u011095110/article/details/86088976一、entity层别名:model层,domain层用途:实体层,用于存放我们的实体类,与数据库中的属性值基本......
  • 4.Java程序初识
    4.Java程序初识4.1HelloWorld程序以Hello.java为例,java应用程序的执行入口是main()方法,其有固定格式:/***类文件,一个源文件最多只能有一个public类,且与文件名称一......
  • Xml转Java实体类对象 xml转Javabena 对象 且多级嵌套 复杂嵌套
    最近在做企微开发,遇到了一个比较复杂的xml然后要去我将xml转成实体类xml如下<xml> <ToUserName><![CDATA[toUser]]></ToUserName> <FromUserName><![CDATA[sys]]></Fro......