第一、ThreadLocal介绍
1.线程并发: 在多线程并发的场景下
2.传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
3.线程隔离: 每个线程的变量都是独立的,不会相互影响
第二、常用方法
第三、事务案例
1、场景构建
这里我们先构建一个简单的转账场景: 有一个数据表account,里面有两个用户Jack和Rose,用户Jack 给用户Rose 转账。 案例的实现就简单的用mysql数据库,JDBC 和 C3P0 框架实现。
2、数据准备,新建表t_account,初始化数据如下
3、常规解决方案
(1)为了保证所有的操作在一个事务中,案例中使用的连接必须是同一个:service层开启事务的connection需要跟dao层访问数据库的connection保持一致。
(2)线程并发情况下, 每个线程只能操作各自的connection。
a、新建maven工程,pom.xml代码如下
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.12</version>
</dependency>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
</dependencies>
b、src目录添加配置文件c3p0-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
<!-- 使用默认的配置读取连接池对象 -->
<default-config>
<!-- 连接参数 -->
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/test</property>
<property name="user">root</property>
<property name="password">1234</property>
<!-- 连接池参数 -->
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">10</property>
<property name="checkoutTimeout">3000</property>
</default-config>
</c3p0-config>
c、新建工具类com.util.JdbcUtils.java
package com.util;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* 工具类
* @author shixiangcheng
* 2020-07-18
*/
public class JdbcUtils {
// c3p0 数据库连接池对象属性
private static final ComboPooledDataSource ds = new ComboPooledDataSource();
// 获取连接
public static Connection getConnection() throws SQLException {
return ds.getConnection();
}
//释放资源
public static void release(AutoCloseable... ios){
for (AutoCloseable io : ios) {
if(io != null){
try {
io.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static void commitAndClose(Connection conn) {
try {
if(conn != null){
conn.commit();//提交事务
conn.close();//释放连接
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void rollbackAndClose(Connection conn) {
try {
if(conn != null){
conn.rollback();//回滚事务
conn.close();//释放连接
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
d、新建com.dao.AccountDao.java。
package com.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import com.util.JdbcUtils;
/**
* Dao层
* @author shixiangcheng
* 2020-07-18
*/
public class AccountDao {
public void out(Connection conn, String outUser, int money) throws SQLException{
String sql = "update t_account set money = money - ? where name = ?";
//注释从连接池获取连接的代码,使用从service中传递过来的connection
// Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,outUser);
pstm.executeUpdate();
//连接不能在这里释放,service层中还需要使用
// JdbcUtils.release(pstm,conn);
JdbcUtils.release(pstm);
}
public void in(Connection conn, String inUser, int money) throws SQLException {
String sql = "update t_account set money = money + ? where name = ?";
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,inUser);
pstm.executeUpdate();
JdbcUtils.release(pstm);
}
}
e、新建com.service.AccountService.java。 一个转出,一个转入。这些操作是需要具备原子性的,不可分割。不然就有可能出现数据修改异常情况。
package com.service;
import java.sql.Connection;
import com.dao.AccountDao;
import com.util.JdbcUtils;
/**
* Service层
* @author shixiangcheng
* 2020-07-18
*/
public class AccountService {
public boolean transfer(String outUser, String inUser, int money) {
AccountDao ad = new AccountDao();
//线程并发情况下,为了保证每个线程使用各自的connection,故加锁
synchronized (AccountService.class) {
Connection conn = null;
try {
conn = JdbcUtils.getConnection();
//开启事务
conn.setAutoCommit(false);
// 转出
ad.out(conn, outUser, money);
// 模拟转账过程中的异常
int i = 1/0;
// 转入
ad.in(conn, inUser, money);
//事务提交
JdbcUtils.commitAndClose(conn);
} catch (Exception e) {
e.printStackTrace();
//事务回滚
JdbcUtils.rollbackAndClose(conn);
return false;
}
return true;
}
}
}
f、新建com.web.AccountWeb.java
package com.web;
import com.service.AccountService;
/**
* web层
* @author shixiangcheng
* 2020-07-18
*/
public class AccountWeb {
public static void main(String[] args) {
// 模拟数据 : Jack 给 Rose 转账 100
String outUser = "Jack";
String inUser = "Rose";
int money = 100;
AccountService as = new AccountService();
boolean result = as.transfer(outUser, inUser, money);
if (result == false) {
System.out.println("转账失败!");
} else {
System.out.println("转账成功!");
}
}
}
g、运行web类
七月 18, 2020 6:01:46 下午 com.mchange.v2.log.MLog
信息: MLog clients using java 1.4+ standard logging.
七月 18, 2020 6:01:47 下午 com.mchange.v2.c3p0.C3P0Registry
信息: Initializing c3p0-0.9.5.2 [built 08-December-2015 22:06:04 -0800; debug? true; trace: 10]
七月 18, 2020 6:01:47 下午 com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource
信息: Initializing c3p0 pool... com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 3, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 3000, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, contextClassLoaderSource -> caller, dataSourceName -> 1hge15yabr0mkob168zbu4|1424e82, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.jdbc.Driver, extensions -> {}, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, forceSynchronousCheckins -> false, forceUseNamedDriverClass -> false, identityToken -> 1hge15yabr0mkob168zbu4|1424e82, idleConnectionTestPeriod -> 0, initialPoolSize -> 5, jdbcUrl -> jdbc:mysql://localhost:3306/test, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 0, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 10, maxStatements -> 0, maxStatementsPerConnection -> 0, minPoolSize -> 3, numHelperThreads -> 3, preferredTestQuery -> null, privilegeSpawnedThreads -> false, properties -> {user=******, password=******}, propertyCycle -> 0, statementCacheNumDeferredCloseThreads -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, userOverrides -> {}, usesTraditionalReflectiveProxies -> false ]
java.lang.ArithmeticException: / by zero
at com.service.AccountService.transfer(AccountService.java:23)
at com.web.AccountWeb.main(AccountWeb.java:15)
转账失败!
h、观察数据
4、常规解决方案的弊端
a.直接从service层传递connection到dao层, 造成代码耦合度提高
b.加锁会造成线程失去并发性,程序性能降低
5、ThreadLocal解决方案
ThreadLocal方案的实现像这种需要在项目中进行数据传递和线程隔离的场景,我们不妨用ThreadLocal来解决。
a、com.util.JdbcUtils.java修改如下
package com.util;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* 工具类
* @author shixiangcheng
* 2020-07-18
*/
public class JdbcUtils {
//ThreadLocal对象 : 将connection绑定在当前线程中
private static final ThreadLocal<Connection> tl = new ThreadLocal();
// c3p0 数据库连接池对象属性
private static final ComboPooledDataSource ds = new ComboPooledDataSource();
// 获取连接
public static Connection getConnection() throws SQLException {
Connection conn = tl.get();取出当前线程绑定的connection对象
if (conn == null) {//如果没有,则从连接池中取出
conn = ds.getConnection();
tl.set(conn);//再将connection对象绑定到当前线程中
}
return conn;
}
//释放资源
public static void release(AutoCloseable... ios) {
for (AutoCloseable io : ios) {
if (io != null) {
try {
io.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static void commitAndClose() {
try {
Connection conn = getConnection();
conn.commit();//提交事务
tl.remove();//解除绑定
conn.close();//释放连接
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void rollbackAndClose() {
try {
Connection conn = getConnection();
conn.rollback();//回滚事务
tl.remove();//解除绑定
conn.close();//释放连接
} catch (SQLException e) {
e.printStackTrace();
}
}
}
b、com.dao.AccountDao.java修改如下
package com.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import com.util.JdbcUtils;
/**
* Dao层
* @author shixiangcheng
* 2020-07-18
*/
public class AccountDao {
public void out(String outUser, int money) throws SQLException {
String sql = "update t_account set money = money - ? where name = ?";
Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,outUser);
pstm.executeUpdate();
JdbcUtils.release(pstm);
}
public void in(String inUser, int money) throws SQLException {
String sql = "update t_account set money = money + ? where name = ?";
Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,inUser);
pstm.executeUpdate();
JdbcUtils.release(pstm);
}
}
c、com.service.AccountService.java修改如下
package com.service;
import java.sql.Connection;
import com.dao.AccountDao;
import com.util.JdbcUtils;
/**
* Service层
* @author shixiangcheng
* 2020-07-18
*/
public class AccountService {
public boolean transfer(String outUser, String inUser, int money) {
AccountDao ad = new AccountDao();
try {
Connection conn = JdbcUtils.getConnection();
conn.setAutoCommit(false);//开启事务
ad.out(outUser, money);// 转出 : 这里不需要传参了 !
int i = 1 / 0;// 模拟转账过程中的异常
ad.in(inUser, money);// 转入
JdbcUtils.commitAndClose();//事务提交
} catch (Exception e) {
e.printStackTrace();
JdbcUtils.rollbackAndClose();//事务回滚
return false;
}
return true;
}
}
d、运行web类,效果和常规方案一致。
6、ThreadLocal方案的好处
a、传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合问题。
b、线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。
欢迎大家积极留言交流学习心得,点赞的人最美丽,谢谢