原文:http://blog.csdn.net/c289054531/article/details/9196053
引言:
在使用Spring時,很多人可能對Spring中為什么DAO和Service對象采用單實例方式很迷惑,這些讀者是這么認為的:
DAO對象必須包含一個數據庫的連接Connection,而這個Connection不是線程安全的,所以每個DAO都要包含一個不同的Connection對象實例,這樣一來DAO對象就不能是單實例的了。
上述觀點對了一半。對的是“每個DAO都要包含一個不同的Connection對象實例”這句話,錯的是“DAO對象就不能是單實例”。
其實Spring在實現Service和DAO對象時,使用了ThreadLocal這個類,這個是一切的核心! 如果你不知道什么事ThreadLocal,請看
《
深入研究java.lang.ThreadLocal類》:。請放心,這個類很簡單的。
1。每個線程中都有一個自己的ThreadLocalMap類對象,可以將線程自己的對象保持到其中,各管各的,線程可以正確的訪問到自己的對象。
2。將一個共用的ThreadLocal靜態實例作為key,將不同對象的引用保存到不同線程的ThreadLocalMap中,然后在線程執行的各處通過這個靜態ThreadLocal實例的get()方法取得自己線程保存的那個對象,避免了將這個對象作為參數傳遞的麻煩。
2。將一個共用的ThreadLocal靜態實例作為key,將不同對象的引用保存到不同線程的ThreadLocalMap中,然后在線程執行的各處通過這個靜態ThreadLocal實例的get()方法取得自己線程保存的那個對象,避免了將這個對象作為參數傳遞的麻煩。
要弄明白這一切,又得明白事務管理在Spring中是怎么工作的,所以本文就對Spring中多線程、事務的問題進行解析。
Spring使用ThreadLocal解決線程安全問題:
Spring中DAO和Service都是以單實例的bean形式存在,Spring通過ThreadLocal類將有狀態的變量(例如數據庫連接Connection)本地線程化,從而做到多線程狀況下的安全。在一次請求響應的處理線程中, 該線程貫通展示、服務、數據持久化三層,通過ThreadLocal使得所有關聯的對象引用到的都是同一個變量。
參考下面代碼,這個是《Spring3.x企業應用開發實戰中的例子》,本文后面也會多次用到該書中例子(有修改)。
- <span style="font-family:SimSun;font-size:14px;">public class SqlConnection {
- //①使用ThreadLocal保存Connection變量
- privatestatic ThreadLocal <Connection>connThreadLocal = newThreadLocal<Connection>();
- publicstatic Connection getConnection() {
- // ②如果connThreadLocal沒有本線程對應的Connection創建一個新的Connection,
- // 並將其保存到線程本地變量中。
- if (connThreadLocal.get() == null) {
- Connection conn = getConnection();
- connThreadLocal.set(conn);
- return conn;
- } else {
- return connThreadLocal.get();
- // ③直接返回線程本地變量
- }
- }
- public voidaddTopic() {
- // ④從ThreadLocal中獲取線程對應的Connection
- try {
- Statement stat = getConnection().createStatement();
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- }</span>
這個是例子展示了不同線程使用TopicDao時如何使得每個線程都獲得不同的Connection實例副本,同時保持TopicDao本身是單實例。
事務管理器:
事務管理器用於管理各個事務方法,它產生一個事務管理上下文。下文以SpringJDBC的事務管理器DataSourceTransactionManager類為例子。
我們知道數據庫連接Connection在不同線程中是不能共享的,事務管理器為不同的事務線程利用ThreadLocal類提供獨立的Connection副本。事實上,它將Service和Dao中所有線程不安全的變量都提取出來單獨放在一個地方,並用ThreadLocal替換。而多線程可以共享的部分則以單實例方式存在。
事務傳播行為:
當我們調用Service的某個事務方法時,如果該方法內部又調用其它Service的事務方法,則會出現事務的嵌套。Spring定義了一套事務傳播行為,請參考。這里我們假定都用的REQUIRED這個類型:如果當前沒有事務,就新建一個事務,如果已經存在一個事務,則加入到的當前事務。參考下面例子(代碼不完整):
- <span style="font-family:SimSun;font-size:14px;">@Service( "userService")
- public class UserService extends BaseService {
- @Autowired
- private JdbcTemplate jdbcTemplate;
- @Autowired
- private ScoreService scoreService;
- public void logon(String userName) {
- updateLastLogonTime(userName);
- scoreService.addScore(userName, 20);
- }
- public void updateLastLogonTime(String userName) {
- String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";
- jdbcTemplate.update(sql, System. currentTimeMillis(), userName);
- }
- public static void main(String[] args) {
- ApplicationContext ctx = new ClassPathXmlApplicationContext("com/baobaotao/nestcall/applicatonContext.xml" );
- UserService service = (UserService) ctx.getBean("userService" );
- service.logon( "tom");
- }
- }
- @Service( "scoreUserService" )
- public class ScoreService extends BaseService{
- @Autowired
- private JdbcTemplate jdbcTemplate;
- public void addScore(String userName, int toAdd) {
- String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";
- jdbcTemplate.update(sql, toAdd, userName);
- }
- }</span>
同時,在配置文件中指定UserService、ScoreService中的所有方法都開啟事務。
上述例子中UserService.logon()執行開始時Spring創建一個新事務,UserService.updateLastLogonTime()和ScoreService.addScore()會加入這個事務中,好像所有的代碼都“直接合並”了!
多線程中事務傳播的困惑:
還是上面那個例子,加入現在我在UserService.logon()方法中手動新開一個線程,然后在新開的線程中執行ScoreService.add()方法,此時事務傳播行為會怎么樣?飛線程安全的變量,比如Connection會怎樣?改動之后的UserService 代碼大體是:
- <span style="font-family:SimSun;font-size:14px;">@Service( "userService")
- public class UserService extends BaseService {
- @Autowired
- private JdbcTemplate jdbcTemplate;
- @Autowired
- private ScoreService scoreService;
- public void logon(String userName) {
- updateLastLogonTime(userName);
- Thread myThread = new MyThread(this.scoreService , userName, 20);//使用一個新線程運行
- myThread .start();
- }
- public void updateLastLogonTime(String userName) {
- String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";
- jdbcTemplate.update(sql, System. currentTimeMillis(), userName);
- }
- 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);
- }
- }
- public static void main(String[] args) {
- ApplicationContext ctx = new ClassPathXmlApplicationContext("com/baobaotao/multithread/applicatonContext.xml" );
- UserService service = (UserService) ctx.getBean("userService" );
- service.logon( "tom");
- }
- }</span>
這個例子中,MyThread會新開一個事務,於是UserService.logon()和UserService.updateLastLogonTime()會在一個事務中,而ScoreService.addScore()在另一個事務中,需要注意的是這兩個事務都被事務管理器放在事務上下文中。
結論是:在事務屬性為REQUIRED時,在相同線程中進行相互嵌套調用的事務方法工作於相同的事務中。如果互相嵌套調用的事務方法工作在不同線程中,則不同線程下的事務方法工作在獨立的事務中。
底層數據庫連接Connection訪問問題
程序只要使用SpringDAO模板,例如JdbcTemplate進行數據訪問,一定沒有數據庫連接泄露問題!如果程序中顯式的獲取了數據連接Connection,則需要手工關閉它,否則就會泄露!
當Spring事務方法運行時,事務會放在事務上下文中,這個事務上下文在本事務執行線程中對同一個數據源綁定了唯一一個數據連接,所有被該事務的上下文傳播的放發都共享這個數據連接。這一切都在Spring控制下,不會產生泄露。Spring提供了數據資源獲取工具類DataSourceUtils來獲取這個數據連接.
- <span style="font-family:SimSun;font-size:14px;">@Service( "jdbcUserService" )
- public class JdbcUserService {
- @Autowired
- private JdbcTemplate jdbcTemplate;
- @Transactional
- public void logon(String userName) {
- try {
- Connection conn = jdbcTemplate.getDataSource().getConnection();
- String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";
- jdbcTemplate.update(sql, System. currentTimeMillis(), userName);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- public static void asynchrLogon(JdbcUserService userService, String userName) {
- UserServiceRunner runner = new UserServiceRunner(userService, userName);
- runner.start();
- }
- public static void reportConn(BasicDataSource basicDataSource) {
- System. out.println( "連接數[active:idle]-[" +
- basicDataSource.getNumActive()+":" +basicDataSource.getNumIdle()+ "]");
- }
- private static class UserServiceRunner extends Thread {
- private JdbcUserService userService;
- private String userName;
- public UserServiceRunner(JdbcUserService userService, String userName) {
- this. userService = userService;
- this. userName = userName;
- }
- public void run() {
- userService.logon( userName);
- }
- }
- public static void main(String[] args) {
- ApplicationContext ctx = new ClassPathXmlApplicationContext("com/baobaotao/connleak/applicatonContext.xml" );
- JdbcUserService userService = (JdbcUserService) ctx.getBean("jdbcUserService" );
- JdbcUserService. asynchrLogon(userService, "tom");
- }
- }</span>
在這個例子中,main線程拿到一個UserService實例,獲取一個Connection的副本,它會被Spring管理,不會泄露。UserServiceRunner 線程手動從數據源拿了一個Connection但沒有關閉因此會泄露。
如果希望使UserServiceRunner能拿到UserService中那個Connection們就要使用DataSourceUtils類,DataSourceUtils.getConnection()方法會首先查看當前是否存在事務管理上下文,如果存在就嘗試從事務管理上下文拿連接,如果獲取失敗,直接從數據源中拿。在獲取連接后,如果存在事務管理上下文則把連接綁定上去。
實際上,上面的代碼只用改動一行,把login()方法中獲取連接那行改成就可以做到:
Connection
conn = DataSourceUtils.
getConnection( jdbcTemplate .getDataSource());
需要注意的是:如果DataSourceUtils在沒有事務上下文的方法中使用getConnection()獲取連接,依然要手動管理這個連接!
此外,開啟了事務的方法要在整個事務方法結束后才釋放事務上下文綁定的Connection連接,而沒有開啟事務的方法在調用完Spring的Dao模板方法后立刻釋放。
多線程一定要與事務掛鈎么?
不是!即便沒有開啟事務,利用ThreadLocal機制也能保證線程安全,Dao照樣可以操作數據。但是事務和多線程確實糾纏不清,上文已經分析了在多線程下事務傳播行為、事務對Connection獲取的影響。
結論:
- Spring中DAO和Service都是以單實例的bean形式存在,Spring通過ThreadLocal類將有狀態的變量(例如數據庫連接Connection)本地線程化,從而做到多線程狀況下的安全。在一次請求響應的處理線程中, 該線程貫通展示、服務、數據持久化三層,通過ThreadLocal使得所有關聯的對象引用到的都是同一個變量。
- 在事務屬性為REQUIRED時,在相同線程中進行相互嵌套調用的事務方法工作於相同的事務中。如果互相嵌套調用的事務方法工作在不同線程中,則不同線程下的事務方法工作在獨立的事務中。
- 程序只要使用SpringDAO模板,例如JdbcTemplate進行數據訪問,一定沒有數據庫連接泄露問題!如果程序中顯式的獲取了數據連接Connection,則需要手工關閉它,否則就會泄露!
- 當Spring事務方法運行時,就產生一個事務上下文,它在本事務執行線程中對同一個數據源綁定了一個唯一的數據連接,所有被該事務上下文傳播的方法都共享這個連接。要獲取這個連接,如要使用Spirng的資源獲取工具類DataSourceUtils。
- 事務管理上下文就好比一個盒子,所有的事務都放在里面。如果在某個事務方法中開啟一個新線程,新線程中執行另一個事務方法,則由上面第二條可知這兩個方法運行於兩個獨立的事務中,但是:如果使用DataSourcesUtils,則新線程中的方法可以從事務上下文中獲取原線程中的數據連接!