引言:
在使用Spring時,不少人可能對Spring中爲何DAO和Service對象採用單實例方式很迷惑,這些讀者是這麼認爲的:
DAO對象必須包含一個數據庫的鏈接Connection,而這個Connection不是線程安全的,因此每一個DAO都要包含一個不一樣的Connection對象實例,這樣一來DAO對象就不能是單實例的了。
上述觀點對了一半。對的是「每一個DAO都要包含一個不一樣的Connection對象實例」這句話,錯的是「DAO對象就不能是單實例」。
1。每一個線程中都有一個本身的ThreadLocalMap類對象,能夠將線程本身的對象保持到其中,各管各的,線程能夠正確的訪問到本身的對象。
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 {
-
- privatestatic ThreadLocal <Connection>connThreadLocal = newThreadLocal<Connection>();
- publicstatic Connection getConnection() {
-
-
- if (connThreadLocal.get() == null) {
- Connection conn = getConnection();
- connThreadLocal.set(conn);
- return conn;
- } else {
- return connThreadLocal.get();
-
- }
- }
- public voidaddTopic() {
-
- 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,則新線程中的方法能夠從事務上下文中獲取原線程中的數據鏈接!