ThreadLocal 的原理講述 + 基于ThreadLocal實作MVC中的M層的事務控制

- ThreadLocal 的原理講述 + 基于ThreadLocal實作MVC中的M層的事務控制
- 每博一文案
- 1. ThreadLocal 給概述
- 2. 拋磚引玉——>ThreadLocal
- 3. ThreadLocal 的模擬撰寫
- 4. ThreadLocal 原始碼原理分析
- 5. ThreadLocal 常用方法
- 5.1 ThreadLocal的set()方法
- 5.2 ThreadLocal的get( )方法
- 5.3 ThreadLocal的remove( )方法
- 5.4 ThreadLocal 的 initialValue( )方法
- 5. ThreadLocal 常用方法
- 6. ThreadLocal 注意移除資料
- 7. ThreadLocal 記憶體泄漏
- 8. 正確的使用ThreadLocal
- 9. ThreadLocal 常見使用場景
- 10. 案例:MVC三層架構 + 面向介面編程 + ThreadLocal 事務處理實:現用戶轉賬功能的優化
- 10.1 M(Model 模型層/業務邏輯處理層)
- 10.2 C(Controller 控制層)
- 10.3 V(View 顯示層)
- 10.4 測驗
- 11. ThreadLocal與Synchronized的區別
- 12. ThreadLocal與Thread,ThreadLocalMap之間的關系
- 13. 總結:
- 14. 最后:
每博一文案
生活不是努力了就可以變好的,喜歡做的事情也不是輕易就可以做的,以前總聽別人說,
堅持就好了,努力就好了,都會好的,可是真的做起來壓根就不是這樣,這種時候要怎么辦?
這種時候還能輕易地相信時間嗎?
我總是一時間不知道怎么回答:直到今天我決定記錄這些日子的生活時,直到我寫完以上的文字時,我
腦海里才出現了一個清晰的答案,四個字:盡力而為,
我想這樣的,世事無常,分道揚鑣,生老病死,我們常常沒法得償所愿,
然而我們都必須盡力而為,
我覺得挺好的:把眼前的事情做好就行了,路都是走著走著才知道能走到哪里的,
越是焦慮,就越是要回到生活里去,因為身處迷霧中本就很難找到方向,能看見的也就
眼前的五米,那就五米五米地一步步走下去,
至于路能走成什么樣,又能走去哪里......
走著走著,就都知道了,
但或許其實終點到底是哪里也不是那么重要,
重要的是,我們走了很遠的路,最終找到的人,是我們自己,
是哪個可以很好地應對挫折,應對痛苦,應對生活的變故的自己,
是那個依然前行,依然努力,依然能夠為了小事而欣喜,為了善良而感動的自己,
是那個終于學會了珍惜的自己,是那個不再害怕平方的自己,
生活如河,自己就是自己的船,
——————盧思浩《你也走了,很遠的路吧》
1. ThreadLocal 給概述
ThreadLocal叫做執行緒變數,意思是ThreadLocal中填充的變數屬于當前執行緒 ,該變數對其他執行緒而言是隔離的,也就是說該變數是當前執行緒獨有的變數,ThreadLocal為變數在每個執行緒中都創建了一個副本,那么每個執行緒可以訪問自己內部的副本變數,
ThreadLoal 變數,執行緒區域變數,同一個 ThreadLocal 所包含的物件,在不同的 Thread 中有不同的副本,這里有幾點需要注意:
- 因為每個 Thread 內有自己的實體副本,且該副本只能由當前 Thread 使用,這是也是 ThreadLocal 命名的由來,
- 既然每個 Thread 有自己的實體副本,且其它 Thread 不可訪問,那就不存在多執行緒間共享的問題,
ThreadLocal 提供了執行緒本地的實體,它與普通變數的區別在于,每個使用該變數的執行緒都會初始化一個完全獨立的實體副本,ThreadLocal 變數通常被private static修飾,當一個執行緒結束時,它所使用的所有 ThreadLocal 相對的實體副本都可被回收,
- 這種變數在多執行緒環境下訪問(通過get和set方法訪問)時能保證各個執行緒的變數相對獨立于其他執行緒內的變數
- 在執行緒的生命周期內起作用,可以減少同一個執行緒內多個函式或組件之間一些公共變數傳遞的復雜度
總的來說,ThreadLocal 適用于每個執行緒需要自己獨立的實體且該實體需要在多個方法中被使用,也即變數在執行緒間隔離而在方法或類間共享的場景
下圖可以增強理解:

2. 拋磚引玉——>ThreadLocal
從上述一篇文章中:我們運用 MVC的架構模式——> 實作了用戶轉賬的功能:?????? MVC 三層架構案例詳細講解_ChinaRainbowSea的博客-CSDN博客 但是其中存在,一個事務處理的問題,
如下事務控制的處理是在:M(Model 模型層/業務邏輯處理層) 的原始碼(注意: 對應事務上的控制一定是在 M層當中的),我們可以看到其中并沒有進行一個事務上從處理,
package com.RainbowSea.bank.mvc;
/**
* service 翻譯為:業務,
* AccountService 專門處理Account業務的一個類
* 在該類中應該撰寫純業務代碼,(只專注域業務處理,不寫別的,不和其他代碼混合在一塊)
* 只希望專注業務,能夠將業務完美實作,少量bug.
* <p>
* 業務類一般起名:XXXService,XXXBiz...
*/
public class AccountService {
// 這里的方法起名,一定要體現出,你要處理的是什么業務:
// 我們要提供一個能夠實作轉賬的業務的方法(一個業務對應一個方法)
// 比如:UserService StudentService OrderService
// 處理Account 轉賬業務的增刪改查的Dao
private AccountDao accountDao = new AccountDao();
/**
* 完成轉賬的業務邏輯
*
* @param fromActno 轉出賬號
* @param toActno 轉入賬號
* @param money 轉賬金額
*/
public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException {
// 查詢余額是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
throw new MoneyNotEnoughException("對不起,余額不足");
}
// 程式到這里說明余額充足
Account toAct = accountDao.selectByActno(toActno);
// 修改金額,先從記憶體上修改,再從硬碟上修改
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
// 從硬碟資料庫上修改
int count = accountDao.update(fromAct);
count += accountDao.update(toAct);
if(count != 2) {
throw new AppException("賬戶轉賬例外,請聯系管理員");
}
}
}
如下:如果我們沒有進行事務處理控制存在一個什么樣的問題:
假如:
用戶 act001 ——> 轉賬給用戶 act002 ,10000元
轉賬的程序中,突然用戶 act001 網路出現了問題,轉賬失敗了,
注意:這里轉賬失敗了,用戶act001的錢是不應該減少的,因為我們沒有轉賬成功嘛,
可是這里,并沒有進行一個事務上的控制,導致的結果就是,我們轉賬失敗的,但是用戶 act001 的錢少 10000,用戶act002 的錢卻沒有增加,其中 用戶 act001 的 10000 元丟失在了,網路中,這是不可以的,用戶會發飆的,錢轉賬失敗了,錢還少了,這不是坑錢嘛,
如下測驗:

我們執行轉賬操作:act002 轉賬給用戶 act001 ,10000元,中途發生網路中斷:



下面我們進行“事務的控制”:
在Java中要進行事務的控制就需要使用到 Connectino物件了,
// 開啟事務,不會自動提交資料給資料庫
connection.setAutoCommit(false);
connection.commit(); // 提交資料
connection.rollback(); // 事務的回滾
package com.RainbowSea.bank.mvc;
import com.RainbowSea.bank.utils.DBUtil;
import java.sql.Connection;
import java.sql.SQLException;
/**
* service 翻譯為:業務,
* AccountService 專門處理Account業務的一個類
* 在該類中應該撰寫純業務代碼,(只專注域業務處理,不寫別的,不和其他代碼混合在一塊)
* 只希望專注業務,能夠將業務完美實作,少量bug.
* <p>
* 業務類一般起名:XXXService,XXXBiz...
*/
public class AccountService {
// 這里的方法起名,一定要體現出,你要處理的是什么業務:
// 我們要提供一個能夠實作轉賬的業務的方法(一個業務對應一個方法)
// 比如:UserService StudentService OrderService
// 處理Account 轉賬業務的增刪改查的Dao
private AccountDao accountDao = new AccountDao();
/**
* 完成轉賬的業務邏輯
*
* @param fromActno 轉出賬號
* @param toActno 轉入賬號
* @param money 轉賬金額
*/
public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException {
// 查詢余額是否充足
Account fromAct = accountDao.selectByActno(fromActno);
Connection connection = DBUtil.getConnection();
try {
if (fromAct.getBalance() < money) {
throw new MoneyNotEnoughException("對不起,余額不足");
}
// 程式到這里說明余額充足
Account toAct = accountDao.selectByActno(toActno);
// 修改金額,先從記憶體上修改,再從硬碟上修改
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
// 開啟事務,不會自動提交資料給資料庫
connection.setAutoCommit(false);
// 從硬碟資料庫上修改
int count = accountDao.update(fromAct);
// null 參考例外,模擬轉賬程序中發生網路例外,轉賬失敗
String s = null;
s.toString();
count += accountDao.update(toAct);
if(count != 2) {
throw new AppException("賬戶轉賬例外,請聯系管理員");
}
// 程式走到這說明,沒有問題,提交資料給資料庫
connection.commit(); // 提交資料
} catch (SQLException e) {
try {
connection.rollback(); // 事務的回滾
} catch (SQLException ex) {
throw new RuntimeException(ex);
}
throw new RuntimeException(e);
} finally {
DBUtil.close(connection,null,null);
}
}
}

我們再次進行一個,轉賬看看是否,真的做到的事務的處理:也就是,轉賬失敗的,act001用戶的錢不會減少,

如果結果:為什么我們使用了Connection 物件進行了一個事務的控制,但是,還是會少錢???事務上并沒有控制成功,
為什么會出現如上:情況,明明我們使用了 Connection 進行了一個事務上的控制,但是,卻還是會少錢,就沒有把事務真正控制上了,
解釋:
這里你卻是是將事務開啟了,但是,你開啟的事務是對于當前:AccountService類當中的transfer( )方法當中的Connetion 區域變數進行了一個事務上的控制,我們對資料庫的修改是存在兩個位置的:
其中這兩個位置上的 accountDao.update() 方法中同樣是存在了一個 Connection 物件的
簡單的說就是:我們對資料的更新,需要通過:兩個位置上的更新
- AccountService類當中的transfer( )方法當中的Connetion 區域變數進行了一個事務上的控制,
- AccountDao 類當中的update() 方法到當中的Connection 區域變數進行一個事務上的控制,
存在一個問題就是:我們這里的service 層雖然進行了事務的控制,但是這里的使用的 Connection事務控制 的物件是不一致的,也就是說:我們Connection的事務控制對應不上,我們對資料庫修改的操作上,就導致無法對 資料庫進行事務控制,操作同一個事務,但是存在兩個Connection ,而且這兩者之間的Connection 物件是不一致的,就會導致事務的控制失敗,
因為你控制了一個Connection,但是還存在一個Connection ,沒有對事務進行控制,
如何解決上述問題:
解決方法:
既然一個操作同一個事務,存在兩個不同的Connection,
那我們就控制成:同一個事務,雖然存在兩個Connection,但是它們的值是一樣的,也就是同一個事務上的處理,一個Connection就夠了,
怎么做到,共用一個Connection,我們可以通過傳參考型別引數的方式:
如下修改:

測驗:



優化:
雖然我們上述:通過傳參考型別的引數,對Connetion 物件進行了共用的操作,
但是存在一個問題就是:我們每次對資料庫操作,進行一個事務的處理,在 M(Model)層都要先創建一個Connection物件,并將該物件作為引數傳送給 XXxDao,這樣的操作,大大提高代碼的耦合度,背離了 "高內聚,低耦合" 的思想,
有沒有別的方法,將Connetion 存盤起來,做到同一個執行緒當中獲取到的Connection 都是同一個,不同的執行緒獲取到的Connection是不同的,
有的,我們的ThreadLocal 就實作了這種方式,
同一個執行緒,我們知道在同一個執行緒當的 Thread 執行緒物件是一樣的如下測驗:



,既然同一個執行緒的Thread 是一樣,那么我們可不可以,創建一個大Map ,將 Thread 作為 key ,其中Connection 作為value,所有需要同一個執行緒共用Connection物件的,都從這個 大Map當中獲取,因為同一個執行緒的 Thread 都是一樣的,而通過Thread 作為key ,獲取到的Value(也就是 Connection )也就是一樣的了,
3. ThreadLocal 的模擬撰寫
根據上述的講述,我們這里來模擬撰寫一個 大Map,將 Thread 作為 key ,其中Connection 作為value,其中這種在Java中就叫做:ThreadLocal

首先,這里我們先演示沒有使用:大Map的結果:
自定義的Connection 類
package com.rainbowSea.testThreadLocal;
public class MyConnection {
}
package com.rainbowSea.testThreadLocal;
public class UserDao {
public void insert(){
MyConnection myConnection = new MyConnection();
System.out.println("UserDao Connection : " + myConnection);
Thread thread = Thread.currentThread();
System.out.println("UserDao Thread : " + thread);
}
}
package com.rainbowSea.testThreadLocal;
public class UserService {
public UserDao userDao = new UserDao();
public void save() {
MyConnection myConnection = new MyConnection();
System.out.println("UserService Connection :" + myConnection);
Thread thread = Thread.currentThread();
System.out.println("UserService Thread : " + thread);
userDao.insert();
}
}
package com.rainbowSea.testThreadLocal;
public class Test {
public static void main(String[] args) {
MyConnection myConnection = new MyConnection();
System.out.println("Test Connection : " + myConnection);
Thread thread = Thread.currentThread();
System.out.println("Test Thread : " + thread);
UserService userService = new UserService();
userService.save();
}
}

使用 "大Map處理:"
package com.rainbowSea.testThreadLocal;
import java.util.HashMap;
import java.util.Map;
public class MyThreadLocal<T> {
/**
* 所有需要和當前執行緒系結的資料要放到整個容器當中
*/
private Map<Thread,T> map = new HashMap<Thread,T>();
/**
* 向ThreadLocal 中系結資料
* 注意是:Thread.currentThread() 當前執行緒作為 key 存在
*/
public void set(T t) {
map.put(Thread.currentThread(),t);
}
/**
* 向ThreadLocal 當中獲取資料
* 注意:獲取到的是當前執行緒的Connection系結的資料,
*/
public T get() {
// 通過 Thread.currentThread()當中執行緒作為key ,獲取到對應的 value值
return map.get(Thread.currentThread());
}
/**
* 移除ThreadLocal當中資料
* 注意:移除的是Thread.currentThread()當前執行緒作為key 存盤的資料資訊,
*/
public void remove() {
map.remove(Thread.currentThread());
}
}
package com.rainbowSea.testThreadLocal;
public class DBUtil {
// 靜態變數特點:類加載時執行,并且只執行一次
// 全域的大Map集合
public static MyThreadLocal<MyConnection> local = new MyThreadLocal<MyConnection>();
/**
* 每一次都呼叫這個方法來獲取Connection 物件
*/
public static MyConnection getConnection() {
// 從這個大的Map當中獲取 Connection 物件
MyConnection connection = local.get();
// 如果是第一次:獲取到的話這個 大MyThreadLocal 是沒有存盤到 Connection 物件的
// 所有我們需要向 MyThreadLocal 添加上
if (connection == null) {
connection = new MyConnection();
// 添加到 這個大Map當中
local.set(connection);
}
// 回傳從這個大Map當中獲取到的Connection物件
return connection;
}
}
package com.rainbowSea.testThreadLocal;
public class Test {
public static void main(String[] args) {
// 從大Map MyThreadLocal中獲取Connection物件
MyConnection myConnection = DBUtil.getConnection();
System.out.println("Test Connection : " + myConnection);
UserService userService = new UserService();
userService.save();
}
}
package com.rainbowSea.testThreadLocal;
public class UserService {
public UserDao userDao = new UserDao();
public void save() {
// 從大Map MyThreadLocal中獲取Connection物件
MyConnection myConnection = DBUtil.getConnection();
System.out.println("UserService Connection :" + myConnection);
userDao.insert();
}
}
package com.rainbowSea.testThreadLocal;
public class UserDao {
public void insert(){
// 從大Map MyThreadLocal中獲取Connection物件
MyConnection myConnection = DBUtil.getConnection();
System.out.println("UserDao Connection : " + myConnection);
}
}
測驗:

4. ThreadLocal 原始碼原理分析
下面我們來看看,Java為我們提供的 ThreadLocal 類吧


ThreadLocal的主要用途是實作執行緒間變數的隔離,表面上他們使用的是同一個ThreadLocal, 但是實際上使用的值value卻是自己獨有的一份, 用一圖直接表示threadlocal 的使用方式

從圖中我們可以當執行緒使用threadlocal 時,是將threadlocal當做當前執行緒thread的屬性ThreadLocalMap 中的一個Entry的key值,實際上存放的變數是Entry的value值,我們實際要使用的值是value值, value值為什么不存在并發問題呢,因為它只有一個執行緒能訪問,threadlocal我們可以當做一個索引看待,可以有多個threadlocal 變數,不同的threadlocal對應于不同的value值,他們之間互不影響,ThreadLocal為每一個執行緒都提供了變數的副本,使得每個執行緒在某一時間訪問到的并不是同一個物件,這樣就隔離了多個執行緒對資料的資料共享,簡單的說就是:實作一個執行緒當中的資訊物件共用,共享,代替傳參考型別引數的方式,
這里我們使用ThreadLocal是基于一個用戶轉賬的案例來講解的,為了解決事務上控制問題,一個執行緒共用一個Connection ,其中的我們的ThreadLocal 就作為了一個容器,其中的key 存盤的就是當前執行緒,而value值則是對應Connection

5. ThreadLocal 常用方法

| 方法名 | 描述 |
|---|---|
| ThreadLocal() | 創建ThreadLocal物件 |
| public void set( T value) | 設定當前執行緒系結的區域變數 |
| public T get() | 獲取當前執行緒系結的區域變數 |
| public T remove() | 移除當前執行緒系結的區域變數,該方法可以幫助JVM進行GC |
| protected T initialValue() | 回傳當前執行緒區域變數的初始值 |
5.1 ThreadLocal的set()方法
public void set(T value) {
//1、獲取當前執行緒
Thread t = Thread.currentThread();
//2、獲取執行緒中的屬性 threadLocalMap ,如果threadLocalMap 不為空,
//則直接更新要保存的變數值,否則創建threadLocalMap,并賦值
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 初始化thradLocalMap 并賦值
createMap(t, value);
}
/**
* 設定當前執行緒對應的ThreadLocal的值
* @param value 將要保存在當前執行緒對應的ThreadLocal的值
*/
public void set(T value) {
// 獲取當前執行緒物件
Thread t = Thread.currentThread();
// 獲取此執行緒物件中維護的ThreadLocalMap物件
ThreadLocalMap map = getMap(t);
// 判斷map是否存在
if (map != null)
// 存在則呼叫map.set設定此物體entry,this這里指呼叫此方法的ThreadLocal物件
map.set(this, value);
else
// 1)當前執行緒Thread 不存在ThreadLocalMap物件
// 2)則呼叫createMap進行ThreadLocalMap物件的初始化
// 3)并將 t(當前執行緒)和value(t對應的值)作為第一個entry存放至ThreadLocalMap中
createMap(t, value);
}
/**
* 獲取當前執行緒Thread對應維護的ThreadLocalMap
*
* @param t the current thread 當前執行緒
* @return the map 對應維護的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
*創建當前執行緒Thread對應維護的ThreadLocalMap
* @param t 當前執行緒
* @param firstValue 存放到map中第一個entry的值
*/
void createMap(Thread t, T firstValue) {
//這里的this是呼叫此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
從上面的代碼可以看出,ThreadLocal set賦值的時候首先會獲取當前執行緒thread,并獲取thread執行緒中的ThreadLocalMap屬性,如果map屬性不為空,則直接更新value值,如果map為空,則實體化threadLocalMap,并將value值初始化,
那么ThreadLocalMap又是什么呢,還有createMap又是怎么做的,我們繼續往下看,大家最后自己再idea上跟下原始碼,會有更深的認識,
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = https://www.cnblogs.com/TheMagicalRainbowSea/archive/2023/05/17/v;
}
}
}
可看出ThreadLocalMap是ThreadLocal的內部靜態類,而它的構成主要是用Entry來保存資料 ,而且還是繼承的弱參考,在Entry內部使用ThreadLocal作為key,使用我們設定的value作為value,詳細內容要大家自己去跟,
//這個是threadlocal 的內部方法
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//ThreadLocalMap 構造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
- 獲取當前執行緒,并根據當前執行緒獲取一個Map
- 如果獲取的Map不為空,則將引數設定到Map中(當前ThreadLocal的參考作為key)
- 如果Map為空,則給該執行緒創建 Map,并設定初始值

5.2 ThreadLocal的get( )方法
/**
* 回傳當前執行緒中保存ThreadLocal的值
* 如果當前執行緒沒有此ThreadLocal變數,
* 則它會通過呼叫{@link #initialValue} 方法進行初始化值
* @return 回傳當前執行緒對應此ThreadLocal的值
*/
public T get() {
// 獲取當前執行緒物件
Thread t = Thread.currentThread();
// 獲取此執行緒物件中維護的ThreadLocalMap物件
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以當前的ThreadLocal 為 key,呼叫getEntry獲取對應的存盤物體e
ThreadLocalMap.Entry e = map.getEntry(this);
// 對e進行判空
if (e != null) {
@SuppressWarnings("unchecked")
// 獲取存盤物體 e 對應的 value值,即為我們想要的當前執行緒對應此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
/*
初始化 : 有兩種情況有執行當前代碼
第一種情況: map不存在,表示此執行緒沒有維護的ThreadLocalMap物件
第二種情況: map存在, 但是沒有與當前ThreadLocal關聯的entry
*/
return setInitialValue();
}
/**
* 初始化
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 呼叫initialValue獲取初始化的值
// 此方法可以被子類重寫, 如果不重寫默認回傳null
T value = https://www.cnblogs.com/TheMagicalRainbowSea/archive/2023/05/17/initialValue();
// 獲取當前執行緒物件
Thread t = Thread.currentThread();
// 獲取此執行緒物件中維護的ThreadLocalMap物件
ThreadLocalMap map = getMap(t);
// 判斷map是否存在
if (map != null)
// 存在則呼叫map.set設定此物體entry
map.set(this, value);
else
// 1)當前執行緒Thread 不存在ThreadLocalMap物件
// 2)則呼叫createMap進行ThreadLocalMap物件的初始化
// 3)并將 t(當前執行緒)和value(t對應的值)作為第一個entry存放至ThreadLocalMap中
createMap(t, value);
// 回傳設定的值value
return value;
}
執行流程
- 獲取當前執行緒, 根據當前執行緒獲取一個Map
- 如果獲取的Map不為空,則在Map中以ThreadLocal的參考作為key來在Map中獲取對應的Entrye,否則轉到4
- 如果e不為null,則回傳e.value,否則轉到4
- Map為慷訓者e為空,則通過initialValue函式獲取初始值value,然后用ThreadLocal的參考和value作為firstKey和firstValue創建一個新的Map

5.3 ThreadLocal的remove( )方法
/**
* 洗掉當前執行緒中保存的ThreadLocal對應的物體entry
*/
public void remove() {
// 獲取當前執行緒物件中維護的ThreadLocalMap物件
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在則呼叫map.remove
// 以當前ThreadLocal為key洗掉對應的物體entry
m.remove(this);
}
執行流程:
- 首先獲取當前執行緒,并根據當前執行緒獲取一個Map
- 如果獲取的Map不為空,則移除當前ThreadLocal物件對應的entry
remove()方法,直接將ThrealLocal 對應的值從當前相差Thread中的ThreadLocalMap中洗掉,
為什么要洗掉,這涉及到記憶體泄露的問題?,
實際上 ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱參考,弱參考的特點是,如果這個物件只存在弱參考,那么在下一次垃圾回收的時候必然會被清理掉,
所以如果 ThreadLocal 沒有被外部強參考的情況下,在垃圾回收的時候會被清理掉的,這樣一來 ThreadLocalMap中使用這個 ThreadLocal 的 key 也會被清理掉,但是,value 是強參考,不會被清理,這樣一來就會出現 key 為 null 的 value,
ThreadLocal其實是與執行緒系結的一個變數,如此就會出現一個問題:如果沒有將ThreadLocal內的變數洗掉(remove)或替換,它的生命周期將會與執行緒共存,通常執行緒池中對執行緒管理都是采用執行緒復用的方法,在執行緒池中執行緒很難結束甚至于永遠不會結束,這將意味著執行緒持續的時間將不可預測,甚至與JVM的生命周期一致,舉個例字,如果ThreadLocal中直接或間接包裝了集合類或復雜物件,每次在同一個ThreadLocal中取出物件后,再對內容做操作,那么內部的集合類和復雜物件所占用的空間可能會開始持續膨脹,
5.4 ThreadLocal 的 initialValue( )方法
- 此方法的作用是回傳該執行緒區域變數的初始值
- 這個方法是一個延遲呼叫方法,從上面的代碼我們得知,在set方法還未呼叫而先呼叫了get方法時才執行,并且僅執行1次
- 這個方法預設實作直接回傳一個null
- 如果想要一個除null之外的初始值,可以重寫此方法,(備注: 該方法是一個protected的方法,顯然是為了讓子類覆寫而設計的)
/**
* 回傳當前執行緒對應的ThreadLocal的初始值
* 此方法的第一次呼叫發生在,當執行緒通過get方法訪問此執行緒的ThreadLocal值時
* 除非執行緒先呼叫了set方法,在這種情況下,initialValue 才不會被這個執行緒呼叫,
* 通常情況下,每個執行緒最多呼叫一次這個方法,
*
* <p>這個方法僅僅簡單的回傳null {@code null};
* 如果想ThreadLocal執行緒區域變數有一個除null以外的初始值,
* 必須通過子類繼承{@code ThreadLocal} 的方式去重寫此方法
* 通常, 可以通過匿名內部類的方式實作
*
* @return 當前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}
6. ThreadLocal 注意移除資料
當我們對于 ThreadLocal 中的value殖澩物件,使用完畢的時候,一定要執行ThreadLocal.remove()物件方法的移除系結在ThreadLocal中的資源資訊,因為一般ThreadLocal的使用場景都是在 多執行緒的,而多執行緒一般都是使用執行緒池管理執行緒的,就會存在一個問題?
我們通過上述賬戶轉賬案例來講解這個問題:
我們的運行環境是在Tomcat10,Tomcat10本身就是一個多執行緒的,
如下當我們對應一個資料庫操作完以后,我們需要將對應的資源釋放,最后使用的最先關閉,分開 try,防止關閉資源的時候出現例外導致其他資源沒有關閉,
如下:我們的ThreadLocal對應的 value 是 Connection 物件
// 創建 ThreadLocal 容器存盤系結執行緒相關的 資訊
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
/**
* 這里沒有使用資料庫連接池,直接創建連接物件
*/
public static Connection getConnection() {
Connection connection = threadLocal.get(); // 從ThreadLocal容器中獲取
try {
// 第一次ThreadLocal 是為空的
if (connection == null) {
connection = DriverManager.getConnection(url, user, password);
threadLocal.set(connection);
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return connection;
}
if (connection != null) {
try {
connection.close();
threadLocal.remove(); // 注意關閉資源的時候需要將系結在threadLocal移除
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
當我們將Connection 資源關閉了,但是:其中該Connection 存盤在ThreadLocal當中value值,卻是還存盤著是為 當前Connection 關閉了的物件的,由于Tomcat是多執行緒,其中Tomcat服務器是內置了一個執行緒池的,執行緒池中的多執行緒物件是有限的,
這樣執行緒物件 t1,t2,t3 都是提前創建好的,也就是說,t1,t2,t3,是在重復使用的,如果你沒有將其 ThreadLocal.remove( 移除掉),
當新的用戶,一個新的執行緒,出現的時候,可能會獲取到上一個Connection 已經關閉了的物件,t1執行緒物件,從而導致的結果就是 這個新用戶使用的是 t1 這個對應上的Connection 物件已經關閉了,出現錯誤,
所以對于: ThreadLocal 中的value殖澩物件,使用完畢的時候,一定要執行ThreadLocal.remove()物件方法的移除系結在ThreadLocal中的資源資訊

7. ThreadLocal 記憶體泄漏
1 . 沒有手動洗掉這個 Entry
2 . CurrentThread 當前執行緒依然運行
? 第一點很好理解,只要在使用完下 ThreadLocal ,呼叫其 remove 方法洗掉對應的 Entry ,就能避免記憶體泄漏,
? 第二點稍微復雜一點,由于ThreadLocalMap 是 Thread 的一個屬性,被當前執行緒所參考,所以ThreadLocalMap的生命周期跟 Thread 一樣長,如果threadlocal變數被回收,那么當前執行緒的threadlocal 變數副本指向的就是key=null, 也即entry(null,value),那這個entry對應的value永遠無法訪問到,實際私用ThreadLocal場景都是采用執行緒池,而執行緒池中的執行緒都是復用的,這樣就可能導致非常多的entry(null,value)出現,從而導致記憶體泄露,
綜上, ThreadLocal 記憶體泄漏的根源是:
由于ThreadLocalMap 的生命周期跟 Thread 一樣長,對于重復利用的執行緒來說,如果沒有手動洗掉(remove()方法)對應 key 就會導致entry(null,value)的物件越來越多,從而導致記憶體泄漏.
8. 正確的使用ThreadLocal
- 將ThreadLocal變數定義成private static的,這樣的話ThreadLocal的生命周期就更長,由于一直存在ThreadLocal的強參考,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱參考訪問到Entry的value值,然后remove它,防止記憶體泄露
- 每次使用完ThreadLocal,都呼叫它的remove()方法,清除資料,
9. ThreadLocal 常見使用場景
如上文所述,ThreadLocal 適用于如下兩種場景
- 每個執行緒需要有自己單獨的實體
- 實體需要在多個方法中共享,但不希望被多執行緒共享
對于第一點,每個執行緒擁有自己實體,實作它的方式很多,例如可以在執行緒內部構建一個單獨的實體,ThreadLoca 可以以非常方便的形式滿足該需求,
對于第二點,可以在滿足第一點(每個執行緒有自己的實體)的條件下,通過方法間參考傳遞的形式實作,ThreadLocal 使得代碼耦合度更低,且實作更優雅,
10. 案例:MVC三層架構 + 面向介面編程 + ThreadLocal 事務處理實:現用戶轉賬功能的優化
如下是我們對于上篇MVC三層架構?????? MVC 三層架構案例詳細講解_ChinaRainbowSea的博客-CSDN博客 存在事務安全問題的優化:
對應的包架構:
- resources: 表示一些資源:比如這里是一些連接資料庫的一些配置資訊:
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mvc
user=root
password=MySQL
- lib : 該專案所需要的依賴:注意:該目錄名一定要為:
lib,不然Tomcat 無法識別到,該目錄一定要在web/WEB-INF/下,不然Tomcat 無法識別到的,

- M(Model 模型層/業務邏輯處理層)
- utils : 工具包,這里使用一個資料庫連接的工具包,
- dao : 表示對應XXx資料表的業務邏輯上的處理(增刪改查),面向介面編程:
AccountDao,介面定義規范,這里的AccoutDao 定義了對于這張account資料表的業務邏輯上的操作規范,- Impl: 表示實作的介面的類:
AccountDaoImpl以Impl命名為后綴,表示介面的實作類,這是大家共識的一種規范,
- Impl: 表示實作的介面的類:
- exceptions : 表示對應業務上自定義的例外,
- javaBean: 表示對應的封裝資料的物體類,
- service: 表示對應的業務邏輯的處理,面向介面編程:
AccountService,介面定義規范,這里的AccountService定義了對于這張account資料表的業務邏輯處理上的操作規范,可能需要多個 Dao同時配合,獲取多個Service 之間相互配合,- Impl: 表示實作的介面的類:
AccountServiceImpl以Impl命名為后綴,表示介面的實作類,這是大家共識的一種規范,
- Impl: 表示實作的介面的類:
- C(Controller 控制層):對應 M層,V的之間的橋梁,進行一個調度處理,本身僅僅只做一個調度,不進行業務的處理,比如一個事情:需要調度M層進行處理,同時需要將該M層處理的結果,通過調度V層顯示給用戶,


10.1 M(Model 模型層/業務邏輯處理層)
package com.RainbowSea.bank.utils;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ResourceBundle;
public class DBUtil {
// resourceBundle 只能讀取到 properties 后綴的檔案,注意不要加檔案后綴名
private static ResourceBundle resourceBundle = ResourceBundle.getBundle("resources/jdbc");
private static String driver = resourceBundle.getString("driver");
private static String url = resourceBundle.getString("url");
private static String user = resourceBundle.getString("user");
private static String password = resourceBundle.getString("password");
// DBUtil 類加載注冊驅動
static {
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
// 將構造器私有化,不讓創建物件,因為工具類中的方法都是靜態的,不需要創建物件
// 為了防止創建物件,故將構造方法私有化
private DBUtil() {
}
// 創建 ThreadLocal 容器存盤系結執行緒相關的 資訊
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
/**
* 這里沒有使用資料庫連接池,直接創建連接物件
*/
public static Connection getConnection() {
Connection connection = threadLocal.get(); // 從ThreadLocal容器中獲取
try {
// 第一次ThreadLocal 是為空的
if (connection == null) {
connection = DriverManager.getConnection(url, user, password);
threadLocal.set(connection);
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return connection;
}
/**
* 資源的關閉
* 最后使用的最先關閉,逐個關閉,防止存在沒有關閉的
*/
public static void close(Connection connection, PreparedStatement preparedStatement, ResultSet resultSet) {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (connection != null) {
try {
connection.close();
threadLocal.remove(); // 注意關閉資源的時候需要將系結在threadLocal移除
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
}
package com.RainbowSea.bank.javabeen;
import java.io.Serializable;
import java.util.Objects;
/**
* 賬戶物體類,封裝賬戶資訊的
* 一般是一張表一個,
* pojo 物件
* 有的人也會把這種專門封裝資料的物件,稱為:"bean物件" (javabean物件,咖啡豆)
* 有的人也會把這種專門封裝資料的物件,稱為領域模型物件,domain物件
* 不同的程式員不同的習慣,
*/
public class Account implements Serializable { // 這種普通的簡單的物件被成為pojo物件
// 注意我們這里定義的資料型別,使用參考資料型別
// 因為我們資料庫中可能存在 null 值,而基本資料型別是不可以存盤 null值的
private Long id = null; // id
private String actno; // 賬號
private Double balance; // 余額
// 反序列化
private static final long serialVersionUID = 1L;
public Account() {
}
public Account(Long id, String actno, Double balance) {
this.id = id;
this.actno = actno;
this.balance = balance;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Account)) return false;
Account account = (Account) o;
return Objects.equals(getId(), account.getId()) && Objects.equals(getActno(), account.getActno()) && Objects.equals(getBalance(), account.getBalance());
}
@Override
public int hashCode() {
return Objects.hash(getId(), getActno(), getBalance());
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
}
package com.RainbowSea.bank.dao;
import com.RainbowSea.bank.javabeen.Account;
import java.util.List;
public interface AccountDao {
/**
* 插入資料
*
* @param account
* @return
*/
public int insert(Account account);
/**
* 通過Id洗掉資料
*
* @param id
* @return
*/
public int deleteById(String id);
/**
* 更新資料
*
* @param account
* @return
*/
public int update(Account account);
/**
* 通過 actno 查找賬戶資訊
*
* @param actno
* @return
*/
public Account selectByActno(String actno);
/**
* 查詢所有的賬戶資訊
*
* @return
*/
public List<Account> selectAll();
}
package com.RainbowSea.bank.dao.impl;
import com.RainbowSea.bank.dao.AccountDao;
import com.RainbowSea.bank.javabeen.Account;
import com.RainbowSea.bank.utils.DBUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* AccountDao 是負責Account 資料的增上改查
* <p>
* 1. 什么是DAO ?
* Data Access Object (資料訪問物件)
* 2. DAO實際上是一種設計模式,屬于 JavaEE的設計模式之一,不是 23種設計模式
* 3.DAO只負責資料庫表的CRUD ,沒有任何業務邏輯在里面
* 4.沒有任何業務邏輯,只負責表中資料增上改查的物件,有一個特俗的稱謂:DAO物件
* 5. 為什么叫做 AccountDao 呢?
* 這是因為DAO是專門處理t_act 這張表的
* 如果處理t_act 表的話,可以叫做:UserDao
* 如果處理t-student表的話,可以叫做 StudentDao
* <p>
* int insert() ;
* int deleteByActno();
* int update() ;
* Account selectByActno();
* List<Account> selectAll();
*/
public class AccountDaoImpl implements AccountDao {
/**
* 插入資料
*
* @param account
* @return
*/
public int insert(Account account) {
Connection connection = DBUtil.getConnection();
PreparedStatement preparedStatement = null;
int count = 0;
try {
String sql = "insert into t_act(actno,balance) values(?,?)";
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, account.getActno());
preparedStatement.setDouble(2, account.getBalance());
count = preparedStatement.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(connection, preparedStatement, null);
}
return count;
}
/**
* 通過Id洗掉資料
*
* @param id
* @return
*/
public int deleteById(String id) {
Connection connection = DBUtil.getConnection();
int count = 0;
PreparedStatement preparedStatement = null;
try {
String sql = "delete from t_act where id = ?";
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, id);
count = preparedStatement.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(connection, preparedStatement, null);
}
return count;
}
/**
* 更新資料
*
* @param account
* @return
*/
public int update(Account account) {
PreparedStatement preparedStatement = null;
Connection connection = DBUtil.getConnection(); // 從 ThreadLocal中獲取到的
int count = 0;
System.out.println("update: "+connection);
try {
String sql = "update t_act set balance = ?, actno = ? where id = ?";
preparedStatement = connection.prepareStatement(sql);
//注意設定的 set型別要保持一致,
preparedStatement.setDouble(1, account.getBalance());
preparedStatement.setString(2, account.getActno());
preparedStatement.setLong(3, account.getId());
count = preparedStatement.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(null, preparedStatement, null);
}
return count;
}
/**
* 通過 actno 查找賬戶資訊
*
* @param actno
* @return
*/
public Account selectByActno(String actno) {
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
Account account = new Account();
Connection connection = DBUtil.getConnection(); // 從 ThreadLocal中獲取到的
System.out.println("selectByActno :" + connection);
try {
String sql = "select id,actno,balance from t_act where actno = ?";
preparedStatement = connection.prepareStatement(sql);
//注意設定的 set型別要保持一致,
preparedStatement.setString(1, actno);
resultSet = preparedStatement.executeQuery();
if (resultSet.next()) {
Long id = resultSet.getLong("id");
Double balance = resultSet.getDouble("balance");
// 將結果集封裝到java 物件中
account.setActno(actno);
account.setId(id);
account.setBalance(balance);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(null, preparedStatement, resultSet);
}
return account;
}
/**
* 查詢所有的賬戶資訊
*
* @return
*/
public List<Account> selectAll() {
Connection connection = DBUtil.getConnection();
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
List<Account> list = null;
try {
String sql = "select id,actno,balance from t_act";
preparedStatement = connection.prepareStatement(sql);
resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
String actno = resultSet.getString("actno");
Long id = resultSet.getLong("id");
Double balance = resultSet.getDouble("balance");
// 將結果集封裝到java 物件中
Account account = new Account(id,actno,balance);
// 添加到List集合當中
list.add(account);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(connection, preparedStatement, resultSet);
}
return list;
}
}
package com.RainbowSea.bank.exceptions;
/**
* 余額不足例外
*/
public class AppException extends Exception{
public AppException() {
}
public AppException(String msg) {
super(msg);
}
}
package com.RainbowSea.bank.exceptions;
/**
* 余額不足例外
*/
public class MoneyNotEnoughException extends Exception{
public MoneyNotEnoughException() {
}
public MoneyNotEnoughException(String msg) {
super(msg);
}
}
package com.RainbowSea.bank.service;
import com.RainbowSea.bank.exceptions.AppException;
import com.RainbowSea.bank.exceptions.MoneyNotEnoughException;
public interface AccountService {
public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException;
}
package com.RainbowSea.bank.service.impl;
import com.RainbowSea.bank.dao.AccountDao;
import com.RainbowSea.bank.dao.impl.AccountDaoImpl;
import com.RainbowSea.bank.exceptions.AppException;
import com.RainbowSea.bank.exceptions.MoneyNotEnoughException;
import com.RainbowSea.bank.javabeen.Account;
import com.RainbowSea.bank.service.AccountService;
import com.RainbowSea.bank.utils.DBUtil;
import java.sql.Connection;
import java.sql.SQLException;
/**
* service 翻譯為:業務,
* AccountService 專門處理Account業務的一個類
* 在該類中應該撰寫純業務代碼,(只專注域業務處理,不寫別的,不和其他代碼混合在一塊)
* 只希望專注業務,能夠將業務完美實作,少量bug.
* <p>
* 業務類一般起名:XXXService,XXXBiz...
*/
public class AccountServiceImpl implements AccountService {
// 這里的方法起名,一定要體現出,你要處理的是什么業務:
// 我們要提供一個能夠實作轉賬的業務的方法(一個業務對應一個方法)
// 比如:UserService StudentService OrderService
// 處理Account 轉賬業務的增刪改查的Dao
private AccountDao accountDao = new AccountDaoImpl(); // 多型:父類的參考指向子類
/**
* 完成轉賬的業務邏輯
*
* @param fromActno 轉出賬號
* @param toActno 轉入賬號
* @param money 轉賬金額
*/
public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException {
Thread thread = Thread.currentThread(); // 獲取當前執行緒
System.out.println("seervice tranfer: " + thread);
Connection connection = DBUtil.getConnection(); // 從ThreadLocal獲取到的
// service 層控制事務:
// 事務的控制需要 Connection 物件
try { // 自動管理,會自動關閉資源
// 開啟事務
connection.setAutoCommit(false);
System.out.println("service transfer: " + connection);
// 查詢余額是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
throw new MoneyNotEnoughException("對不起,余額不足");
}
// 程式到這里說明余額充足
Account toAct = accountDao.selectByActno(toActno);
// 修改金額,先從記憶體上修改,再從硬碟上修改
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
// 從硬碟資料庫上修改
int count = accountDao.update(fromAct);
// 模擬例外
/* String s = null;
s.toString();*/
count += accountDao.update(toAct);
if (count != 2) {
throw new AppException("賬戶轉賬例外,請聯系管理員");
}
// 提交事務
connection.commit();
} catch (SQLException e) {
// 事務的回滾
// 因為我們這里是失敗了,是不會提交資料的,資料庫也就不會發生改變了,
throw new AppException("賬戶例外,請聯系管理員");
} finally {
// 關閉資源,移除ThreadLocal當中系結的 Connection 物件
DBUtil.close(connection, null, null);
}
}
}
10.2 C(Controller 控制層)
package com.RainbowSea.bank.web;
import com.RainbowSea.bank.exceptions.AppException;
import com.RainbowSea.bank.exceptions.MoneyNotEnoughException;
import com.RainbowSea.bank.service.AccountService;
import com.RainbowSea.bank.service.impl.AccountServiceImpl;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 賬戶小程式
* AccountServlet 是一個司令官,他負責調度其他組件來完成任務,
*
*/
@WebServlet("/transfer")
public class AccountServlet extends HttpServlet { // AccountServlet 作為一個 Controller 司令官
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException,
IOException {
// 獲取資料
String fromActno = request.getParameter("fromActno");
String toActno = request.getParameter("toActno");
double money = Double.parseDouble(request.getParameter("money"));
// 呼叫業務方法處理業務(調度Model處理業務,其中是對應資料表的 CRUD操作)
AccountService accountService = new AccountServiceImpl(); // 多型 父類的參考指向子類
try {
accountService.transfer(fromActno,toActno,money);
// 執行到這里說明,成功了,
// 展示處理結束(調度 View 做頁面展示)
response.sendRedirect(request.getContextPath()+"/success.jsp");
} catch (MoneyNotEnoughException e) {
// 執行到種類,說明失敗了,(余額不足
// 展示處理結束(調度 View 做頁面展示)
response.sendRedirect(request.getContextPath()+"/error.jsp");
} catch (AppException e) {
// 執行到種類,說明失敗了,轉賬例外
// 展示處理結束(調度 View 做頁面展示)
response.sendRedirect(request.getContextPath()+"/error.jsp");
}
// 頁面的展示 (調度View做頁面展示)
}
}
10.3 V(View 顯示層)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>銀行賬號轉賬</title>
</head>
<body>
<form action="<%=request.getContextPath()%>/transfer" method="post">
轉出賬戶: <input type="text" name="fromActno" /> <br>
轉入賬戶: <input type="text" name="toActno" /> <br>
轉賬金額: <input type="text" name="money" /><br>
<input type="submit" value="https://www.cnblogs.com/TheMagicalRainbowSea/archive/2023/05/17/轉賬" />
</form>
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>轉賬成功</title>
</head>
<body>
<h3>轉賬成功</h3>
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>轉賬失敗</title>
</head>
<body>
<h3>轉賬失敗</h3>
</body>
</html>
10.4 測驗





11. ThreadLocal與Synchronized的區別
ThreadLocal
但是ThreadLocal與synchronized有本質的區別:
-
Synchronized用于執行緒間的資料共享,而ThreadLocal則用于執行緒間的資料隔離,
-
Synchronized是利用鎖的機制,使變數或代碼塊在某一時該只能被一個執行緒訪問,而ThreadLocal為每一個執行緒都提供了變數的副本,使得每個執行緒在某一時間訪問到的并不是同一個物件,這樣就隔離了多個執行緒對資料的資料共享,
-
而Synchronized卻正好相反,它用于在多個執行緒間通信時能夠獲得資料共享,
一句話理解ThreadLocal,threadlocl是作為當前執行緒中屬性ThreadLocalMap集合中的某一個Entry的key值Entry(threadlocl,value),雖然不同的執行緒之間threadlocal這個key值是一樣,但是不同的執行緒所擁有的ThreadLocalMap是獨一無二的,也就是不同的執行緒間同一個ThreadLocal(key)對應存盤的值(value)不一樣,從而到達了執行緒間變數隔離的目的,但是在同一個執行緒中這個value變數地址是一樣的,
12. ThreadLocal與Thread,ThreadLocalMap之間的關系


Thread、THreadLocal、ThreadLocalMap之間啊的資料關系圖
從這個圖中我們可以非常直觀的看出,ThreadLocalMap其實是Thread執行緒的一個屬性值,而ThreadLocal是維護ThreadLocalMap這個屬性指的一個工具類,Thread執行緒可以擁有多個ThreadLocal維護的自己執行緒獨享的共享變數(這個共享變數只是針對自己執行緒里面共享)
13. 總結:
-
ThreadLocal叫做
執行緒變數,意思是ThreadLocal中填充的變數屬于當前執行緒,該變數對其他執行緒而言是隔離的,也就是說該變數是當前執行緒獨有的變數,ThreadLocal為變數在每個執行緒中都創建了一個副本,那么每個執行緒可以訪問自己內部的副本變數, -
ThreadLocal的主要用途是實作執行緒間變數的隔離,表面上他們使用的是同一個ThreadLocal, 但是實際上使用的值value卻是自己獨有的一份, 簡單的說就是:實作一個執行緒當中的資訊物件共用,共享,代替傳參考型別引數的方式,
-
ThreadLocal 常用的方法,
-
當我們對于 ThreadLocal 中的value殖澩物件,使用完畢的時候,一定要執行
ThreadLocal.remove()物件方法的移除系結在ThreadLocal中的資源資訊
14. 最后:
??????????????? 感謝如下博主的分享: ??????????????
【1】https://blog.csdn.net/u010445301/article/details/111322569?spm=1001.2014.3001.5502
【2】https://blog.csdn.net/u010445301/article/details/124935802?csdn_share_tail
【3】https://blog.csdn.net/silence_yb/article/details/124265702?ops_request_misc
限于自身水平,其中存在的錯誤,希望大家,給予指教,韓信點兵——多多益善,謝謝大家,江湖再見,后會有期!!!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/552676.html
標籤:其他
上一篇:認識Java
下一篇:返回列表





