主頁 > 後端開發 > ThreadLocal 的原理講述 + 基于ThreadLocal實作MVC中的M層的事務控制

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

2023-05-18 07:42:13 後端開發

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( )方法
  • 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 物件的

在這里插入圖片描述

簡單的說就是:我們對資料的更新,需要通過:兩個位置上的更新

  1. AccountService類當中的transfer( )方法當中的Connetion 區域變數進行了一個事務上的控制,
  2. 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 的使用方式

img

從圖中我們可以當執行緒使用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);
        }
  1. 獲取當前執行緒,并根據當前執行緒獲取一個Map
  2. 如果獲取的Map不為空,則將引數設定到Map中(當前ThreadLocal的參考作為key)
  3. 如果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;
    }

執行流程

  1. 獲取當前執行緒, 根據當前執行緒獲取一個Map
  2. 如果獲取的Map不為空,則在Map中以ThreadLocal的參考作為key來在Map中獲取對應的Entrye,否則轉到4
  3. 如果e不為null,則回傳e.value,否則轉到4
  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);
     }

執行流程:

  1. 首先獲取當前執行緒,并根據當前執行緒獲取一個Map
  2. 如果獲取的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

  1. 將ThreadLocal變數定義成private static的,這樣的話ThreadLocal的生命周期就更長,由于一直存在ThreadLocal的強參考,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱參考訪問到Entry的value值,然后remove它,防止記憶體泄露
  2. 每次使用完ThreadLocal,都呼叫它的remove()方法,清除資料,

9. ThreadLocal 常見使用場景

如上文所述,ThreadLocal 適用于如下兩種場景

  1. 每個執行緒需要有自己單獨的實體
  2. 實體需要在多個方法中共享,但不希望被多執行緒共享

對于第一點,每個執行緒擁有自己實體,實作它的方式很多,例如可以在執行緒內部構建一個單獨的實體,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: 表示實作的介面的類:AccountDaoImplImpl命名為后綴,表示介面的實作類,這是大家共識的一種規范,
    • exceptions : 表示對應業務上自定義的例外,
    • javaBean: 表示對應的封裝資料的物體類,
    • service: 表示對應的業務邏輯的處理,面向介面編程:AccountService,介面定義規范,這里的AccountService定義了對于這張account資料表的業務邏輯處理上的操作規范,可能需要多個 Dao同時配合,獲取多個Service 之間相互配合,
      • Impl: 表示實作的介面的類:AccountServiceImplImpl命名為后綴,表示介面的實作類,這是大家共識的一種規范,
  • 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和Synchonized**都用于解決多執行緒并發訪問,

但是ThreadLocal與synchronized有本質的區別:

  1. Synchronized用于執行緒間的資料共享,而ThreadLocal則用于執行緒間的資料隔離,

  2. Synchronized是利用鎖的機制,使變數或代碼塊在某一時該只能被一個執行緒訪問,而ThreadLocal為每一個執行緒都提供了變數的副本,使得每個執行緒在某一時間訪問到的并不是同一個物件,這樣就隔離了多個執行緒對資料的資料共享,

  3. 而Synchronized卻正好相反,它用于在多個執行緒間通信時能夠獲得資料共享,

一句話理解ThreadLocal,threadlocl是作為當前執行緒中屬性ThreadLocalMap集合中的某一個Entry的key值Entry(threadlocl,value),雖然不同的執行緒之間threadlocal這個key值是一樣,但是不同的執行緒所擁有的ThreadLocalMap是獨一無二的,也就是不同的執行緒間同一個ThreadLocal(key)對應存盤的值(value)不一樣,從而到達了執行緒間變數隔離的目的,但是在同一個執行緒中這個value變數地址是一樣的,

12. ThreadLocal與Thread,ThreadLocalMap之間的關系

img

img

Thread、THreadLocal、ThreadLocalMap之間啊的資料關系圖

從這個圖中我們可以非常直觀的看出,ThreadLocalMap其實是Thread執行緒的一個屬性值,而ThreadLocal是維護ThreadLocalMap這個屬性指的一個工具類,Thread執行緒可以擁有多個ThreadLocal維護的自己執行緒獨享的共享變數(這個共享變數只是針對自己執行緒里面共享)

13. 總結:

  1. ThreadLocal叫做執行緒變數,意思是ThreadLocal中填充的變數屬于當前執行緒 ,該變數對其他執行緒而言是隔離的,也就是說該變數是當前執行緒獨有的變數,ThreadLocal為變數在每個執行緒中都創建了一個副本,那么每個執行緒可以訪問自己內部的副本變數,

  2. ThreadLocal的主要用途是實作執行緒間變數的隔離,表面上他們使用的是同一個ThreadLocal, 但是實際上使用的值value卻是自己獨有的一份, 簡單的說就是:實作一個執行緒當中的資訊物件共用,共享,代替傳參考型別引數的方式,

  3. ThreadLocal 常用的方法,

  4. 當我們對于 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

下一篇:返回列表

標籤雲
其他(159180) Python(38148) JavaScript(25431) Java(18055) C(15228) 區塊鏈(8267) C#(7972) AI(7469) 爪哇(7425) MySQL(7191) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5871) 数组(5741) R(5409) Linux(5340) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4572) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2433) ASP.NET(2403) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1975) 功能(1967) Web開發(1951) HtmlCss(1938) python-3.x(1918) C++(1917) 弹簧靴(1913) xml(1889) PostgreSQL(1878) .NETCore(1861) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • ThreadLocal 的原理講述 + 基于ThreadLocal實作MVC中的M層的事務

    ThreadLocal 的原理講述 + 基于ThreadLocal實作MVC中的M層的事務控制 每博一文案 生活不是努力了就可以變好的,喜歡做的事情也不是輕易就可以做的。以前總聽別人說, 堅持就好了,努力就好了,都會好的,可是真的做起來壓根就不是這樣。這種時候要怎么辦? 這種時候還能輕易地相信時間嗎 ......

    uj5u.com 2023-05-18 07:42:13 more
  • 認識Java

    Java的產生和發展 產生與發展歷程 1991年,由Sun公司開發Oak,最初為家用消費電子產品進行編程,是Java前身。 1994年,使用Oak語言撰寫了Web瀏覽器 1995年,改名為Java,96年發布JDK1.1 … 1998年,發布JDK1.2,從語言發展為平臺 … 2004年,發布JDK ......

    uj5u.com 2023-05-18 07:41:35 more
  • 位段/位域 的使用

    在一些特定的應用場景中,需要對一個整數型別的變數中的每個位進行單獨的控制或訪問。例如,硬體暫存器常常包含一些特定的位用于表示設備的狀態、配置選項或標志位。使用位段區可以使程式員更方便地訪問和控制這些位,而無需進行位運算或掩碼操作。(類似于位尋址?) 位段區使用特定的語法來定義和操作位段。在C語言中, ......

    uj5u.com 2023-05-18 07:41:30 more
  • Java中列印物件輸出的字串到底是什么

    列印輸出的Java物件是一知半解的字串,那么這個字串是怎么來的?代表什么?我們如何列印出物件中的資料呢? ......

    uj5u.com 2023-05-18 07:41:27 more
  • python中set和frozenset方法和區別

    set(可變集合)與frozenset(不可變集合)的區別: set無序排序且不重復,是可變的,有add(),remove()等方法。既然是可變的,所以它不存在哈希值。基本功能包括關系測驗和消除重復元素. 集合物件還支持union(聯合), intersection(交集), difference( ......

    uj5u.com 2023-05-18 07:41:23 more
  • MVC 三層架構案例詳細講解

    MVC 三層架構案例詳細講解 @ 每博一文案 多讀書,書中有,你對生活,困難所解不開的答案 比如:《殺死一只是更鳥》中提到的 對應我們:我們努力中考,高考,升本,考研,每天都在努力學習,但是某天突然想到萬一沒有考上的話,那現在的努力又有什么意義呢? 答案:在《殺死一只是更鳥》里有這樣一段話: > 勇 ......

    uj5u.com 2023-05-18 07:40:57 more
  • Python從零到壹丨帶你了解影像直方圖理論知識和繪制實作

    摘要:本文將從OpenCV和Matplotlib兩個方面介紹如何繪制直方圖,這將為影像處理像素對比提供有效支撐。 本文分享自華為云社區《[Python從零到壹] 五十.影像增強及運算篇之影像直方圖理論知識和繪制實作》,作者:eastmount。 一.影像直方圖理論知識 灰度直方圖是灰度級的函式,描述 ......

    uj5u.com 2023-05-18 07:40:23 more
  • java設計模式【抽象工廠模式】

    java設計模式【抽象工廠模式】 抽象工廠模式 抽象工廠模式是對簡單工廠模式的一個變種,它允許通過一個統一的介面來創建不同的產品實體,而無需指定具體的子類。在這個模式中,我們只關心產品的抽象介面,而將具體的產品實作留給子類去實作。這樣,我們可以通過創建不同的工廠物件來創建不同的產品實體,而無需關心它 ......

    uj5u.com 2023-05-18 07:40:11 more
  • web應用模式、API介面、介面測驗工具postman、如何在瀏覽器中測

    一、web應用模式 Django框架就是一種web框架,專門用來寫web專案,之前學的,寫的BBS專案,圖書管理系統,用的都是前后端混合開發 -后端人員,寫后端,也要寫【模板語法】 》xx.html的python代碼 -全堆疊開發-->前后端混合時代,比較多 從今天開始,學的是前后端分離 -后端人員, ......

    uj5u.com 2023-05-18 07:39:52 more
  • Django authenticate() 函式查找不到與提交的用戶名和密碼匹配的

    在你的user APP下面添加一個utils.py檔案 class UsernameMobileBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): """ 重寫 ......

    uj5u.com 2023-05-18 07:39:40 more