ThreadLocal從入門到放棄

1、ThreadLocal

1.1 定義

用來提供線程內部的局部變量,這種變量在多線程環境下訪問(通過get或者set方法訪問)時能保證各個線程的變量對應相對獨立於其他線程內的變量。

作用是:提供了線程內部的局部變量,不同的線程之間不會相互干擾,這種變量在線程的生命週期內起作用,減少同一個線程內多個函數或組件之間一些公共變量傳遞的複雜度。

1.2 示例

需求:線程隔離

在多線程併發的場景下,每個線程中變量都是相互隔離的

  • 線程A:設置變量1,獲取變量1
  • 線程B:設置變量2,獲取變量2

1.2.1 錯誤實現

public class ThreadLockDemo {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) {

        ThreadLockDemo demo = new ThreadLockDemo();

        for (int i = 1; i <= 10; i++) {

            new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.setName(Thread.currentThread().getName() + "的數據");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "--->" + demo.getName());
                }
            }, "線程" + i).start();
        }
    }
}

運行結果

線程4--->線程10的數據
線程3--->線程10的數據
線程8--->線程10的數據
線程10--->線程10的數據
線程2--->線程10的數據
線程9--->線程10的數據
線程7--->線程10的數據
線程5--->線程10的數據
線程6--->線程10的數據
線程1--->線程10的數據

1.2.2 使用 ThreadLocal 解決

ThreadLocal
1、set() 將變量綁定到當前線程中
2、get() 獲取當前線程綁定的變量

public class ThreadLockDemo {

    ThreadLocal<String> threadLocal = new ThreadLocal<>();

    private String name;

//    public String getName() {
//        return name;
//    }
//
//    public void setName(String name) {
//        this.name = name;
//    }

    public String getName() {
        return threadLocal.get();
    }

    public void setName(String name) {
        threadLocal.set(name);
    }

    public static void main(String[] args) {

        ThreadLockDemo demo = new ThreadLockDemo();

        for (int i = 1; i <= 10; i++) {

            new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.setName(Thread.currentThread().getName() + "的數據");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "--->" + demo.getName());
                }
            }, "線程" + i).start();
        }
    }
}

運行結果

線程8--->線程8的數據
線程1--->線程1的數據
線程5--->線程5的數據
線程6--->線程6的數據
線程7--->線程7的數據
線程10--->線程10的數據
線程3--->線程3的數據
線程9--->線程9的數據
線程2--->線程2的數據
線程4--->線程4的數據

1.3 使用 synchronize 來處理

1.3.1 使用synchronize 加鎖來實現

使用 synchronize 加鎖也能解決這個問題

public class ThreadLockDemo {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) {

        ThreadLockDemo demo = new ThreadLockDemo();

        for (int i = 1; i <= 10; i++) {

            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (ThreadLockDemo.class){
                        demo.setName(Thread.currentThread().getName() + "的數據");
                        try {
                            TimeUnit.SECONDS.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "--->" + demo.getName());
                    }
                }
            }, "線程" + i).start();
        }
    }
}

運行也沒有問題,但是加鎖後,各個線程需要排隊進入運行,性能會降低,造成該操作不是併發運行。

1.3.2 ThreadLocal 與 synchronize 的區別

​ 都用於處理多線程併發訪問變量的問題,不過兩者處理問題的角度和思路不同。

synchronize ThreadLocal
原理 同步機制採用 以時間換空間的方式,只提供了一份變量,讓不容的線程排隊訪問 ThreadLocal採用以空間換時間的方式,爲每一個線程都提供了一份變量的副本,從而實現同時訪問而相不干擾
側重點 多個線程之間訪問資源的同步 多線程中讓每個線程之間的數據互相隔離

2 在數據庫連接案例中使用到了 ThreadLocal

  • 傳遞數據:保存每個線程綁定的數據,在需要的地方直接獲取,避免參數直接傳遞帶來的代碼耦合問題
  • 線程隔離:各線程之間的數據相互隔離卻又具備併發性,避免同步方式帶來的性能損失

3 ThreadLocal 的內部結構

JDK8 中 ThreadLocal 的設計是:

  • 每一個Thread線程內部都有一個Map(ThreadLocalMap)
  • Map 裏邊存儲 ThreadLocal 對象(key)和線程的變量副本(value)
  • Thread 內部的Map是由ThreadLocal維護的,由ThreadLocal負責向 map 獲取和設置線程的變量值
  • 對於不同的線程,每次獲取副本值時,別的線程並不能獲取到當前線程的副本值,形成了副本的隔離,互不干擾
public void set(T value) {
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null)
  	map.set(this, value);
  else
 		createMap(t, 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();
}


/**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
private T setInitialValue() {
  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);
}

4 爲什麼要使用弱引用

無論 ThreadLocalMap 中的 key 使用哪種類型引用都無法完全避免出現內存泄漏。

要避免內存泄漏有兩種方式:

  • 使用完 ThreadLocal ,調用其 remove 方法刪除對應的 Entry
  • 使用完 ThreadLocal,當前Thread也隨之運行結束

第二種方式是不好控制,特別是使用線程池的時候,線程結束不會銷燬的。

也就說,在使用完後,記得調用 remove,無論是強引用還是弱引用都不會有問題,那爲什麼要使用弱引用呢?

在 ThreadLocalMap 中的 set/getEntry 的方法中,會對 key 爲 null(即 ThreadLocal 爲 null)進行判斷,如果爲 null 的話,那麼會對 value 設置爲 null。

即在使用完 ThreadLocal ,CurrentThread 依然運行的情況下,就算忘記調用 remove 方法,弱引用比強引用多一層保障:弱引用的 ThreadLocal 會被回收,對應的 value 在下一次 ThreadLocalMap 調用 set/get/remove中的任意方法時都會被清除,從而避免內存泄漏。

總結:ThreadLocal內存泄漏的根源是:由於ThreadLocalMap 的生命週期跟 Thread 一樣長,如果沒有手動刪除掉對應的 key 就會導致內存泄漏。

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