1. 程式人生 > >ThreadLocal詳解,附帶例項(threadlocal實現銀行轉賬事務管理)

ThreadLocal詳解,附帶例項(threadlocal實現銀行轉賬事務管理)

一.前言   在很早之前接觸到ThreadLocal很不瞭解一件事情,就是執行緒用來處理多執行緒情景,那為什麼要用threadlocal來再為每個執行緒分發一個單獨的變數副本,是否違背多執行緒的實際存在意義,而且threadlocal是否能用同步代替?   其實還是有很大差別的,同步和鎖解決問題最大的特點就是序列,雖然解決了問題,但是這樣效率大大降低;相比之下,threadlocal可以並行,通過為每個執行緒分發單獨的副本變數,來提高效率。兩者都可以避免執行緒不安全的問題。用網上很好理解的一句話來說:同步鎖是以時間換空間方式,而threadlocal是以空間換時間。各有各自的優缺點。    二.Threadlocal儲存形式

  其實ThreadLocal實際上就是一個以當前執行緒為主鍵key,使用者存入的變數副本為Value的Map集合,從原始碼我們可以看到:   get方法   從ThreadLocal的get()方法原始碼中,我們可以清晰看到主鍵就是Thread型別的t,而這個t正是當前執行緒。   set方法   從ThreadLocal的set()方法我們可以看到,我們通過主鍵t,將我們所要儲存的變數副本存入這個ThreadLocalMap集合中。    三.運用ThreadLocal實現資料庫事務管理,實現銀行轉賬

1. 專案前言   大家思考一下,轉賬的過程,假如A給B轉賬,需要兩個過程,即一A先減錢,二B再加錢,大家思考這種情況,如果A減錢資料庫操作沒有問題,但是B在加錢操作資料庫過程中出現異常,則造成A錢少了,B錢沒有改變。這在轉賬過程中是絕對不允許出現的情況。所以這裡要給資料庫操作新增事務管理,讓減錢和加錢融為一體要麼都操作要麼都不操作。   可能有人會說將兩個資料庫操作寫在一個方法裡,直接用資料庫事務不就ok了?是可以這只是兩個sql語句操作,那多個數據庫操作呢?這麼做還符合MVC設計模式,符合程式碼解耦性麼?答案當然是否定。   所以我們可以運用ThreadLocal來為一個執行緒儲存一個數據庫Connection連線,這樣不論多少資料庫操作,只要運用的是一個Connection,就可以增加事務管理,這樣極大的方便了我們想要實現的功能,而且不違背設計思想。    2. 程式碼實現

  首先說明,這裡通過MVC設計模式設計儘可能做到解耦,中間有很多工具類,所以只給出重要部分程式碼,如有不清楚明白可以看另一篇部落格(關於Druid資料庫連線池實現Dao)

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還是有很大的侷限性的,所以大家在使用時候一定要注意哦。