天天看點

Java ThreadLocal 有記憶體洩漏的風險怎麼搞?分析下原理吧

ThreadLocal

    • 通過閱讀本遍你将擷取的知識
    • ThreadLocal
    • ThreadLocal實作
    • ThreadLocalMap
    • ThreadLocalMap記憶體洩漏

通過閱讀本遍你将擷取的知識

  1. ThreadLocal 使用方法
  2. ThreadLocal 适合使用的場景
  3. ThreadLocal實作方法與原理
  4. ThreadLocalMap實作方法與原理
  5. Thread如何存儲ThreadLocalMap
  6. ThreadLocalMap記憶體洩漏原因與避坑方法

ThreadLocal被解釋為線程本地變量,生命周期與和它綁定的線程相同,在新建立一個線程時會初始化一個ThreadLocal執行個體,線程銷毀時與會被一同銷毀,線上程内部隻能通路到本線程自己的ThreadLocal執行個體變量。

下面通過一個例子來了解ThreadLocal

public static  class ConnectionManger{
  private static Connection connection;

  public static Connection openConnection() throws SQLException {
    if(connection==null){
      connection=DriverManager.getConnection("");
    }
    return connection;
  }

  public static void close() throws SQLException {
    if(connection!=null){
      connection.close();
      connection=null;
    }
  }
}
public void demo() throws SQLException {
  Connection connection = ConnectionManger.openConnection();
  connection.close();
}
           

ConnectionManger 類為一個管理資料庫連接配接的類,在内部有一個共享變量

connection

單線程的應用環境下調用demo方法不會出現什麼問題,運作也正常。

多線程環境下就不好使了,調用demo方法的結果将會不可控,可能什麼出現多個線程使用現一個

Connection

對象,一個關閉連接配接後其它的出現異常。

解決多線程中的問題可以在ConnectionManager的方法中添加同步塊,通過同步塊保證多線程環境下的線程安全,但ConnectionManager将會成為多線程環境下通路資料庫能力上的瓶頸,所有的資料讀寫都将串行化執行。

解決性能瓶頸的辦法是将

ConnectionManager

也多線程化,将它做為一個連接配接存儲池(資料庫連接配接池),通過動态配置池的大小來控制資料讀寫能力。那如何解決業務上多線程使用

Connection

執行個體的問題呢?

ThreadLocal

主角登場,通過上面問題的分析,每一個業務線程在需要使用資料庫連接配接時,通過調用**ConnectionManger.openConnection()方法擷取一個連接配接,在業務線程中通過connection執行個體完成業務操作,再通過ConnectionManger.close()**關閉連接配接;業務之前是互相隔離的,每一個業務配置設定一個

connection

就可以解決串行化的問題。

每個線程都有一個ThreadLocal執行個體,将共享的

connection

執行個體存儲在ThreadLocal,線上程中執行的業務隻需要擷取ThreadLocal中的connection執行個體完成資料讀寫即可,下面看下優化後的ConnectionManager對象:

public void demo() throws SQLException {
  Connection connection = ConnectionManger.openConnection();
  connection.close();
}
public static class ConnectionManger {
  static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
  //模拟連接配接池擷取連接配接
  private static Connection openNewConnection() {
    try {
      return DriverManager.getConnection("");
    } catch (SQLException ignore) {
    }
    return null;
  }
  public static Connection openConnection() throws SQLException {
    threadLocal.set(ConnectionManger.openNewConnection());
    return threadLocal.get();
  }
  public static void close() throws SQLException {
    threadLocal.get().close();
    threadLocal.remove();
  }
}
           

在ConnectionManager對象中引用

ThreadLocal<Connection>

變量類型存儲目前線程中使用的Connection執行個體。

通過優化解決了多線程中線程對資源競争的問題,通過添加同步塊導緻的性能問題。

你好奇ThreadLocal怎麼實作的嗎?我好奇

ThreadLocal實作

ThreadLocal執行個體供外部使用的三個方法分别是:

ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
threadLocal.get();//擷取值
threadLocal.set(<Object>);//設定值
threadLocal.remove();//移除值
           

get()

用于擷取存儲在目前線程中值,

set()

用于将值存儲在目前線程中,

remove()

用于移除目前線程中的值。

下面來簡單過下各個方法是怎麼實作的:

public T get() {
  //擷取目前線程
  Thread t = Thread.currentThread();
  //擷取線程中的ThreadLocalMap
  ThreadLocalMap map = getMap(t);
  if (map != null) {
    //擷取Map中的值
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
      @SuppressWarnings("unchecked")
      T result = (T)e.value;
      return result;
    }
  }
  //沒有map時進行初始化
  return setInitialValue();
}
public void set(T value) {
  //擷取目前線程
  Thread t = Thread.currentThread();
  //擷取線程中的ThreadLocalMap
  ThreadLocalMap map = getMap(t);
  if (map != null) {
    //将值存儲在Map
    map.set(this, value);
  } else {
    //新建立一個Map
    createMap(t, value);
  }
}
public void remove() {
  //擷取目前線程的Map
  ThreadLocalMap m = getMap(Thread.currentThread());
  if (m != null) {
    //将值從Map中移除
    m.remove(this);
  }
}
//擷取線程的Map
ThreadLocalMap getMap(Thread t) {
  return t.threadLocals;
}
           

從上面的源碼中可以得到幾個關鍵知識點:

  1. Thread使用ThreadLocalMap管理引用的值,ThreadLocalMap是ThreadLocal内部類
  2. ThreadLocalMap執行個體存儲線上程的

    threadLocals

    變量中
  3. ThreadLocal的

    set

    get

    remove

    執行個體操作的是目前線程中存儲在threadLocals變量中的值
  4. Thread中的threadLocals可能為null,為null時

    get

    set

    時會進行初始化

ThreadLocalMap

要完全搞清楚ThreadLocal的底層原理,ThreadLocalMap的實作肯定是要搞清楚的。

在内部和Map的思路比較像,但存儲池改為了數組,Entry對象繼承自WeakReference弱引用類型,和Map類型相似點在于有擴容操作,ThreadLocalMap不繼承于Map類型,隻在思想上相同。

可能有小夥伴要問了,為什麼不直接用Map類型來存儲呢,在存儲資料量比較多時查找性能優于數組;這裡解釋下沒有用Map類型的原因于一般每個線程中存儲在ThreadLocalMap中的資料不會特别多,在這樣的情況下使用數組的性能就要優于Map類型了,查找時不用計算Hash值,資料量大于擴容也比較簡單。

ThreadLocalMap記憶體洩漏

ThreadLocalMap使用内部Entry做為存儲資料的結構,先來看下實作代碼:

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

  Entry(ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
  }
}
           

在Entry内部将ThreadLocal執行個體做為Key,将值做為value變量,ThreadLocalMap的存儲池中将直接存儲Entry執行個體,從Entry類的結構上可以看出繼承了

WeakReference

類型,在建構方法中對TheadLocal執行個體的引用值調用

super(k)

,這樣做的結果是Entry類型中的key對ThreadLocal的值引用為弱引用類型;

在JVM的GC回收中,如果一個值隻是被弱引用了,那麼将在GC回收時對這個引用進行回收。

這又怎樣能導緻記憶體洩漏呢?

根據弱引用的特性,一個值隻被弱引用時會被GC回收,那一起來分析下下面的示例代碼:

public static class ConnectionManger {
  private static Connection openNewConnection() {
    try {
      return DriverManager.getConnection("");
    } catch (SQLException ignore) {
    }
    return null;
  }
  public static Connection openConnection() throws SQLException {
    ThreadLocal<Connection> local = ThreadLocal.withInitial(ConnectionManger::openNewConnection);
    return local.get();
  }
}
           

設上面的代碼運作在多線程環境,且線程使用線程池管理,線程池沒有停步且在任務隊列中還有等待執行的任務。

openConnection

中使用

ThreadLocal.withInitial

建立了一個ThreadLocal執行個體,随後使用

get

方法傳回值。

在這個示例的

openConnection

方法中沒有對ThreadLocal執行個體做強引用,在方法執行完成後局部變量将會被回收,随後也沒有調用

ThreadLocal

的執行個體方法

remove

将引用的值移除掉。

現在我們來分析下記憶體中JVM進行GC後的情況:

  1. 使用

    ThreadLocal.withInitial

    建立TheadLocal執行個體後再使用

    get

    方法擷取時會将初始值存入線程對應的ThreadLocalMap中,在TheadLocalMap中會使用Entry類型将ThreadLocal類型值做為key,将Connection類型值做為value
  2. JVM開始執行GC對記憶體進行回收,回收時發現Key是弱引用,在其它位址沒有再引用(沒有強引用),JVM對Key的值進行回收

在JVM執行完GC後key被成功回收,但value引用的值并沒有被回收,這時ThreadLocalMap中的Entry的Key将變為null,而value則沒有變,因為沒有執行

remove

操作,這條記錄将會一直存在于ThreadLocalMap中直到線程被銷毀後導緻線程引用的ThreadLocalMap執行個體被回收才會将洩漏的記憶體進行回收。

精彩推薦:

包郵贈書!《阿裡巴巴Java開發手冊 第二版》

知識點:Java sychronized 内部鎖實作原理

知識點: Java FutureTask 使用詳解

Java ClassLoader詳解雙親委派的實作原理