ThreadLocal詳解,附帶例項(threadlocal實現銀行轉賬事務管理)
一.前言 在很早之前接觸到ThreadLocal很不瞭解一件事情,就是執行緒用來處理多執行緒情景,那為什麼要用threadlocal來再為每個執行緒分發一個單獨的變數副本,是否違背多執行緒的實際存在意義,而且threadlocal是否能用同步代替? 其實還是有很大差別的,同步和鎖解決問題最大的特點就是序列,雖然解決了問題,但是這樣效率大大降低;相比之下,threadlocal可以並行,通過為每個執行緒分發單獨的副本變數,來提高效率。兩者都可以避免執行緒不安全的問題。用網上很好理解的一句話來說:同步鎖是以時間換空間方式,而threadlocal是以空間換時間。各有各自的優缺點。 二.Threadlocal儲存形式
1. 專案前言 大家思考一下,轉賬的過程,假如A給B轉賬,需要兩個過程,即一A先減錢,二B再加錢,大家思考這種情況,如果A減錢資料庫操作沒有問題,但是B在加錢操作資料庫過程中出現異常,則造成A錢少了,B錢沒有改變。這在轉賬過程中是絕對不允許出現的情況。所以這裡要給資料庫操作新增事務管理,讓減錢和加錢融為一體要麼都操作要麼都不操作。 可能有人會說將兩個資料庫操作寫在一個方法裡,直接用資料庫事務不就ok了?是可以這只是兩個sql語句操作,那多個數據庫操作呢?這麼做還符合MVC設計模式,符合程式碼解耦性麼?答案當然是否定。 所以我們可以運用ThreadLocal來為一個執行緒儲存一個數據庫Connection連線,這樣不論多少資料庫操作,只要運用的是一個Connection,就可以增加事務管理,這樣極大的方便了我們想要實現的功能,而且不違背設計思想。 2. 程式碼實現
2.1DataSourc工具類(重點)
package com.qjl.utils; import java.io.IOException; import java.io.InputStream; import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; import javax.sql.DataSource; import com.alibaba.druid.pool.DruidDataSourceFactory; /** * DataSource工具類 * @author Joe * */ public class DataSourceUtils { //執行緒區域性變數(map集合,key為thrad,value為connection) private static ThreadLocal<Connection> threadLocal; private static DataSource ds; static { threadLocal = new ThreadLocal<>(); InputStream is = DataSourceUtils.class.getClassLoader().getResourceAsStream("database.properties"); Properties properties = new Properties(); try { properties.load(is); ds=DruidDataSourceFactory.createDataSource(properties); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } public static DataSource getDataSource() { return ds; } public static Connection getConnection() throws Exception{ //先從集合取 Connection connection = threadLocal.get(); if(connection == null) { connection = ds.getConnection(); threadLocal.set(connection); } return connection; } /** * 開啟事務(start transcation) */ public static void beginTranscation() throws Exception{ Connection connection = getConnection(); connection.setAutoCommit(false); } /** * 提交事務 * @throws Exception */ public static void commit() throws Exception{ Connection connection = getConnection(); connection.commit(); } /** * 回滾 * @throws Exception */ public static void rollback() throws Exception{ Connection connection = getConnection(); connection.rollback(); } /** * close * @throws Exception */ public static void close() throws Exception{ Connection connection = getConnection(); threadLocal.remove(); //key就是當前執行緒,從當前執行緒解綁 connection.close(); } }
這裡的DataSource也就是我們的資料庫連線工具類,通過getConnection()方法大家可以看到我們在這裡向ThreadLocal存入了一個Connection,可能有人會有疑惑,不是說他是map集合嗎?為什麼之儲存一個value呢?那key呢?當然,key就是我們當前執行緒,這在ThreadLocal內部已經寫好了所以不用我們存入了。 2.2 AccountDaoImpl 介面實現類(資料庫操作)
package com.qjl.dao.impl;
import java.sql.SQLException;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import com.qjl.dao.AccountDao;
import com.qjl.domain.Account;
import com.qjl.utils.DataSourceUtils;
public class AccountDaoImpl implements AccountDao{
QueryRunner qr = new QueryRunner();
@Override
public void update(Account account) {
try {
qr.update(DataSourceUtils.getConnection(),"update account set money=? where id=?",account.getMoney(),account.getId());
} catch (Exception e) {
throw new RuntimeException("更新賬戶失敗",e);
}
}
@Override
public Account findById(int id) {
Account account = null;
try {
account = qr.query(DataSourceUtils.getConnection(),"select * from account where id=?",new BeanHandler<>(Account.class),id);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return account;
}
}
在這裡我們可以看到,在呼叫QueryRunner 的update或query方法執行sql語句的時候,我們同時傳入了DataSourceUtils.getConnection(),這個其實就是我們剛剛給上面程式碼所編寫的Connection,通過ThreadLocal讓這個Connection是唯一,這樣不論多少個數據庫操作,這樣就都用的是一個Connection了,這裡說明一下(AB轉賬可以看成一個執行緒,CD轉賬是另一個執行緒,這樣AB轉賬過程用的是一個Connection,CD轉賬過程用的則是另一個Connection,這樣也就形成了我們多執行緒的實際例子)如圖(自認為圖比較清晰啦):
2.3 AccountServiceImpl服務層實現類
package com.qjl.service.impl;
import com.qjl.dao.AccountDao;
import com.qjl.dao.impl.AccountDaoImpl;
import com.qjl.domain.Account;
import com.qjl.service.AccountService;
import com.qjl.utils.DataSourceUtils;
public class AccountServiceImpl implements AccountService {
@Override
public void transMoney(int fromid, int toid, double money) {
AccountDao accountDao = new AccountDaoImpl();
try {
// 0開啟事務
DataSourceUtils.beginTranscation();
// 1查詢使用者
Account from = accountDao.findById(fromid);
Account to = accountDao.findById(toid);
// 2減錢
if (from == null && to == null) {
throw new RuntimeException("賬戶不存在");
}
if (money > from.getMoney()) {
throw new RuntimeException("餘額不足");
}
from.setMoney(from.getMoney() - money);
accountDao.update(from);
// 3加錢
to.setMoney(to.getMoney() + money);
accountDao.update(to);
// 4提交或者回滾
DataSourceUtils.commit();
} catch (Exception e) {
try {
DataSourceUtils.rollback();
DataSourceUtils.commit();
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
throw new RuntimeException(e.getMessage());
} finally {
try {
DataSourceUtils.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
在這裡我們先在開始時呼叫DataSourceUtils工具類中的beginTranscation()方法來開啟事務,如果出現異常程式碼進入catch中,所以我們在catch中呼叫DataSourceUtils.rollback();和DataSourceUtils.commit();方法來實現回滾(也就是回到事務開啟時的狀態),如果沒有異常才commit()提交事務,這樣就做到了要麼沒有問題直接轉賬,要麼不論轉賬過程中哪裡出現異常,都會回滾,防止造成錢的丟失或異常增多。
其他程式碼就不給啦,這些就夠了。
四.總結 ThreadLocal大家可以就把他當作一個特殊的Map集合,key是當前執行緒,value是我們所需要的儲存的變數,在多執行緒情況下,讓不同的執行緒操作不同的變數副本,這樣也就達成了我們想要執行緒安全的問題,同時併發也提高多執行緒的執行效率,當然ThreadLocal是不可以取代同步鎖的,因為ThreadLocal還是有很大的侷限性的,所以大家在使用時候一定要注意哦。