新闻资讯

首页 / 新闻资讯 / 正文

Image

java ThreadLocal详解(面试常问)

在java的多线程模块中,ThreadLocal是经常被提问到的一个知识点,提问的方式有很多种,可能是循序渐进也可能是就像我的题目那样,因此只有理解透彻了,不管怎么问,都能游刃有余。
接下来我们分步来剖析ThreadLocal

1.什么是ThreadLocal

从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
简要言之:往ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。

从字面意思来看非常容易理解,但是从实际使用的角度来看,就没那么容易了,作为一个面试常问的点,使用场景那也是相当的丰富

2. 使用场景

1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

2、线程间数据隔离

3、进行事务操作,用于存储线程事务信息。

4、数据库连接,Session会话管理。

3.怎么用

既然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) {      }      //......   

4.源码解析

4.1 set方法

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。

4.2 get方法

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;     } 

原理很简单

4.3 remove方法

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。

5. 避免内存泄漏

java ThreadLocal详解(面试常问)
上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。

1、Thread中有一个map,就是ThreadLocalMap

2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。

3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收

4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。

解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。

6.总结

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设计的目的就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题