ThreadLocal的使用場景分析

目錄

一.ThreadLocal介紹

二.使用場景1——數據庫事務問題

  2.1 問題背景

  2.2 方案1-修改接口傳參

  2.3 方案2-使用ThreadLocal

三.使用場景2——日誌追蹤問題

四.其他使用場景

 

 

一.ThreadLocal介紹

  我們知道,變量從作用域範圍進行分類,可以分為“全局變量”、“局部變量”兩種:

  1.全局變量(global variable),比如類的靜態屬性(加static關鍵字),在類的整個生命周期都有效;

  2.局部變量(local variable),比如在一個方法中定義的變量,作用域只是在當前方法內,方法執行完畢后,變量就銷毀(釋放)了;

  使用全局變量,當多個線程同時修改靜態屬性,就容易出現併發問題,導致臟數據;而局部變量一般來說不會出現併發問題(在方法中開啟多線程併發修改局部變量,仍可能引起併發問題);

  再看ThreadLocal,從名稱上就能知道,它可以用來保存局部變量,只不過這個“局部”是指“線程”作用域,也就是說,該變量在該線程的整個生命周期中有效。

 

二.使用場景1——數據庫事務問題

2.1問題背景

  下面介紹示例,UserService調用UserDao刪除用戶信息,涉及到兩張表的操作,所以用到了數據庫事務:

  數據庫封裝類DbUtils

public class DbUtils {

    // 使用C3P0連接池
    private static ComboPooledDataSource dataSource = new ComboPooledDataSource("dev");

    public static Connection getConnectionFromPool() throws SQLException {
        return dataSource.getConnection();
    }

    // 省略其他方法.....
}

  UserService代碼如下:  

public class UserService {

    private UserDao userDao;

    public void deleteUserInfo(Integer id, String operator) {
        Connection connection = null;
        try {
            // 從連接池中獲取一個連接
            connection = DbUtils.getConnectionFromPool();
            // 因為涉及事務操作,所以需要關閉自動提交
            connection.setAutoCommit(false);

            // 事務涉及兩步操作,刪除用戶表,增加操作日誌
            userDao.deleteUserById(id);
            userDao.addOperateLog(id, operator);

            connection.commit();
        } catch (SQLException e) {
            // 回滾操作
            try {
                if (connection != null) {
                    connection.rollback();
                }
            } catch (SQLException ex) {
            }
        } finally {
            DbUtils.freeConnection(connection);
        }
    }
}

  下面是UserDao,省略了部分代碼:

package cn.ganlixin.dao;
import cn.ganlixin.util.DbUtils;
import java.sql.Connection;

/**
 * @author ganlixin
 * @create 2020-06-12
 */
public class UserDao {

    public void deleteUserById(Integer id) {
        // 從連接池中獲取一個數據連接
        Connection connection = DbUtils.getConnectionFromPool();

        // 利用獲取的數據庫連接,執行sql...........刪除用戶表的一條數據

        // 歸還連接給連接池
        DbUtils.freeConnection(connection);
    }

    public void addOperateLog(Integer id, String operator) {
        // 從連接池中獲取一個數據連接
        Connection connection = DbUtils.getConnectionFromPool();

        // 利用獲取的數據庫連接,執行sql...........插入一條記錄到操作日誌表

        // 歸還連接給連接池
        DbUtils.freeConnection(connection);
    }
}

  上面的代碼乍一看,好像沒啥問題,但是仔細看,其實是存在問題的!!問題出在哪兒呢?就出在從數據庫連接池獲取連接哪個位置。

  1.UserService會從數據庫連接池獲取一個連接,關閉該連接的自動提交;

  2.UserService然後調用UserDao的兩個接口進行數據庫操作;

  3.UserDao的兩個接口,都會從數據庫連接池獲取一個連接,然後執行sql;

  注意,第1步和第3步獲得的連接不一定是同一個!!!!這才是關鍵。

  如果UserService和UserDao獲取的數據庫連接不是同一個,那麼UserService中關閉自動提交的數據庫連接,並不是UserDao接口中執行sql的數據庫連接,當userService中捕獲異常,即使執行rollback,userDao中的sql已經執行完了,並不會回滾,所以數據已經出現不一致!!!

 

2.2方案1-修改接口傳參

  上面的例子中,因為UserService和UserDao獲取的連接不是同一個,所以並不能保證事務原子性;那麼只要能夠解決這個問題,就可以保證了

  可以修改userDao中的代碼,不要每次在UserDao中從數據庫連接池獲取連接,而是增加一個參數,該參數就是數據庫連接,有UserService傳入,這樣就能保證UserService和UserDao使用同一個數據庫連接了

public class UserDao {

    public void deleteUserById(Connection connection, Integer id) {
        // 利用傳入的數據庫連接,執行sql...........刪除用戶表的一條數據
    }

    public void addOperateLog(Connection connection, Integer id, String operator) {
        // 利用傳入的數據庫連接,執行sql...........插入一條記錄到操作日誌表
    }
}

  UserService調用接口時,傳入數據庫連接,修改代碼后如下:

// 事務涉及兩步操作,刪除用戶表,增加操作日誌
// 新增參數傳入數據庫連接,保證UserService和UserDao使用同一個連接
userDao.deleteUserById(connection, id);
userDao.addOperateLog(connection, id, operator);

  這樣做,的確是能解決數據庫事務的問題,但是並不推薦這樣做,耦合度太高,不利於維護,修改起來也不方便;

 

2.3使用ThreadLocal解決

  ThreadLocal可以保存當前線程有效的變量,正好適合解決這個問題,而且改動的點也特別小,只需要在DbUtils獲取連接的時候,將獲取到的連接存到ThreadLocal中即可:

public class DbUtils {

    // 使用C3P0連接池
    private static ComboPooledDataSource dataSource = new ComboPooledDataSource("dev");

    // 創建threadLocal對象,保存每個線程的數據庫連接對象
    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();

    public static Connection getConnectionFromPool() throws SQLException {
        if (threadLocal.get() == null) {
            threadLocal.set(dataSource.getConnection());
        }

        return threadLocal.get();
    }

    // 省略其他方法.....
}

  然後UserService和UserDao中,恢復最初的版本,UserService和UserDao中都調用DbUtils獲取數據庫連接,此時他們獲取到的連接則是同一個Connection對象,就可以解決數據庫事務問題了。

 

三.使用場景2——日誌追蹤問題

  如果理解了場景1的數據庫事務問題,那麼對於本小節的日誌追蹤,光看標題就知道是怎麼回事了;

  開發過程時,會在項目中打很多的日誌,一般來說,查看日誌的時候,都是通過關鍵字去找日誌,這就需要我們在打日誌的時候明確的寫入某些標識,比如用戶ID、訂單號、流水號…

  如果業務比較複雜,那麼一個請求的處理流程就會比較長,如果將這麼一長串的流程給串起來,也可以通過前面說的用戶ID、訂單號、流水號來串,但有個問題,某些接口沒有用戶ID或者訂單號作為參數!!!!這個時候,雖然可以像場景1中給接口增加用戶ID或者訂單號作為參數,但是並不推薦這麼做。

  此時可以就可以使用ThreadLocal,封裝一個工具類,提供唯一標識(可以是用戶ID、訂單號、或者是分佈式全局ID),示例如下:

package cn.ganlixin.util;

/**
 * 描述:
 * 日誌追蹤工具類,設置和獲取traceId,
 * 此處的traceId使用snowFlake雪花數算法,詳情可以參考:https://www.cnblogs.com/-beyond/p/12452632.html
 *
 * @author ganlixin
 * @create 2020-06-12
 */
public class TraceUtils {
    // 創建ThreadLocal靜態屬性,存Long類型的uuid
    private static final ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    // 全局id生成器(雪花數算法)
    private static final SnowFlakeIdGenerator generator = new SnowFlakeIdGenerator(1, 1);

    public static void setUuid(String uuid) {
        // 雪花數算法
        threadLocal.set(generator.nextId());
    }

    public static Long getUuid() {
        if (threadLocal.get() == null) {
            threadLocal.set(generator.nextId());
        }
        return threadLocal.get();
    }
}

  

  使用示例:

@Slf4j
public class UserService {

    private UserDao userDao;

    public void deleteUserInfo(Integer id, String operator) {
        log.info("traceId:{}, id:{}, operator:{}", TraceUtils.getUuid(), id, operator);
        
        //.....
    }
}

@Slf4j
public class UserDao {

    public void deleteUserById(Connection connection, Integer id) {
        log.info("traceId:{}, id:{}", TraceUtils.getUuid(), id);
    }
}

  

 四.其他場景

  其他場景,其實就是利用ThreadLocal“線程私有且線程間互不影響”特性,除了上面的兩個場景,常見的還有用來記錄用戶的登錄狀態(當然也可以用session或者cookie實現)。

 

  原文地址:https://www.cnblogs.com/-beyond/p/13111015.html 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

您可能也會喜歡…