多線程中ThreadLocal的使用

前言

多線程是Java的一個重要特性,多線程從某方面可以等價於多任務,當你有多個任務要處理時,多個任務一起做所消耗的時間肯定比任務串行起來做,所消耗的時間短。而對於多線程不熟悉的新手則容易踩到很多坑,最典型的則是變量問題。

概念介紹

下面先用簡單粗俗的語言解釋一下幾個基本概念

線程安全:多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其他線程不能進行訪問直到該線程讀取完,其他線程纔可使用。不會出現數據不一致或者數據污染。典型的例子爲StringBuffer類。

線程不安全:不提供數據訪問保護,有可能出現多個線程先後更改數據造成所得到的數據是髒數據。典型的例子爲StringBuilder類。Servlet和SpringMVC採用的是單例設計模式,因此也是線程不安全的。而aop中如果定義了成員變量,也是線程不安全的。

Java內存模型

參考我之前的文章Java內存模型介紹

結合Java內存模型的介紹可知,在單例模式下,多個線程操作同一個變量,會發生線程安全性問題簡單來說就是一個變量name在線程A中命名爲“李鐵蛋”,而線程B將其命名爲“李蛋”,此時線程A輸出變量name,極有可能輸出的是“李蛋”。因此,就需要使用ThreadLocal來給每個線程提供局部變量,解決線程安全問題。

示例

首先我們來看一下關於線程不安全的情況

public class test003 implements Runnable {

    private Res res;

    public test003(Res res) {
        this.res = res;
    }

    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "," + res.getNumber());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Res res = new Res();
        for (int i = 0; i < 4; i++) {
            new Thread(new test003(res)).start();
        }
    }

}

class Res {
    public Integer count = 0;

    public Integer getNumber() {
        return ++count;
    }

}

程序中的res變量則爲主內存中的變量,每個線程都會操作同一個res,獲取到的也是同一個count,因此其中一次運行打印出來的結果如下

Thread-0,1
Thread-1,2
Thread-2,3
Thread-3,4
Thread-0,5
Thread-1,6
Thread-2,7
Thread-3,8
Thread-0,9
Thread-1,10
Thread-2,11
。。。。。

程序的本意爲打印每個線程從1開始增長,而運行結果中,比如線程0,第一次爲1,第二次爲5,很明顯不符合要求,我們將程序部分代碼如下改造:

class Res {
    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    // 這裏其實可以使用JDK8的Lambda表達式簡化代碼
    // public static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public Integer getNumber() {
        int count = threadLocal.get() + 1;
        threadLocal.set(count);
        return count;
    }

}

改造後的代碼,使用ThreadLocal創建一個成員變量,泛型爲Integer,表示這個成員變量爲int類型。在getNumber方法中,執行的則是count++操作。threadLocal.get()方法的作用是獲取當前線程threadLocal中的值,+1之後獲取本次的count,並set回去。我們看一下輸出結果。

Thread-0,1
Thread-2,1
Thread-3,1
Thread-1,1
Thread-0,2
Thread-2,2
Thread-3,2
Thread-1,2
Thread-1,3
Thread-0,3
Thread-2,3
Thread-3,3
。。。。

每個線程的結果都是從1開始增長。

總結

ThreadLocal的作用是給每個線程提供局部變量,而這個局部變量就是存儲到工作內存中的。線程之間的局部變量互不影響,達到線程安全的目的。ThreadLocal的應用相當廣泛,如SpringCloud在網關中獲取當前的request,就是使用的ThreadLocal

部分代碼如下:

public class RequestContext extends ConcurrentHashMap<String, Object> {

    // ThreadLocal存儲RequestContext
    protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
        protected RequestContext initialValue() {
            try {
                return (RequestContext)RequestContext.contextClass.newInstance();
            } catch (Throwable var2) {
                throw new RuntimeException(var2);
            }
        }
    };

    // 獲取Request上下文
    public static RequestContext getCurrentContext() {
        if (testContext != null) {
            return testContext;
        } else {
            RequestContext context = (RequestContext)threadLocal.get();
            return context;
        }
    }

    // 獲取當前線程的request
    public HttpServletRequest getRequest() {
        return (HttpServletRequest)this.get("request");
    }

}

SpringCloud的源碼我還沒開始看(這玩意源碼太多了估計啃不動),現在在啃Mybatis源碼,因此下面對此的分析只是推測,還希望大佬們不要打我。

Zuul在請求進入後,首先會獲取到request,並將其存儲在RequestContext中,使用threadLocal存儲,可以保證每個線程獲取到的request都是屬於自己的。後續在程序的任意處,都可以使用 RequestContext.getCurrentContext().getRequest() 來獲取當前請求的request對象。

錯誤使用

在web應用中,經常會有人把ThreadLocal作爲每個線程的全局變量使用,這種用法是錯誤的。SpringBoot底層有線程池,對於每一個請求,都會從線程池中隨機取出一個線程,因此即使是同一個登錄的用戶,每一次請求都有可能不是同一個線程,而從ThreadLocal中獲取到的值自然也不一樣。關於每次請求都不是同一線程的問題,可以自行打印請求的線程id進行證明,這裏就不貼代碼了。

ThreadLocal在web應用中的使用場景爲,爲每次請求提供一個全局的值,在這一次請求中,可以在任何地方取出來這個值進行操作。如:在aop中解析token獲取登錄中的用戶信息,存放到ThreadLocal,本次請求需要用到登錄用戶的信息,就可以取出來。再如:開發者在aop中記錄日誌,代碼全部寫到環繞通知中就顯得冗餘,因此獲取ip、參數等內容會寫到前置通知中。而對於要存表的日誌,參數在前置通知,返回值在後置通知,報錯信息在環繞通知中,可能會想到把變量定義到最上面,這種寫法也是錯誤的。在上面說過,aop是單例模式,因此這種寫法存在線程安全性問題,在這裏就也可以使用ThreadLocal存儲日誌信息,最後在後置通知中存表。

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