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)

https://blog.csdn.net/a754895/article/details/82557395

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章