在java的多线程模块中,ThreadLocal是经常被提问到的一个知识点,提问的方式有很多种,可能是循序渐进也可能是就像我的题目那样,因此只有理解透彻了,不管怎么问,都能游刃有余。
接下来我们分步来剖析ThreadLocal
从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
简要言之:往ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。
从字面意思来看非常容易理解,但是从实际使用的角度来看,就没那么容易了,作为一个面试常问的点,使用场景那也是相当的丰富
既然ThreadLocal的作用是每一个线程创建一个副本,我们使用一个例子来验证一下:
import java.util.Random; import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; public class test { public static void main(String[] args) { //新建一个ThreadLocal ThreadLocal<String> local =new ThreadLocal<>(); //新建一个随机数类 Random random = new Random(); //使用stream新建五个线程 IntStream.range(0,5).forEach(a->new Thread(()->{ //为每一个线程设置相应的Local值 local.set(a+" "+ random.nextInt(10)); System.out.println("线程和Local值分别为:"+ local.get()); try { TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e) { e.printStackTrace(); } }).start()); } }
结果:
线程和Local值分别为:3 0
线程和Local值分别为:4 4
线程和Local值分别为:0 6
线程和Local值分别为:1 6
线程和Local值分别为:2 3
从结果我们可以看到,每一个线程都有各自的local值,我们设置了一个休眠时间,就是为了另外一个线程也能够及时的读取当前的local值。
再来介绍一下最典型的管理数据库的Connection
:当时在学JDBC的时候,为了方便操作写了一个简单数据库连接池,需要数据库连接池的理由也很简单,频繁创建和关闭Connection是一件非常耗费资源的操作,因此需要创建数据库连接池~那么,数据库连接池的连接怎么管理呢??我们交由ThreadLocal来进行管理。为什么交给它来管理呢??ThreadLocal能够实现当前线程的操作都是用同一个Connection,保证了事务!当时候写的代码:
public class DBUtil { //数据库连接池 private static BasicDataSource source; //为不同的线程管理连接 private static ThreadLocal<Connection> local; static { try { //加载配置文件 Properties properties = new Properties(); //获取读取流 InputStream stream = DBUtil.class.getClassLoader().getResourceAsStream("连接池/config.properties"); //从配置文件中读取数据 properties.load(stream); //关闭流 stream.close(); //初始化连接池 source = new BasicDataSource(); //设置驱动 source.setDriverClassName(properties.getProperty("driver")); //设置url source.setUrl(properties.getProperty("url")); //设置用户名 source.setUsername(properties.getProperty("user")); //设置密码 source.setPassword(properties.getProperty("pwd")); //设置初始连接数量 source.setInitialSize(Integer.parseInt(properties.getProperty("initsize"))); //设置最大的连接数量 source.setMaxActive(Integer.parseInt(properties.getProperty("maxactive"))); //设置最长的等待时间 source.setMaxWait(Integer.parseInt(properties.getProperty("maxwait"))); //设置最小空闲数 source.setMinIdle(Integer.parseInt(properties.getProperty("minidle"))); //初始化线程本地 local = new ThreadLocal<>(); } catch (IOException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { //获取Connection对象 Connection connection = source.getConnection(); //把Connection放进ThreadLocal里面 local.set(connection); //返回Connection对象 return connection; } //关闭数据库连接 public static void closeConnection() { //从线程中拿到Connection对象 Connection connection = local.get(); try { if (connection != null) { //恢复连接为自动提交 connection.setAutoCommit(true); //这里不是真的把连接关了,只是将该连接归还给连接池 connection.close(); //既然连接已经归还给连接池了,ThreadLocal保存的Connction对象也已经没用了 local.remove(); } } catch (SQLException e) { e.printStackTrace(); } } }
还有就是session,cookie的应用:
每当我访问一个页面的时候,浏览器都会帮我们从硬盘中找到对应的Cookie发送过去。浏览器是十分聪明的,不会发送别的网站的Cookie过去,只带当前网站发布过来的Cookie过去浏览器就相当于我们的ThreadLocal,它仅仅会发送我们当前浏览器存在的Cookie(ThreadLocal的局部变量),不同的浏览器对Cookie是隔离的(Chrome,Opera,IE的Cookie是隔离的【在Chrome登陆了,在IE你也得重新登陆】),同样地:线程之间ThreadLocal变量也是隔离的….那上面避免了参数的传递了吗??其实是避免了。Cookie并不是我们手动传递过去的,并不需要写来进行传递参数…
在编写程序中也是一样的:日常中我们要去办理业务可能会有很多地方用到身份证,各类证件,每次我们都要掏出来很麻烦
// 咨询时要用身份证,学生证,房产证等等.... public void consult(IdCard idCard,StudentCard studentCard,HourseCard hourseCard){ } // 办理时还要用身份证,学生证,房产证等等.... public void manage(IdCard idCard,StudentCard studentCard,HourseCard hourseCard) { } //......
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { map.set(this, value); } else { createMap(t, value); } }
从set方法我们可以看到,首先获取到了当前线程t,然后调用getMap获取ThreadLocalMap,如果map存在,则将当前线程对象t作为key,要存储的对象作为value存到map里面去。如果该Map不存在,则初始化一个。
OK,到这一步了,相信你会有几个疑惑了,ThreadLocalMap是什么,getMap方法又是如何实现的。带着这些问题,继续往下看。先来看ThreadLocalMap。
static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
我们可以看到ThreadLocalMap其实就是ThreadLocal的一个静态内部类,里面定义了一个Entry来保存数据,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。
还有一个getMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
调用当期线程t,返回当前线程t中的成员变量threadLocals。而threadLocals其实就是ThreadLocalMap。
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(); }
通过上面ThreadLocal的介绍相信你对这个方法能够很好的理解了,首先获取当前线程,然后调用getMap方法获取一个ThreadLocalMap,如果map不为null,那就使用当前线程作为ThreadLocalMap的Entry的键,然后值就作为相应的的值,如果没有那就设置一个初始值。
如何设置一个初始值呢?
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); } if (this instanceof TerminatingThreadLocal) { TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this); } return value; }
原理很简单
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) { m.remove(this); } }
从我们的map移除即可。
OK,其实内部源码很简单,现在我们总结一波
(1)每个Thread维护着一个ThreadLocalMap的引用
(2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
(3)ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。
(4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中
(5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。
(6)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
OK,现在从源码的角度上不知道你能理解不,对于ThreadLocal来说关键就是内部的ThreadLocalMap。
上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。
1、Thread中有一个map,就是ThreadLocalMap
2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。
3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收
4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。
解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
1.每个Thread维护着一个ThreadLocalMap的引用
2.ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
3.调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
4.调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
5.ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响~
但是ThreadLocal设计的目的就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题