由于 Spring 的事务管理器是通过线程相关的 ThreadLocal 来保存数据访问基础设施,再结合 IOC 和 AOP 实现高级声明式事务的功能,所以 Spring 的事务天然地和线程有着千丝万缕的联系。
我们知道 Web 容器本身就是多线程的,Web 容器为一个 Http 请求创建一个独立的线程,所以由此请求所牵涉到的 Spring 容器中的 Bean 也是运行于多线程的环境下。在绝大多数情况下,Spring 的 Bean 都是单实例的(singleton),单实例 Bean 的最大的好处是线程无关性,不存在多线程并发访问的问题,也即是线程安全的。
一个类能够以单实例的方式运行的前提是“无状态”:即一个类不能拥有状态化的成员变量。我们知道,在传统的编程中,DAO 必须执有一个 Connection,而 Connection 即是状态化的对象。所以传统的 DAO 不能做成单实例的,每次要用时都必须 new 一个新的实例。传统的 Service 由于将有状态的 DAO 作为成员变量,所以传统的 Service 本身也是有状态的。
但是在 Spring 中,DAO 和 Service 都以单实例的方式存在。Spring 是通过 ThreadLocal 将有状态的变量(如 Connection 等)本地线程化,达到另一个层面上的“线程无关”,从而实现线程安全。Spring 不遗余力地将状态化的对象无状态化,就是要达到单实例化 Bean 的目的。
由于 Spring 已经通过 ThreadLocal 的设施将 Bean 无状态化,所以 Spring 中单实例 Bean 对线程安全问题拥有了一种天生的免疫能力。不但单实例的 Service 可以成功运行于多线程环境中,Service 本身还可以自由地启动独立线程以执行其它的 Service。下面,通过一个实例对此进行描述:
清单 13 UserService.java 在事务方法中启动独立线程运行另一个事务方法
@Service("userService")
public class UserService extends BaseService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private ScoreService scoreService;
//① 在logon方法体中启动一个独立的线程,在该独立的线程中执行ScoreService#addScore()方法
public void logon(String userName) {
System.out.println("logon method...");
updateLastLogonTime(userName);
Thread myThread = new MyThread(this.scoreService,userName,20);
myThread.start();
}
public void updateLastLogonTime(String userName) {
System.out.println("updateLastLogonTime...");
String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";
jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
}
//② 封装ScoreService#addScore()的线程
private class MyThread extends Thread{
private ScoreService scoreService;
private String userName;
private int toAdd;
private MyThread(ScoreService scoreService,String userName,int toAdd) {
this.scoreService = scoreService;
this.userName = userName;
this.toAdd = toAdd;
}
public void run() {
scoreService.addScore(userName,toAdd);
}
}
}
将日志级别设置为 DEBUG,执行 UserService#logon() 方法,观察以下输出的日志:
清单 14 执行日志
[main] (AbstractPlatformTransactionManager.java:365) - Creating new transaction with name
[user.multithread.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT ①
[main] (DataSourceTransactionManager.java:205) - Acquired Connection
[org.apache.commons.dbcp.PoolableConnection@1353249] for JDBC transaction
logon method...
updateLastLogonTime...
[main] (JdbcTemplate.java:785) - Executing prepared SQL update
[main] (JdbcTemplate.java:569) - Executing prepared SQL statement
[UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?]
[main] (JdbcTemplate.java:794) - SQL update affected 0 rows
[main] (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit
[Thread-2](AbstractPlatformTransactionManager.java:365) -
Creating new transaction with name [user.multithread.ScoreService.addScore]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT ②
[main] (DataSourceTransactionManager.java:265) - Committing JDBC transaction
on Connection [org.apache.commons.dbcp.PoolableConnection@1353249] ③
[main] (DataSourceTransactionManager.java:323) - Releasing JDBC Connection
[org.apache.commons.dbcp.PoolableConnection@1353249] after transaction
[main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource
[Thread-2] (DataSourceTransactionManager.java:205) - Acquired Connection
[org.apache.commons.dbcp.PoolableConnection@10dc656] for JDBC transaction
addScore...
[main] (JdbcTemplate.java:416) - Executing SQL statement
[DELETE FROM t_user WHERE user_name='tom']
[main] (DataSourceUtils.java:112) - Fetching JDBC Connection from DataSource
[Thread-2] (JdbcTemplate.java:785) - Executing prepared SQL update
[Thread-2] (JdbcTemplate.java:569) - Executing prepared SQL statement
[UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]
[main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource
[Thread-2] (JdbcTemplate.java:794) - SQL update affected 0 rows
[Thread-2] (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit
[Thread-2] (DataSourceTransactionManager.java:265) - Committing JDBC transaction
on Connection [org.apache.commons.dbcp.PoolableConnection@10dc656] ④
[Thread-2] (DataSourceTransactionManager.java:323) - Releasing JDBC Connection
[org.apache.commons.dbcp.PoolableConnection@10dc656] after transaction
在 ① 处,在主线程(main)执行的 UserService#logon() 方法的事务启动,在 ③ 处,其对应的事务提交,而在子线程(Thread-2)执行的 ScoreService#addScore() 方法的事务在 ② 处启动,在 ④ 处对应的事务提交。
所以,我们可以得出这样的结论:在 相同线程中进行相互嵌套调用的事务方法工作于相同的事务中。如果这些相互嵌套调用的方法工作在不同的线程中,不同线程下的事务方法工作在独立的事务中。