這個老大難問題,我使用 ThreadLocal 一次解決

1.ThreadLocal的使用場景

1.1 場景1
每個線程需要一個獨享對象(通常是工具類,典型需要使用的類有SimpleDateFormat和Random)

每個Thread內有自己的實例副本,不共享

比喻:教材只有一本,一起做筆記有線程安全問題。複印後沒有問題,使用ThradLocal相當於複印了教材。

1.2 場景2
每個線程內需要保存全局變量(例如在攔截器中獲取用戶信息),可以讓不同方法直接使用,避免參數傳遞的麻煩

2.對以上場景的實踐

2.1 實踐場景1

/**
 * 兩個線程打印日期
 */
public class ThreadLocalNormalUsage00 {

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalNormalUsage00().date(10);
                System.out.println(date);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalNormalUsage00().date(104707);
                System.out.println(date);
            }
        }).start();

    }

    public String date(int seconds) {

        //參數的單位是毫秒,從1970.1.1 00:00:00 GMT 開始計時
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);
    }
}

運行結果

在這裏插入圖片描述

因爲中國位於東八區,所以時間從1970年1月1日的8點開始計算的

/**
 * 三十個線程打印日期
 */
public class ThreadLocalNormalUsage01 {

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 30; i++) {
            int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage01().date(finalI);
                    System.out.println(date);
                }
            }).start();
            //線程啓動後,休眠100ms
            Thread.sleep(100);
        }
    }

    public String date(int seconds) {

        //參數的單位是毫秒,從1970.1.1 00:00:00 GMT 開始計時
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);
    }
}

運行結果

在這裏插入圖片描述

多個線程打印自己的時間(如果線程超級多就會產生性能問題),所以要使用線程池。

/**
 * 1000個線程打印日期,用線程池來執行
 */
public class ThreadLocalNormalUsage02 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            //提交任務
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage02().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {

        //參數的單位是毫秒,從1970.1.1 00:00:00 GMT 開始計時
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);
    }
}

運行結果

在這裏插入圖片描述

但是使用線程池時就會發現每個線程都有一個自己的SimpleDateFormat對象,沒有必要,所以將SimpleDateFormat聲明爲靜態,保證只有一個

/**
 * 1000個線程打印日期,用線程池來執行,出現線程安全問題
 */
public class ThreadLocalNormalUsage03 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    //只創建一次 SimpleDateFormat 對象,避免不必要的資源消耗
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            //提交任務
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage03().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {

        //參數的單位是毫秒,從1970.1.1 00:00:00 GMT 開始計時
        Date date = new Date(1000 * seconds);
        return dateFormat.format(date);
    }
}

運行結果

出現了秒數相同的打印結果,這顯然是不正確的。

在這裏插入圖片描述

出現問題的原因

在這裏插入圖片描述

多個線程的task指向了同一個SimpleDateFormat對象,SimpleDateFormat是非線程安全的。

解決問題的方案

方案1:加鎖

格式化代碼是在最後一句return dateFormat.format(date);,所以可以爲最後一句代碼添加synchronized鎖

public String date(int seconds) {

    //參數的單位是毫秒,從1970.1.1 00:00:00 GMT 開始計時
    Date date = new Date(1000 * seconds);
    String s;
    synchronized (ThreadLocalNormalUsage04.class) {
        s = dateFormat.format(date);
    }
    return s;
}

運行結果
在這裏插入圖片描述

運行結果中沒有發現相同的時間,達到了線程安全的目的

缺點:因爲添加了synchronized,所以會保證同一時間只有一條線程可以執行,這在高併發場景下肯定不是一個好的選擇,所以看看其他方案吧。

方案2:使用ThreadLocal

/**
 * 利用 ThreadLocal 給每個線程分配自己的 dateFormat 對象
 * 不但保證了線程安全,還高效的利用了內存
 */
public class ThreadLocalNormalUsage05 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            //提交任務
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage05().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {

        //參數的單位是毫秒,從1970.1.1 00:00:00 GMT 開始計時
        Date date = new Date(1000 * seconds);
        //獲取 SimpleDateFormat 對象
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }
}

class ThreadSafeFormatter {

    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new
            ThreadLocal<SimpleDateFormat>(){

        //創建一份 SimpleDateFormat 對象
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };
}

運行結果
在這裏插入圖片描述

使用了ThreadLocal後不同的線程不會有共享的 SimpleDateFormat 對象,所以也就不會有線程安全問題
2.2 實踐場景2
當前用戶信息需要被線程內的所有方法共享

方案1:傳遞參數

在這裏插入圖片描述

可以將user作爲參數在每個方法中進行傳遞,

缺點:但是這樣做會產生代碼冗餘問題,並且可維護性差。

方案2:使用Map

對此進行改進的方案是使用一個Map,在第一個方法中存儲信息,後續需要使用直接get()即可,

在這裏插入圖片描述

缺點:如果在單線程環境下可以保證安全,但是在多線程環境下是不可以的。如果使用加鎖和ConcurrentHashMap都會產生性能問題。

方案3:使用ThreadLocal,實現不同方法間的資源共享

使用 ThreadLocal 可以避免加鎖產生的性能問題,也可以避免層層傳遞參數來實現業務需求,就可以實現不同線程中存儲不同信息的要求。

在這裏插入圖片描述

/**
 * 演示 ThreadLocal 的用法2:避免參數傳遞的麻煩
 */
public class ThreadLocalNormalUsage06 {
    public static void main(String[] args) {
        new Service1().process();
    }
}

class Service1 {

    public void process() {
        User user = new User("魯毅");
        //將User對象存儲到 holder 中
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2拿到用戶名: " + user.name);
        new Service3().process();
    }
}

class Service3 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用戶名: " + user.name);
    }
}


class UserContextHolder {

    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {

    String name;

    public User(String name) {
        this.name = name;
    }
}

運行結果
在這裏插入圖片描述
3.對ThreadLocal的總結

  • 讓某個需要用到的對象實現線程之間的隔離(每個線程都有自己獨立的對象)

  • 可以在任何方法中輕鬆的獲取到該對象

  • 根據共享對象生成的時機選擇使用initialValue方法還是set方法

  • 對象初始化的時機由我們控制的時候使用initialValue 方式

  • 如果對象生成的時機不由我們控制的時候使用 set 方式

4.使用ThreadLocal的好處

  • 達到線程安全的目的

  • 不需要加鎖,執行效率高

  • 更加節省內存,節省開銷

  • 免去傳參的繁瑣,降低代碼耦合度

5.ThreadLocal原理

在這裏插入圖片描述

  • Thread

  • ThreadLocal

  • ThreadLocalMap

在Thread類內部有有ThreadLocal.ThreadLocalMap threadLocals = null;這個變量,它用於存儲ThreadLocal,因爲在同一個線程當中可以有多個ThreadLocal,並且多次調用get()所以需要在內部維護一個ThreadLocalMap用來存儲多個ThreadLocal

5.1 ThreadLocal相關方法
T initialValue()

該方法用於設置初始值,並且在調用get()方法時纔會被觸發,所以是懶加載。

但是如果在get()之前進行了set()操作,這樣就不會調用initialValue()。

通常每個線程只能調用一次本方法,但是調用了remove()後就能再次調用

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);

    //獲取到了值直接返回resule
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //沒有獲取到纔會進行初始化
    return setInitialValue();
}

private T setInitialValue() {
    //獲取initialValue生成的值,並在後續操作中進行set,最後將值返回
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

void set(T t)

爲這個線程設置一個新值

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

T get()

獲取線程對應的value

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

void remove()

刪除對應這個線程的值

6.ThreadLocal注意點

6.1 內存泄漏
內存泄露;某個對象不會再被使用,但是該對象的內存卻無法被收回

在這裏插入圖片描述

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            //調用父類,父類是一個弱引用
            super(k);
            //強引用
            value = v;
        }
    }

強引用:當內存不足時觸發GC,寧願拋出OOM也不會回收強引用的內存

弱引用:觸發GC後便會回收弱引用的內存
正常情況

當Thread運行結束後,ThreadLocal中的value會被回收,因爲沒有任何強引用了

非正常情況

當Thread一直在運行始終不結束,強引用就不會被回收,存在以下調用鏈 Thread-->ThreadLocalMap-->Entry(key爲null)-->value因爲調用鏈中的 value 和 Thread 存在強引用,所以value無法被回收,就有可能出現OOM。

JDK的設計已經考慮到了這個問題,所以在set()、remove()、resize()方法中會掃描到key爲null的Entry,並且把對應的value設置爲null,這樣value對象就可以被回收。

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            //當ThreadLocal爲空時,將ThreadLocal對應的value也設置爲null
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

但是只有在調用set()、remove()、resize()這些方法時纔會進行這些操作,如果沒有調用這些方法並且線程不停止,那麼調用鏈就會一直存在,所以可能會發生內存泄漏。

6.2 如何避免內存泄漏(阿里規約)
調用remove()方法,就會刪除對應的Entry對象,可以避免內存泄漏,所以使用完ThreadLocal後,要調用remove()方法。

class Service1 {

    public void process() {
        User user = new User("魯毅");
        //將User對象存儲到 holder 中
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2拿到用戶名: " + user.name);
        new Service3().process();
    }
}

class Service3 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用戶名: " + user.name);
        //手動釋放內存,從而避免內存泄漏
        UserContextHolder.holder.remove();
    }
}

6.3 ThreadLocal的空指針異常問題

/**
 * ThreadLocal的空指針異常問題
 */
public class ThreadLocalNPE {

    ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();

    public void set() {
        longThreadLocal.set(Thread.currentThread().getId());
    }

    public Long get() {
        return longThreadLocal.get();
    }

    public static void main(String[] args) {

        ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();

        //如果get方法返回值爲基本類型,則會報空指針異常,如果是包裝類型就不會出錯
        System.out.println(threadLocalNPE.get());

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocalNPE.set();
                System.out.println(threadLocalNPE.get());
            }
        });
        thread1.start();
    }
}

6.4 空指針異常問題的解決

如果get方法返回值爲基本類型,則會報空指針異常,如果是包裝類型就不會出錯。這是因爲基本類型和包裝類型存在裝箱和拆箱的關係,造成空指針問題的原因在於使用者。

6.5 共享對象問題
如果在每個線程中ThreadLocal.set()進去的東西本來就是多個線程共享的同一對象,比如static對象,那麼多個線程調用ThreadLocal.get()獲取的內容還是同一個對象,還是會發生線程安全問題。

6.6 可以不使用ThreadLocal就不要強行使用
如果在任務數很少的時候,在局部方法中創建對象就可以解決問題,這樣就不需要使用ThreadLocal。

6.7 優先使用框架的支持,而不是自己創造
例如在Spring框架中,如果可以使用RequestContextHolder,那麼就不需要自己維護ThreadLocal,因爲自己可能會忘記調用remove()方法等,造成內存泄漏。

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