六,手写SpringMVC框架--什么是ThreadLocal?
10. 什么是ThreadLocal
ThreadLocal翻译成中文比较准确的叫法应该是:线程局部变量。或称为线程本地变量
目前创新互联公司已为上1000家的企业提供了网站建设、域名、网络空间、网站托管运营、企业网站设计、古县网站维护等服务,公司将坚持客户导向、应用为本的策略,正道将秉承"和谐、参与、激情"的文化,与客户和合作伙伴齐心协力一起成长,共同发展。
这个玩意有什么用处?先解释一下,在并发编程的时候,一个单例模式的类的属性,如果不做任何处理(是否加锁,或者用原子类)其实是线程不安全的,各个线程都在操作同一个属性,比如CoreServlet,Servlet是单例模式,所以如果在Servlet中增加一个属性,那么就会有多线程访问这个属性就会诱发的安全性问题。
这样显然是不行的,并且我们也知道volatile这个关键字只能保证线程的可见性,不能保证线程安全的。如果加锁,效率有会有一定程度的降低。
那么我们需要满足这样一个条件:属性是同一个,但是每个线程都使用同一个初始值,也就是使用同一个变量的一个新的副本。这种情况之下ThreadLocal就非常使用,比如说DAO的数据库连接,DAO我们在实际项目中都会是单例模式的,那么他的属性Connection就不是一个线程安全的变量。而我们每个线程都需要使用他,并且各自使用各自的。这种情况,ThreadLocal就比较好的解决了这个问题。
ThreadLocal的主要作用:
ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。
Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面,代码如下所示:
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class); private static final ThreadLocal private static final ThreadLocal new NamedThreadLocal<>("Transaction synchronizations"); private staticfinal ThreadLocal new NamedThreadLocal<>("Current transaction name"); |
Spring的事务主要是ThreadLocal和AOP去做实现的,我这里提一下,大家知道每个线程自己的Connection conn是靠ThreadLocal保存的就好了。
ThreadLocal结构图:
当ThreadLocal Ref出栈后,由于ThreadLocalMap中Entry对ThreadLocal只是弱引用,所以ThreadLocal对象会被回收,Entry的key会变成null,然后在每次get/set/remove ThreadLocalMap中的值的时候,会自动清理key为null的value,这样value也能被回收了。
注意:如果ThreadLocal Ref一直没有出栈(例如上面的connectionHolder,通常我们需要保证ThreadLocal为单例且全局可访问,所以设为static),具有跟Thread相同的生命周期,那么这里的虚引用便形同虚设了,所以使用完后记得调用ThreadLocal.remove将其对应的value清除。
另外,由于ThreadLocalMap中只对ThreadLocal是弱引用,对value是强引用,如果ThreadLocal因为没有其他强引用而被回收,之后也没有调用过get/set,那么就会产生内存泄露,
在使用线程池时,线程会被复用,那么里面保存的ThreadLocalMap同样也会被复用,会造成线程之间的资源没有被隔离,所以在线程归还回线程池时要记得调用remove方法。
hash冲突
上面提到ThreadLocalMap是自己实现的类似HashMap的功能,当出现Hash冲突(通过两个key对象的hash值计算得到同一个数组下标)时,它没有采用链表模式,而是采用的线性探测的方法,既当发生冲突后,就线性查找数组中空闲的位置。
当数组较大时,这个性能会很差,所以建议尽量控制ThreadLocal的数量。
ThreadLocal常用方法:
ThreadLocal在案例中一般以static形式存在的。
initialValue方法
此方法为ThreadLocal保存的数据类型指定的一个初始化值,在ThreadLocal中默认返回null。但可以重写initialValue()方法进行数据初始化。
如果使用的是Java8提供的Supplier函数接口更加简化:
set(T value)方法
get方法
get()用于返回当前线程ThreadLocal中数据备份,当前线程的数据都存在一个ThreadLocalMap的数据结构中。
remomve()删除值
小结
initialValue() : 初始化ThreadLocal中的value属性值。
set():获取当前线程,根据当前线程从ThreadLocals中获取ThreadLocalMap数据结构,
如果ThreadLocalmap的数据结构没创建,则创建ThreadLocalMap,key为当前ThreadLocal实例,存入数据为当前value。ThreadLocal会创建一个默认长度为16Entry节点,并将k-v放入i位置(i位置计算方式和hashmap相似,当前线程的hashCode&(entry默认长度-1)),并设置阈值(默认为0)为Entry默认长度的2/3。
如果ThreadLocalMap存在。就会遍历整个Map中的Entry节点,如果entry中的key和本线程ThreadLocal相同,将数据(value)直接覆盖,并返回。如果ThreadLoca为null,驱除ThreadLocal为null的Entry,并放入Value,这也是内存泄漏的重点地区。
get()
get()方法比较简单。就是根据Thread获取ThreadLocalMap。通过ThreadLocal来获得数据value。注意的是:如果ThreadLocalMap没有创建,直接进入创建过程。初始化ThreadLocalMap。并直接调用和set方法一样的方法。
11 案例:
基本案例1:
案例0:
packagecom.hy.threadlocal01; publicclassThreadLocalDemo0 { publicstaticThreadLocal publicstaticvoidmain(String[] args) { System.out.println(Thread.currentThread().getName() + ":"+ tl0.get()); // main:null tl0.set(1000); System.out.println(Thread.currentThread().getName() + ":"+ tl0.get()); //main:1000 } } |
补充案例:匿名类
publicstaticvoidmain(String[] args) { Empfbb= newEmp(1, "fbb", "fbb", 40); fbb.run(); Emplbb= newEmp(2, "lbb", "lbb", 50) { @Override publicvoidrun() { super.run(); // 调用父类的run方法 } }; lbb.run(); //new了一个类的对象,这个类是一个匿名类,但是我知道这个类继承/实现了Emp类 Empzjb= newEmp(3, "zjm", "zjm", 18) { @Override// 重写父类run方法 publicvoidrun() { System.out.println(super.getEname() + ","+ super.getAge() + ",run..."); } }; zjb.run(); //和下面这案例,不能说完全相同,只能说一模一样 //new了一个匿名类该匿名类实现了Runnable接口 Thread t1= newThread(newRunnable() { @Override publicvoidrun() { } }); //lambda表达式写法 Thread t2= newThread(()-> { }); } |
案例00:
packagecom.hy.threadlocal01; publicclassThreadLocalDemo00 { publicstaticThreadLocal @Override protectedInteger initialValue() { return100; }; }; publicstaticvoidmain(String[] args) { System.out.println(Thread.currentThread().getName()+":"+tl00.get()); } } |
案例001:
packagecom.hy.threadlocal01; publicclassThreadLocalDemo001 { publicstaticThreadLocal @Override protectedInteger initialValue() { return100; }; }; publicstaticvoidmain(String[] args) { tl001.set(200); System.out.println(Thread.currentThread().getName()+":"+tl001.get()); } } |
案例01:
packagecom.hy.threadlocal01; publicclassThreadLocalDemo01 { publicstaticThreadLocal @Override protectedInteger initialValue() { System.out.println("=======begin"); return100; }; }; publicstaticvoidmain(String[] args) { System.out.println(Thread.currentThread().getName() + ": ->get -> init:"+ tl01.get()); tl01.set(200); // main线程改成200; System.out.println(Thread.currentThread().getName() + ": ->set -> get:"+ tl01.get()); tl01.remove(); System.out.println(Thread.currentThread().getName() + ": -> remove -> get->init:"+ tl01.get()); tl01.get(); System.out.println(Thread.currentThread().getName() + ": -> get:"+ tl01.get()); } } |
案例011
packagecom.hy.threadlocal01; publicclassThreadLocalDemo011 { publicstaticThreadLocal @Override protectedInteger initialValue() { System.out.println("=======begin"); return100; }; }; publicstaticvoidmain(String[] args) { System.out.println(Thread.currentThread().getName()+":"+tl01.get()); tl01.set(200); //main线程改成200; System.out.println(Thread.currentThread().getName()+":"+tl01.get()); System.out.println("***********************"); newThread() { @Override publicvoidrun() { System.out.println(Thread.currentThread().getName()+":"+tl01.get()); }; }.start(); } } |
案例0111:
packagecom.hy.threadlocal01; publicclassThreadLocalDemo0111 { publicstaticThreadLocal @Override protectedObject initialValue() { returnnewObject(); }; }; publicstaticvoidmain(String[] args) { finalObject o1= tl01.get(); System.out.println(Thread.currentThread().getName() + ":"+ o1); newThread() { @Override publicvoidrun() { Object o2= tl01.get(); System.out.println(Thread.currentThread().getName() + ":"+ o2); System.out.println(o1== o2); }; }.start(); } } |
案例2:
public class ThreadLocalTest05 { public static String dateToStr(int millisSeconds) { Date date = new Date(millisSeconds); SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get(); return simpleDateFormat.format(date); } private static final ExecutorService executorService = Executors.newFixedThreadPool(100); public static void main(String[] args) { for (int i = 0; i < 3000; i++) { int j = i; executorService.execute(() -> { String date = dateToStr(j * 1000); // 从结果中可以看出是线程安全的,时间没有重复的。 System.out.println(date); }); } executorService.shutdown(); } } class ThreadSafeFormatter { public static ThreadLocal |
基本案例2:
案例02:
packagecom.hy.threadlocal02; publicclassThreadLocalDemo02 { privatestaticThreadLocal @Override protectedInteger initialValue() { return0; } }; privatestaticvoidadd() { for(inti= 0; i< 5; i++) { // 从当前线程的ThreadLocal中获取默认值 Integer n= tl02.get(); n+= 1; // 往当前线程的ThreadLocal中设置值 tl02.set(n); System.out.println(Thread.currentThread().getName() + " : ThreadLocal num="+ n); } } publicstaticvoidmain(String[] args) { for(inti= 0; i< 3; i++) { newThread(newRunnable() { @Override publicvoidrun() { add(); } }).start(); } } } |
保证每个线程都能遍历完成,并且数据正确,其他线程不会影响当前线程的数据。
典型场景1:
通常用于保存线程不安全的工具类,典型的需要使用的类就是 SimpleDateFormat。
场景介绍
在这种场景下,每个 Thread 内都有自己的实例副本,且该副本只能由当前 Thread 访问到并使用,相当于每个线程内部的本地变量,这也是 ThreadLocal 命名的含义。因为每个线程独享副本,而不是公用的,所以不存在多线程间共享的问题。
我们来做一个比喻,比如饭店要做一道菜,但是有 5 个厨师一起做,这样的话就很乱了,因为如果一个厨师已经放过盐了,假如其他厨师都不知道,于是就都各自放了一次盐,导致最后的菜很咸。这就好比多线程的情况,线程不安全。我们用了 ThreadLocal 之后,相当于每个厨师只负责自己的一道菜,一共有 5 道菜,这样的话就非常清晰明了了,不会出现问题。
典型场景2
使用ThreadLocal的好处,无非就是,同一个线程无需通过方法参数传递变量,因为变量是线程持有的,所以想用就可以直接用。
业务场景的例子
一个request请求进入tomcat容器, 进入controller, 再进入service, 再进入dao, 可能还会向自定义线程池发一个异步任务
在这么多的类的方法中我想用某些共享的变量怎么办?
以userId为例:
- service 方法用 userId _id 判断用户权限
- dao 方法用 userId 在表中存储数据修改人的信息
- 异步调用另一个服务 B 的时候, 让 B 知道是谁调用了他
- 所有方法打印的 log, 我想统一加上 userId,否则不知道是谁调用的, 但是这么多方法改起来是是很崩溃的
以上所有方法, 如果都加上 String userId 作为参数有多丑陋不用我说大家也能想到, 即使你都加上了, 那么以后又多了一个字段你咋办? 再全改一遍吗?
spring的例子:
TransactionSynchronizationManager
spring的事务是可以嵌套的, 可能是10个service方法属于一个事务, 如果没有这个机制那么所有方法签名都要加上 Connection connection 作为参数
RequestContextHolder
在任何地方都可以得到 request 请求的参数, 但是这个容易滥用, 导致不同层的代码耦合在一起, 如果你在 service 方法中用了他, 那么你的 service 方法就无法很方便的单元测试, 因为你耦合了 http 请求的一些东西, 这本身应该是 controller 关注的
以上例子都是在一个 Thread内是ok的,如果新生成一个Thread,这些变量咋带过去呢?不带过去不就失联了吗?
比如异步调用发短信服务, 短信服务想知道user_id是谁, 那么加方法参数依然是丑陋的
好在 jdk 给我们解决了一部分也就是, 如果用的是InheritableThreadLocal 那么在new Thread()的时候会复制这些变量到新线程, 但是如果你用的线程池就搞不定了
因为线程池中的线程初期是 new Thread 可以将变量带过去, 后期就不会 new Thread了, 而是从 pool 中直接拿一个 thread, 也就触发不了这一步了, 因此需要用到阿里开源的一个框架 transmittable-thread-local 来改造线程池来支持tl的变量传递。
=====================================================
每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。
例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。
在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦
ThreadLocal的其他使用场景场景(面试加分项)
除了源码里面使用到ThreadLocal的场景,你自己有使用他的场景么?一般你会怎么用呢?
之前我们项目上线后发现部分用户的日期居然不对了,排查下来是SimpleDataFormat的锅,当时我们使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。 其实要解决这个问题很简单,让每个线程都new 一个自己的 SimpleDataFormat就好了,但是1000个线程难道new1000个SimpleDataFormat? 所以当时我们使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。 |
新建DBManager
packagecom.hy.db; importjava.sql.Connection; importjava.sql.DriverManager; publicclassDBManager { privatestaticfinalString URL= "jdbc:mysql://localhost:3306/jspdb07?characterEncoding=utf8"; privatestaticfinalString USER= "root"; privatestaticfinalString PWD= "root"; publicstaticConnection getConn() throwsException { Class.forName("com.mysql.jdbc.Driver"); Connection conn= DriverManager.getConnection(URL, USER, PWD); returnconn; } publicstaticvoidmain(String[] args) throwsException { System.out.println(DBManager.getConn()); } } |
新建TransactionManagerFilter
packagecom.hy.filter; importjava.io.IOException; importjavax.servlet.Filter; importjavax.servlet.FilterChain; importjavax.servlet.FilterConfig; importjavax.servlet.ServletException; importjavax.servlet.ServletRequest; importjavax.servlet.ServletResponse; importjavax.servlet.annotation.WebFilter; @WebFilter("*.do") publicclassTransactionManagerFilter implementsFilter { @Override publicvoidinit(FilterConfigfilterConfig) throwsServletException { } @Override publicvoiddoFilter(ServletRequest request, ServletResponse response, FilterChain chain) throwsIOException, ServletException { } @Override publicvoiddestroy() { } } |
事务管理过滤器中要写如下的代码:开启事务,提交事务,回滚事务
try{
conn.setAutoCommit(false); //开启事务
chain.doFilter(req,resp);// 放行();
conn.commit(); //提交事务
}catch(Exception ex){
conn.rollback(); //回滚事务
}
将其封装成一个类 TransactionManager
packagecom.hy.utils; publicclassTransactionManager { // 开启事务 publicstaticvoidbeginTrans() { } // 提交事务 publicstaticvoidcommit() { } // 回滚事务 publicstaticvoidrollback() { } } |
现在问题的焦点来到了,如何在TranscationManager中获取Connection对象,当然可以在方法中传递Connection对象,但是这是面向对象的方式。
packagecom.hy.utils; importjava.sql.Connection; importcom.hy.db.DBManager; publicclassTranscationManager { privatestaticThreadLocal // 开启事务 publicvoidbeginTrans() throwsException { // 获取Connection对象 Connection conn= threadLocal.get(); if(conn== null) { // 重新获取connecton对象 conn= DBManager.getConn(); // 将Connection对象放在ThreadLocal操作的map中。 threadLocal.set(conn); } // 设置不自动提交 conn.setAutoCommit(false); } // 提交事务 publicvoidcommit() throwsException { // 获取Connection对象 Connection conn= threadLocal.get(); if(conn== null) { // 重新获取connecton对象 conn= DBManager.getConn(); // 将Connection对象放在ThreadLocal操作的map中。 threadLocal.set(conn); } conn.commit(); } // 回滚事务 publicvoidrollback() throwsException { // 获取Connection对象 Connection conn= threadLocal.get(); if(conn== null) { // 重新获取connecton对象 conn= DBManager.getConn(); // 将Connection对象放在ThreadLocal操作的map中。 threadLocal.set(conn); } conn.rollback(); } } |
大家会发现,在这三个方法中,黄色代码部分都是一样的。这个代码的目的就是获取Connection对象。所以要想办法将这几句代码放入到DBManager当中。
新DBManager
packagecom.hy.db; importjava.sql.Connection; importjava.sql.DriverManager; publicclassDBManager { privatestaticfinalString URL= "jdbc:mysql://localhost:3306/jspdb07?characterEncoding=utf8"; privatestaticfinalString USER= "root"; privatestaticfinalString PWD= "root"; privatestaticThreadLocal privatestaticConnection createConn() throwsException { Class.forName("com.mysql.jdbc.Driver"); Connection conn= DriverManager.getConnection(URL, USER, PWD); returnconn; } publicstaticConnection getConn() throwsException { Connection conn= threadLocal.get(); if(conn== null) { conn= createConn(); threadLocal.set(conn); } returnthreadLocal.get(); } publicstaticvoidcloseConn() throwsSQLException { Connection conn= threadLocal.get(); if(conn== null) { return; } if(!conn.isClosed()) { conn.close(); threadLocal.set(null); } } publicstaticvoidmain(String[] args) throwsException { System.out.println(DBManager.getConn()); } } |
TranscationManager
packagecom.hy.utils; importcom.hy.db.DBManager; publicclassTranscationManager { // 开启事务 publicvoidbeginTrans() throwsException { DBManager.getConn().setAutoCommit(false); } // 提交事务 publicvoidcommit() throwsException { DBManager.getConn().commit(); } // 回滚事务 publicvoidrollback() throwsException { DBManager.getConn().rollback(); } } |
新TransactionManager
packagecom.hy.utils; importcom.hy.db.DBManager; publicclassTransactionManager { // 开启事务 publicstaticvoidbeginTrans() throwsException { DBManager.getConn().setAutoCommit(false); } // 提交事务 publicstaticvoidcommit() throwsException { DBManager.getConn().commit(); DBManager.closeConn(); } // 回滚事务 publicstaticvoidrollback() throwsException { DBManager.getConn().rollback(); DBManager.closeConn(); } } |
部分源码:
ThreadLocal解析:
是ThreadLocal的类图结构,从图中可知:Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的属性。
我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。
在默认情况下,每个线程对象都有两个属性,但是这两个属性量都为null
只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们(后面我们会查看这两个方法的源码)。
除此之外,和我所想的不同的是,每个线程的本地变量的值不是存放在ThreadLocal对象中,而是放在调用的线程对象的threadLocals属性里面(前面也说过,threadLocals是Thread类的属性)。也就是说,
ThreadLocal类 其实相当于一个 管家一样(所谓的工具人),只是用来 存值/取值 的,但是 存的值/取的值都来自于 当前线程对象里threadLocals属性,而这个属性是一个类似于Map的结构。
我们通过调用ThreadLocal的set方法将value值 添加到调用线程的threadLocals中,
通过调用ThreadLocal的get方法,它能够从它的当前线程的threadLocals中取出该值。
如果调用线程一直不终止,那么这个值(本地变量的值)将会一直存放在当前线程对象的threadLocals中。
当不使用本地变量的时候(也就是那个值时),需要只调用工具人ThreadLocal的 remove方法将其从当前线程对象的threadLocals中删除即可。
下面我们通过查看ThreadLocal的set、get以及remove方法来查看ThreadLocal具体实怎样工作的
1、解析:
每个线程内部有一个名为threadLocals的属性,该属性的类型为ThreadLocal.ThreadLocalMap类型(类似于一个HashMap),其中的key为当前定义的ThreadLocal变量的this引用,value为我们使用set方法设置的值。每个线程的本地变量存放在自己的本地内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。
2、set方法源码
public void set(T value) { //(1)获取当前线程(调用者线程) Thread t = Thread.currentThread(); //(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map ThreadLocalMap map = getMap(t); //(3)如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为添加的本地变量值 if (map != null) map.set(this, value); //(4)如果map为null,说明首次添加,需要首先创建出对应的map else createMap(t, value); } |
在上面的代码中,(2)处调用getMap方法获得当前线程对应的threadLocals(参照上面的图示和文字说明),该方法代码如下
ThreadLocalMap getMap(Thread t) {
returnt.threadLocals; //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
}
如果调用getMap方法返回值不为null,就直接将value值设置到threadLocals中(key为当前线程引用,值为本地变量);如果getMap方法返回null说明是第一次调用set方法(前面说到过,threadLocals默认值为null,只有调用set方法的时候才会创建map),这个时候就需要调用createMap方法创建threadLocals,该方法如下所示
1voidcreateMap(Thread t, T firstValue) {
2t.threadLocals = newThreadLocalMap(this, firstValue);
3}
createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了threadLocals中。
3、get方法源码
在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。
public T get() { //(1)获取当前线程 Thread t = Thread.currentThread(); //(2)获取当前线程的threadLocals变量 ThreadLocalMap map = getMap(t); //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量 return setInitialValue(); } private T setInitialValue() { //protected T initialValue() {return null;} T value = initialValue(); //获取当前线程 Thread t = Thread.currentThread(); //以当前线程作为key值,去查找对应的线程变量,找到对应的map ThreadLocalMap map = getMap(t); //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值 if (map != null) map.set(this, value); //如果map为null,说明首次添加,需要首先创建出对应的map else createMap(t, value); return value; } |
4、remove方法的实现
remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量
public void remove() { //获取当前线程绑定的threadLocals ThreadLocalMap m = getMap(Thread.currentThread()); 分享文章:六,手写SpringMVC框架--什么是ThreadLocal? 本文路径:http://ybzwz.com/article/dscgioc.html |