真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網站制作重慶分公司

Java并發(fā)編程學習之ThreadLocal源碼詳析

前言

為柯城等地區(qū)用戶提供了全套網頁設計制作服務,及柯城網站建設行業(yè)解決方案。主營業(yè)務為網站制作、成都做網站、柯城網站設計,以傳統(tǒng)方式定制建設網站,并提供域名空間備案等一條龍服務,秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務。我們深信只要達到每一位用戶的要求,就會得到認可,從而選擇與我們長期合作。這樣,我們也可以走得更遠!

多線程的線程安全問題是微妙而且出乎意料的,因為在沒有進行適當同步的情況下多線程中各個操作的順序是不可預期的,多線程訪問同一個共享變量特別容易出現(xiàn)并發(fā)問題,特別是多個線程需要對一個共享變量進行寫入時候,為了保證線程安全,

一般需要使用者在訪問共享變量的時候進行適當?shù)耐?,如下圖所示:

Java并發(fā)編程學習之ThreadLocal源碼詳析 

可以看到同步的措施一般是加鎖,這就需要使用者對鎖也要有一定了解,這顯然加重了使用者的負擔。那么有沒有一種方式當創(chuàng)建一個變量的時候,每個線程對其進行訪問的時候訪問的是自己線程的變量呢?其實ThreaLocal就可以做這個事情,注意一下,ThreadLocal的出現(xiàn)并不是為了解決上面的問題而出現(xiàn)的。

ThreadLocal是在JDK包里面提供的,它提供了線程本地變量,也就是如果你創(chuàng)建了一個ThreadLocal變量,那么訪問這個變量的每個線程都會有這個變量的本地拷貝,多個線程操作這個變量的時候,實際是操作自己本地內存里面的變量,從而避免了線程安全問題,創(chuàng)建一個ThreadLocal變量后,

每個線程會拷貝一個變量到自己的本地內存,如下圖:

Java并發(fā)編程學習之ThreadLocal源碼詳析

好了,現(xiàn)在我們思考一個問題:ThreadLocal的實現(xiàn)原理,ThreadLocal作為變量的線程隔離方式,其內部又是如何實現(xiàn)的呢?

首先我們要看ThreadLocal的類圖結構,如下圖所示:

Java并發(fā)編程學習之ThreadLocal源碼詳析 如

上類圖可見,Thread類中有一個threadLocals和inheritableThreadLocals 都是ThreadLocalMap類型的變量,而ThreadLocalMap是一個定制化的Hashmap,默認每個線程中這兩個變量都為null,只有當線程第一次調用了ThreadLocal的set或者get方法的時候才會創(chuàng)建。

其實每個線程的本地變量不是存到ThreadLocal實例里面的,而是存放到調用線程的threadLocals變量里面。也就是說ThreadLocal類型的本地變量是存放到具體線程內存空間的。 

ThreadLocal其實就是一個外殼,它通過set方法把value值放入調用線程threadLocals里面存放起來,當調用線程調用它的get方法的時候再從當前線程的threadLocals變量里面拿出來使用。如果調用線程如果一直不終止的話,那么這個本地變量會一直存放到調用線程的threadLocals變量里面,

因此,當不需要使用本地變量時候可以通過調用ThreadLocal變量的remove方法,從當前線程的threadLocals變量里面刪除該本地變量??赡苓€有人會問threadLocals為什么設計為Map結構呢?很明顯是因為每個線程里面可以關聯(lián)多個ThreadLocal變量。

接下來我們可以進入到ThreadLocal中的源碼如看看,如下代碼所示:

主要看set,get,remove這三個方法的實現(xiàn)邏輯,如下:

先看set(T var1)方法

public void set(T var1) {     //(1)獲取當前線程
 Thread var2 = Thread.currentThread();     //(2) 當前線程作為key,去查找對應的線程變量,找到則設置
 ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
 if(var3 != null) {
 var3.set(this, var1);
 } else {       //(3) 第一次調用則創(chuàng)建當前線程對應的Hashmap
 this.createMap(var2, var1);
 }
 }

如上代碼(1)首先獲取調用線程,然后使用當前線程作為參數(shù)調用了 getMap(var2) 方法,getMap(Thread var2) 代碼如下:

   ThreadLocal.ThreadLocalMap getMap(Thread var1) {
 return var1.threadLocals;
 }

可知getMap(var2) 所作的就是獲取線程自己的變量threadLocals,threadlocal變量是綁定到了線程的成員變量里面。

如果getMap(var2) 返回不為空,則把 value 值設置進入到 threadLocals,也就是把當前變量值放入了當前線程的內存變量 threadLocals,threadLocals 是個 HashMap 結構,其中 key 就是當前 ThreadLocal 的實例對象引用,value 是通過 set 方法傳遞的值。

如果 getMap(var2) 返回空那說明是第一次調用 set 方法,則創(chuàng)建當前線程的 threadLocals 變量,下面看 createMap(var2, var1) 里面做了啥呢?

 void createMap(Thread var1, T var2) {
 var1.threadLocals = new ThreadLocal.ThreadLocalMap(this, var2);
 }

可以看到的就是創(chuàng)建當前線程的threadLocals變量。

接下來我們再看get()方法,代碼如下:

public T get() {    //(4)獲取當前線程
 Thread var1 = Thread.currentThread();    //(5)獲取當前線程的threadLocals變量
 ThreadLocal.ThreadLocalMap var2 = this.getMap(var1);    //(6)如果threadLocals不為null,則返回對應本地變量值
 if(var2 != null) {
 ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
 if(var3 != null) {
 Object var4 = var3.value;
 return var4;
 }
 }
    //(7)threadLocals為空則初始化當前線程的threadLocals成員變量。
 return this.setInitialValue();
 }

代碼(4)首先獲取當前線程實例,如果當前線程的threadLocals變量不為null則直接返回當前線程的本地變量。否則執(zhí)行代碼(7)進行初始化,setInitialValue()的代碼如下:

private T setInitialValue() {    //(8)初始化為null
 Object var1 = this.initialValue();
 Thread var2 = Thread.currentThread();
 ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);    //(9)如果當前線程變量的threadLocals變量不為空
 if(var3 != null) {
 var3.set(this, var1);    //(10)如果當前線程的threadLocals變量為空
 } else {
 this.createMap(var2, var1);
 }

 return var1;
 }

如上代碼如果當前線程的 threadLocals 變量不為空,則設置當前線程的本地變量值為 null,否者調用 createMap 創(chuàng)建當前線程的 createMap 變量。

接著我們在看看void remove()方法,代碼如下:

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

如上代碼,如果當前線程的 threadLocals 變量不為空,則刪除當前線程中指定 ThreadLocal 實例的本地變量。

接下來我們看看具體演示demo,代碼如下:

/**
 * Created by cong on 2018/6/3.
 */
public class ThreadLocalTest {
 //(1)打印函數(shù)
 static void print(String str) {
 //1.1 打印當前線程本地內存中l(wèi)ocalVariable變量的值
 System.out.println(str + ":" + localVariable.get());
 //1.2 清除當前線程本地內存中l(wèi)ocalVariable變量
 //localVariable.remove();
 }

 //(2) 創(chuàng)建ThreadLocal變量
 static ThreadLocal localVariable = new ThreadLocal<>();
 public static void main(String[] args) {

 //(3) 創(chuàng)建線程one
 Thread threadOne = new Thread(new Runnable() {
 public void run() {
 //3.1 設置線程one中本地變量localVariable的值
 localVariable.set("線程1的本地變量");
 //3.2 調用打印函數(shù)
 print("線程1----->");
 //3.3打印本地變量值
 System.out.println("移除線程1本地變量后的結果" + ":" + localVariable.get());

 }
 });
 //(4) 創(chuàng)建線程two
 Thread threadTwo = new Thread(new Runnable() {
 public void run() {
 //4.1 設置線程one中本地變量localVariable的值
 localVariable.set("線程2的本地變量");
 //4.2 調用打印函數(shù)
 print("線程2----->");
 //4.3打印本地變量值
 System.out.println("移除線程2本地變量后的結果" + ":" + localVariable.get());

 }
 });
 //(5)啟動線程
 threadOne.start();
 threadTwo.start();
 }
}

代碼(2)創(chuàng)建了一個 ThreadLocal 變量;

代碼(3)、(4)分別創(chuàng)建了線程 1和 2;

代碼(5)啟動了兩個線程;

線程 1 中代碼 3.1 通過 set 方法設置了 localVariable 的值,這個設置的其實是線程 1 本地內存中的一個拷貝,這個拷貝線程 2 是訪問不了的。然后代碼 3.2 調用了 print 函數(shù),代碼 1.1 通過 get 函數(shù)獲取了當前線程(線程 1)本地內存中 localVariable 的值;

線程 2 執(zhí)行類似線程 1。 

運行結果如下:

Java并發(fā)編程學習之ThreadLocal源碼詳析 

這里要注意一下ThreadLocal的內存泄漏問題

每個線程內部都有一個名字為 threadLocals 的成員變量,該變量類型為 HashMap,其中 key 為我們定義的 ThreadLocal 變量的 this 引用,value 則為我們 set 時候的值,每個線程的本地變量是存到線程自己的內存變量 threadLocals 里面的,如果當前線程一直不消失那么這些本地變量會一直存到,

所以可能會造成內存泄露,所以使用完畢后要記得調用 ThreadLocal 的 remove 方法刪除對應線程的 threadLocals 中的本地變量。

解開代碼1.2的注釋后,再次運行,運行結果如下:

Java并發(fā)編程學習之ThreadLocal源碼詳析 

我們有沒有想過這樣的一個問題:子線程中是否獲取到父線程中設置的 ThreadLocal 變量的值呢?

這里可以告訴大家,在子線程中是獲取不到父線程中設置的 ThreadLocal 變量的值的。那么有辦法讓子線程訪問到父線程中的值嗎?為了解決該問題 InheritableThreadLocal 應運而生,InheritableThreadLocal 繼承自 ThreadLocal,提供了一個特性,就是子線程可以訪問到父線程中設置的本地變量。

首先我們先進入InheritableThreadLocal這個類的源碼去看,如下:

public class InheritableThreadLocal extends ThreadLocal {
 public InheritableThreadLocal() {
 }
  //(1)
 protected T childValue(T var1) {
 return var1;
 }
  //(2)
 ThreadLocalMap getMap(Thread var1) {
 return var1.inheritableThreadLocals;
 }
  //(3)
 void createMap(Thread var1, T var2) {
 var1.inheritableThreadLocals = new ThreadLocalMap(this, var2);
 }
}

 可以看到InheritableThreadlocal繼承ThreadLocal,并重寫了三個方法,在上面的代碼已經標出了。代碼(3)可知InheritableThreadLocal重寫createMap方法,那么可以知道現(xiàn)在當?shù)谝淮握{用set方法時候創(chuàng)建的是當前線程的inhertableThreadLocals變量的實例,而不再是threadLocals。

代碼(2)可以知道當調用get方法獲取當前線程的內部map變量時候,獲取的是inheritableThreadLocals,而不再是threadLocals。

關鍵地方來了,重寫的代碼(1)是何時被執(zhí)行的,以及如何實現(xiàn)子線程可以訪問父線程本地變量的。這個要從Thread創(chuàng)建的代碼看起,Thread的默認構造函數(shù)以及Thread.java類的構造函數(shù)如下:

/**
 * Created by cong on 2018/6/3.
 */
  public Thread(Runnable target) {
  init(null, target, "Thread-" + nextThreadNum(), 0);
  }
  private void init(ThreadGroup g, Runnable target, String name,
  long stackSize, AccessControlContext acc) {
  //...
  //(4)獲取當前線程
  Thread parent = currentThread();
  //...
  //(5)如果父線程的inheritableThreadLocals變量不為null
  if (parent.inheritableThreadLocals != null)
  //(6)設置子線程中的inheritableThreadLocals變量
  this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  this.stackSize = stackSize;
  tid = nextThreadID();
  }

創(chuàng)建線程時候在構造函數(shù)里面會調用init方法,前面講到了inheritableThreadLocal類get,set方法操作的是變量inheritableThreadLocals,所以這里inheritableThreadLocal變量就不為null,所以會執(zhí)行代碼(6),下面看createInheritedMap方法源碼,如下:

  static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
 return new ThreadLocalMap(parentMap);
 }

可以看到createInheritedMap內部使用父線程的inheritableThreadLocals變量作為構造函數(shù)創(chuàng)建了一個新的ThreadLocalMap變量,然后賦值給了子線程的inheritableThreadLocals變量,那么接著進入到ThreadLocalMap的構造函數(shù)里面做了什么,源碼如下:

private ThreadLocalMap(ThreadLocalMap parentMap) {
  Entry[] parentTable = parentMap.table;
  int len = parentTable.length;
  setThreshold(len);
  table = new Entry[len];

  for (int j = 0; j < len; j++) {
  Entry e = parentTable[j];
  if (e != null) {
   @SuppressWarnings("unchecked")
   ThreadLocal key = (ThreadLocal) e.get();
   if (key != null) {
   //(7)調用重寫的方法
   Object value = key.childValue(e.value);//返回e.value
   Entry c = new Entry(key, value);
   int h = key.threadLocalHashCode & (len - 1);
   while (table[h] != null)
    h = nextIndex(h, len);
   table[h] = c;
   size++;
   }
  }
  }
 }

如上代碼所做的事情就是把父線程的inhertableThreadLocals成員變量的值復制到新的ThreadLocalMap對象,其中代碼(7)InheritableThreadLocal類重寫的代碼(1)也映入眼簾了。

總的來說:InheritableThreadLocal類通過重寫代碼(2)和(3)讓本地變量保存到了具體線程的inheritableThreadLocals變量里面,線程通過InheritableThreadLocal類實例的set 或者 get方法設置變量時候就會創(chuàng)建當前線程的inheritableThreadLocals變量。當父線程創(chuàng)建子線程時候,

構造函數(shù)里面就會把父線程中inheritableThreadLocals變量里面的本地變量拷貝一份復制到子線程的inheritableThreadLocals變量里面。

好了原理了解到位了,接下來進行一個例子來驗證上面所了解的東西,如下:

package com.hjc;
/**
 * Created by cong on 2018/6/3.
 */
public class InheritableThreadLocalTest {
 //(1) 創(chuàng)建線程變量
 public static ThreadLocal threadLocal = new ThreadLocal();
 public static void main(String[] args) {
 //(2) 設置線程變量
 threadLocal.set("hello Java");
 //(3) 啟動子線程
 Thread thread = new Thread(new Runnable() {
  public void run() {
  //(4)子線程輸出線程變量的值
  System.out.println("子線程:" + threadLocal.get());

  }
 });
 thread.start();
 //(5)主線程輸出線程變量值
 System.out.println("父線程:" + threadLocal.get());
 }
}

運行結果如下:

Java并發(fā)編程學習之ThreadLocal源碼詳析 

也就是說同一個 ThreadLocal 變量在父線程中設置值后,在子線程中是獲取不到的。根據上節(jié)的介紹,這個應該是正?,F(xiàn)象,因為子線程調用 get 方法時候當前線程為子線程,而調用 set 方法設置線程變量是 main 線程,兩者是不同的線程,自然子線程訪問時候返回 null。

那么有辦法讓子線程訪問到父線程中的值嗎?答案是有,就用我們上面原理分析的InheritableThreadLocal。

將上面例子的代碼(1)修改為:

 //(1) 創(chuàng)建線程變量
 public static ThreadLocal threadLocal = new InheritableThreadLocal();

運行結果如下:

Java并發(fā)編程學習之ThreadLocal源碼詳析 

可知現(xiàn)在可以從子線程中正常的獲取到線程變量值了。那么什么情況下需要子線程可以獲取到父線程的 threadlocal 變量呢?

情況還是蠻多的,比如存放用戶登錄信息的 threadlocal 變量,很有可能子線程中也需要使用用戶登錄信息,再比如一些中間件需要用統(tǒng)一的追蹤 ID 把整個調用鏈路記錄下來的情景。

Spring Request Scope 作用域 Bean 中 ThreadLocal 的使用

我們知道 Spring 中在 XML 里面配置 Bean 的時候可以指定 scope 屬性來配置該 Bean 的作用域為 singleton、prototype、request、session 等,其中作用域為 request 的實現(xiàn)原理就是使用 ThreadLocal 實現(xiàn)的。如果你想讓你 Spring 容器里的某個 Bean 擁有 Web 的某種作用域,

則除了需要 Bean 級上配置相應的 scope 屬性,還必須在 web.xml 里面配置如下:


 org.springframework.web.context.request.RequestContextListener

這里主要看RequestContextListener的兩個方法:

public void requestInitialized(ServletRequestEvent requestEvent)

public void requestDestroyed(ServletRequestEvent requestEvent)

當一個web請求過來時候會執(zhí)行requestInitialized方法:

public void requestInitialized(ServletRequestEvent requestEvent) {
    .......省略
  HttpServletRequest request = (HttpServletRequest) requestEvent.getServletRequest();
  ServletRequestAttributes attributes = new ServletRequestAttributes(request);
  request.setAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE, attributes);
  LocaleContextHolder.setLocale(request.getLocale());
  //設置屬性到threadlocal變量
  RequestContextHolder.setRequestAttributes(attributes);
 }
 public static void setRequestAttributes(RequestAttributes attributes) {

  setRequestAttributes(attributes, false);
 }
 public static void setRequestAttributes(RequestAttributes attributes, boolean inheritable) {
  if (attributes == null) {
   resetRequestAttributes();
  }
  else {
   //默認inheritable=false
   if (inheritable) {
    inheritableRequestAttributesHolder.set(attributes);
    requestAttributesHolder.remove();
   }
   else {
    requestAttributesHolder.set(attributes);
    inheritableRequestAttributesHolder.remove();
   }
  }
 }

可以看到上面源碼,由于默認inheritable 為FALSE,我們的屬性值都放到了requestAttributesHoder里面,而它的定義是:

   private static final ThreadLocal requestAttributesHolder =new NamedThreadLocal("Request attributes");

  private static final ThreadLocal inheritableRequestAttributesHolder =new NamedInheritableThreadLocal("Request context");

其中NamedThreadLocal extends ThreadLocal,所以不具有繼承性。

其中 NamedThreadLocal extends ThreadLocal,所以不具有繼承性。

NameInheritableThreadLocal extends InheritableThreadLocal,所以具有繼承性,所以默認放入到RequestContextHolder里面的屬性值在子線程中獲取不到。

當請求結束時候調用requestDestroyed方法,源碼如下:

public void requestDestroyed(ServletRequestEvent requestEvent) {
  ServletRequestAttributes attributes =
    (ServletRequestAttributes) requestEvent.getServletRequest().getAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE);
  ServletRequestAttributes threadAttributes =
    (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  if (threadAttributes != null) {
   // 我們很有可能在最初的請求線程中
   if (attributes == null) {
    attributes = threadAttributes;
   }
   //請求結束則清除當前線程的線程變量。
   LocaleContextHolder.resetLocaleContext();
   RequestContextHolder.resetRequestAttributes();
  }
  if (attributes != null) {
   attributes.requestCompleted();
  }
 }

接下來從時序圖看一下 Web請求調用邏輯如何:

Java并發(fā)編程學習之ThreadLocal源碼詳析 

 也就是說每次發(fā)起一個Web請求在Tomcat中context(具體應用)處理前,host匹配后會設置下RequestContextHolder屬性,讓requestAttributesHolder不為空,在請求結束時會清除。

因此,默認情況下放入RequestContextHolder里面的屬性子線程訪問不到,Spring 的request作用域的bean是使用threadlocal實現(xiàn)的。 

接下來進行一個例子模擬請求,代碼如下:

web.xml配置如下:

因為是 request 作用域,所以必須是 Web 項目,并且需要配置 RequestContextListener 到 web.xml。


  org.springframework.web.context.request.RequestContextListener

接著注入一個 request 作用域 bean 到 IOC 容器。代碼如下:


  
  
 

測試代碼如下:

@WebResource("/testService")
public class TestRpc {
 @Autowired
 private RequestBean requestInfo;
 @ResourceMapping("test")
 public ActionResult test(ErrorContext context) {
  ActionResult result = new ActionResult();
  pvgInfo.setName("hjc");
  String name = requestInfo.getName();
  result.setValue(name);
  return result;
 }
}

如上首先配置 RequestContextListener 到 web.xml 里面,然后注入了 Request 作用域的 RequestBean 的實例到 IOC 容器,最后 TestRpc 內注入了 RequestBean 的實例,方法 test 首先調用了 requestInfo 的方法 setName 設置 name 屬性,然后獲取 name 屬性并返回。

這里如果 requestInfo 對象是單例的,那么多個線程同時調用 test 方法后,每個線程都是設置-獲取的操作,這個操作不是原子性的,會導致線程安全問題。而這里聲明的作用域為 request 級別,也是每個線程都有一個 requestInfo 的本地變量。 

上面例子方法請求的時序圖如下:

Java并發(fā)編程學習之ThreadLocal源碼詳析 

我們要著重關注調用test時候發(fā)生了什么:

Java并發(fā)編程學習之ThreadLocal源碼詳析 

其實前面創(chuàng)建的 requestInfo 是被經過 CGliB 代理后的(感興趣的可以研究下 ScopedProxyFactoryBean 這類),所以這里調用 setName 或者 getName 時候會被 DynamicAdvisedInterceptor 攔截的,攔擊器里面最終會調用到 RequestScope 的 get 方法獲取當前線程持有的本地變量。

關鍵來了,我們要看一下RequestScope的get方法的源碼如下:

public Object get(String name, ObjectFactory objectFactory) {

  RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();//(1)
  Object scopedObject = attributes.getAttribute(name, getScope());
  if (scopedObject == null) {
   scopedObject = objectFactory.getObject();//(2)
   attributes.setAttribute(name, scopedObject, getScope());//(3)
  }
  return scopedObject;
 }

可知當發(fā)起一個請求時候,首先會通過 RequestContextListener.requestInitialized 里面調用 RequestContextHolder.setRequestAttributess 設置 requestAttributesHolder。

然后請求被路由到 TestRpc 的 test 方法后,test 方法內第一次調用 setName 方法時候,最終會調用 RequestScope.get()方法,get 方法內代碼(1)獲取通過 RequestContextListener.requestInitialized 設置的線程本地變量 requestAttributesHolder 保存的屬性集的值。

接著看該屬性集里面是否有名字為 requestInfo 的屬性,由于是第一次調用,所以不存在,所以會執(zhí)行代碼(2)讓 Spring 創(chuàng)建一個 RequestInfo 對象,然后設置到屬性集 attributes,也就是保存到了當前請求線程的本地內存里面了。然后返回創(chuàng)建的對象,調用創(chuàng)建對象的 setName。

最后test 方法內緊接著調用了 getName 方法,最終會調用 RequestScope.get() 方法,get 方法內代碼(1)獲取通過 RequestContextListener.requestInitialized 設置的線程本地變量 RequestAttributes,然后看該屬性集里面是否有名字為 requestInfo 的屬性,

由于是第一次調用 setName 時候已經設置名字為 requestInfo 的 bean 到 ThreadLocal 變量里面了,并且調用 setName 和 getName 的是同一個線程,所以這里直接返回了調用 setName 時候創(chuàng)建的 RequestInfo 對象,然后調用它的 getName 方法。

到目前為止我們了解ThreadLocal 的實現(xiàn)原理,并指出 ThreadLocal 不支持繼承性;然后緊接著講解了 InheritableThreadLocal 是如何補償了 ThreadLocal 不支持繼承的特性;最后簡單的介紹了 Spring 框架中如何使用 ThreadLocal 實現(xiàn)了 Reqeust Scope 的 Bean。

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對創(chuàng)新互聯(lián)的支持。


網站標題:Java并發(fā)編程學習之ThreadLocal源碼詳析
分享鏈接:http://weahome.cn/article/jsjipi.html

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部