扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
ThreadLocal翻译成中文比较准确的叫法应该是:线程局部变量。或称为线程本地变量
目前创新互联公司已为上1000家的企业提供了网站建设、域名、网络空间、网站托管运营、企业网站设计、古县网站维护等服务,公司将坚持客户导向、应用为本的策略,正道将秉承"和谐、参与、激情"的文化,与客户和合作伙伴齐心协力一起成长,共同发展。
这个玩意有什么用处?先解释一下,在并发编程的时候,一个单例模式的类的属性,如果不做任何处理(是否加锁,或者用原子类)其实是线程不安全的,各个线程都在操作同一个属性,比如CoreServlet,Servlet是单例模式,所以如果在Servlet中增加一个属性,那么就会有多线程访问这个属性就会诱发的安全性问题。
这样显然是不行的,并且我们也知道volatile这个关键字只能保证线程的可见性,不能保证线程安全的。如果加锁,效率有会有一定程度的降低。
那么我们需要满足这样一个条件:属性是同一个,但是每个线程都使用同一个初始值,也就是使用同一个变量的一个新的副本。这种情况之下ThreadLocal就非常使用,比如说DAO的数据库连接,DAO我们在实际项目中都会是单例模式的,那么他的属性Connection就不是一个线程安全的变量。而我们每个线程都需要使用他,并且各自使用各自的。这种情况,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 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方法。
上面提到ThreadLocalMap是自己实现的类似HashMap的功能,当出现Hash冲突(通过两个key对象的hash值计算得到同一个数组下标)时,它没有采用链表模式,而是采用的线性探测的方法,既当发生冲突后,就线性查找数组中空闲的位置。
当数组较大时,这个性能会很差,所以建议尽量控制ThreadLocal的数量。
ThreadLocal在案例中一般以static形式存在的。
此方法为ThreadLocal保存的数据类型指定的一个初始化值,在ThreadLocal中默认返回null。但可以重写initialValue()方法进行数据初始化。
如果使用的是Java8提供的Supplier函数接口更加简化:
get()用于返回当前线程ThreadLocal中数据备份,当前线程的数据都存在一个ThreadLocalMap的数据结构中。
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方法一样的方法。
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(()-> { }); } |
packagecom.hy.threadlocal01; publicclassThreadLocalDemo00 { publicstaticThreadLocal @Override protectedInteger initialValue() { return100; }; }; publicstaticvoidmain(String[] args) { System.out.println(Thread.currentThread().getName()+":"+tl00.get()); } } |
packagecom.hy.threadlocal01; publicclassThreadLocalDemo001 { publicstaticThreadLocal @Override protectedInteger initialValue() { return100; }; }; publicstaticvoidmain(String[] args) { tl001.set(200); System.out.println(Thread.currentThread().getName()+":"+tl001.get()); } } |
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()); } } |
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(); } } |
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 |
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(); } } } |
保证每个线程都能遍历完成,并且数据正确,其他线程不会影响当前线程的数据。
通常用于保存线程不安全的工具类,典型的需要使用的类就是 SimpleDateFormat。
在这种场景下,每个 Thread 内都有自己的实例副本,且该副本只能由当前 Thread 访问到并使用,相当于每个线程内部的本地变量,这也是 ThreadLocal 命名的含义。因为每个线程独享副本,而不是公用的,所以不存在多线程间共享的问题。
我们来做一个比喻,比如饭店要做一道菜,但是有 5 个厨师一起做,这样的话就很乱了,因为如果一个厨师已经放过盐了,假如其他厨师都不知道,于是就都各自放了一次盐,导致最后的菜很咸。这就好比多线程的情况,线程不安全。我们用了 ThreadLocal 之后,相当于每个厨师只负责自己的一道菜,一共有 5 道菜,这样的话就非常清晰明了了,不会出现问题。
使用ThreadLocal的好处,无非就是,同一个线程无需通过方法参数传递变量,因为变量是线程持有的,所以想用就可以直接用。
一个request请求进入tomcat容器, 进入controller, 再进入service, 再进入dao, 可能还会向自定义线程池发一个异步任务
以userId为例:
以上所有方法, 如果都加上 String userId 作为参数有多丑陋不用我说大家也能想到, 即使你都加上了, 那么以后又多了一个字段你咋办? 再全改一遍吗?
TransactionSynchronizationManager
spring的事务是可以嵌套的, 可能是10个service方法属于一个事务, 如果没有这个机制那么所有方法签名都要加上 Connection connection 作为参数
RequestContextHolder
在任何地方都可以得到 request 请求的参数, 但是这个容易滥用, 导致不同层的代码耦合在一起, 如果你在 service 方法中用了他, 那么你的 service 方法就无法很方便的单元测试, 因为你耦合了 http 请求的一些东西, 这本身应该是 controller 关注的
比如异步调用发短信服务, 短信服务想知道user_id是谁, 那么加方法参数依然是丑陋的
好在 jdk 给我们解决了一部分也就是, 如果用的是InheritableThreadLocal 那么在new Thread()的时候会复制这些变量到新线程, 但是如果你用的线程池就搞不定了
因为线程池中的线程初期是 new Thread 可以将变量带过去, 后期就不会 new Thread了, 而是从 pool 中直接拿一个 thread, 也就触发不了这一步了, 因此需要用到阿里开源的一个框架 transmittable-thread-local 来改造线程池来支持tl的变量传递。
=====================================================
每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。
例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。
在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦
之前我们项目上线后发现部分用户的日期居然不对了,排查下来是SimpleDataFormat的锅,当时我们使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。 其实要解决这个问题很简单,让每个线程都new 一个自己的 SimpleDataFormat就好了,但是1000个线程难道new1000个SimpleDataFormat? 所以当时我们使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。 |
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()); } } |
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(); //回滚事务
}
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当中。
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()); } } |
packagecom.hy.utils; importcom.hy.db.DBManager; publicclassTranscationManager { // 开启事务 publicvoidbeginTrans() throwsException { DBManager.getConn().setAutoCommit(false); } // 提交事务 publicvoidcommit() throwsException { DBManager.getConn().commit(); } // 回滚事务 publicvoidrollback() throwsException { DBManager.getConn().rollback(); } } |
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的类图结构,从图中可知: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具体实怎样工作的
每个线程内部有一个名为threadLocals的属性,该属性的类型为ThreadLocal.ThreadLocalMap类型(类似于一个HashMap),其中的key为当前定义的ThreadLocal变量的this引用,value为我们使用set方法设置的值。每个线程的本地变量存放在自己的本地内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。
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中。
在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; } |
remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量
public void remove() { //获取当前线程绑定的threadLocals ThreadLocalMap m = getMap(Thread.currentThread()); 当前名称:六,手写SpringMVC框架--什么是ThreadLocal? 当前地址:http://csdahua.cn/article/dscgioc.html 扫二维码与项目经理沟通我们在微信上24小时期待你的声音 解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流 |