ThreadLocal的使用場景分析

目錄

一.ThreadLocal介紹html

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

  2.1 問題背景算法

  2.2 方案1-修改接口傳參sql

  2.3 方案2-使用ThreadLocal數據庫

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

四.其餘使用場景cookie

 

 

一.ThreadLocal介紹

  咱們知道,變量從做用域範圍進行分類,能夠分爲「全局變量」、「局部變量」兩種:session

  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實現)。

 

  原文地址:http://www.javashuo.com/article/p-kxtwcpil-c.html 

相關文章
相關標籤/搜索