并发编程之深入理解threadlocal

并发编程之深入理解threadlocal 前言相信有一些开发经验的童鞋应该都听过threadlocal但是可能有一些只是知道threadlocal的使用并没有真正理解threadlocal的工作的原理以及在使用threadlocal中可能会遇到的问题今天会从源码的角度跟大家一起学习threadlocal使用的场景、常见的源码中如何使用它以及使用threadlocal应该注意什么—内存泄露。1、Threadlocal的使用2、Threadlocal的源码解析3、ThreadLocal内存泄漏的原因T hreadLocal的使用Threadlocal和synchronized都是用于解决多线程并发访问。然而ThreadLocal与synchronized有本质的区别。Synchronized是利用锁的机制使变量或者代码块在某一时刻仅能被一个线程访问。而ThreadLocal使用了线程隔离的方式为每一个线程都提供了变量的副本使得每一个线程在某一时刻访问到的并非同一个对象。先来看下一个简单的例子热身一下现在有创建三个线程每个线程对threadlocal中存的值自增看看打印结果package threadLocal; public class CounterTest { private final ThreadLocalInteger threadLocal new ThreadLocalInteger(){ Override protected Integer initialValue() { return 0; } }; private int getCounter(){ threadLocal.set(threadLocal.get()1); return threadLocal.get(); } public static void main(String[] args) { CounterTest counterTest new CounterTest(); Thread t1 new Thread(new MyTask(counterTest)); Thread t2 new Thread(new MyTask(counterTest)); Thread t3 new Thread(new MyTask(counterTest)); t1.start(); t2.start(); t3.start(); } static class MyTask implements Runnable{ private final CounterTest counterTest; public MyTask(CounterTest counterTest) { this.counterTest counterTest; } Override public void run() { for (int i 0; i 3; i) { // ④每个线程打出3个序列值 System.out.println(thread[ Thread.currentThread().getName() ] -- counterTest[ counterTest.getCounter() ]); } } } }执行结果很和谐是不是这就是用线程隔离的方法使得每个线程都是使用了自己的线程内部的变量。那么我们在哪些地方使用过threadlocal呢其实我们使用的spring事务就是使用了Threadlocal类spring会从数据库连接池中获取一个connection该connection就是放在threadlocal中也就是和线程绑定了事务的提交或者回滚只要从threadlocal中拿到connection进行操作。为何Spring的事务要借助ThreadLocal类以JDBC为例正常的事务代码可能如下dbc new DataBaseConnection();//第1行Connection con dbc.getConnection();//第2行con.setAutoCommit(false);// //第3行con.executeUpdate(...);//第4行con.executeUpdate(...);//第5行con.executeUpdate(...);//第6行con.commit();////第7行上述代码可以分成三个部分:事务准备阶段第13行业务处理阶段第46行事务提交阶段第7行可以很明显的看到不管我们开启事务还是执行具体的sql都需要一个具体的数据库连接。现在我们开发应用一般都采用三层结构如果我们控制事务的代码都放在DAO(DataAccessObject)对象中在DAO对象的每个方法当中去打开事务和关闭事务当Service对象在调用DAO时如果只调用一个DAO那我们这样实现则效果不错但往往我们的Service会调用一系列的DAO对数据库进行多次操作那么这个时候我们就无法控制事务的边界了因为实际应用当中我们的Service调用的DAO的个数是不确定的可根据需求而变化而且还可能出现Service调用Service的情况。如果不使用ThreadLocal代码大概就会是这个样子但是需要注意一个问题如何让三个DAO使用同一个数据源连接呢我们就必须为每个DAO传递同一个数据库连接要么就是在DAO实例化的时候作为构造方法的参数传递要么在每个DAO的实例方法中作为方法的参数传递。这两种方式无疑对我们的Spring框架或者开发人员来说都不合适。为了让这个数据库连接可以跨阶段传递又不显示的进行参数传递就必须使用别的办法。Web容器中每个完整的请求周期会由一个线程来处理。因此如果我们能将一些参数绑定到线程的话就可以实现在软件架构中跨层次的参数共享是隐式的共享。而JAVA中恰好提供了绑定的方法--使用ThreadLocal。结合使用Spring里的IOC和AOP就可以很好的解决这一点。只要将一个数据库连接放入ThreadLocal中当前线程执行时只要有使用数据库连接的地方就从ThreadLocal获得就行了。ThreadLocal源码解析其实threadlocal工作原理非常的简单我们先简单的描述一下然后再从一起阅读分析源码。threadlocal本身并不存储内容存储内容的thread对象中的ThreadLocalMap可以当作Map理解threadLocal是充当Thread对象中ThreadLocalMap中的key值简单的用下面一幅图表示一下所以每个线程取到的值其实是他自己线程内部的值如果结合上面的文字和图你能看懂的话那么恭喜你threadLocal的工作原理你基本上已经了解了。下面我们就来详细的看看threadLocal的源码这里主要介绍threadLocal中的4个方法public void set(T value)设置当前线程的线程局部变量的值public T get()返回当前线程所对应的线程局部变量public void remove()将当前线程局部变量的值删除目的是为了减少内存占用该方法是JDK5.0新增的方法需要注意的是当线程结束后那如果线程不结束呢对应该线程的局部变量将会自动被垃圾回收所以显示调用该方法清除线程局部变量并不是必须的操作但它可以加快内存的回收速度。pretected T initialValue()返回该线程局部变量的初始值该方法是一个protected的方法显然是为了让子类覆盖而设计的这个方法是一个延迟调用方法在线程第一次调用get()或者set(T t)时才运行并且仅执行一次ThreadLocalz中缺省实现直接返回一个null1、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(); }get方法非常的简单就是获取当前线程的ThreadLocalMap然后将this表示该ThreadLocal当作key值获取相应的value如果获取不到那么就返回初始设置的值。2、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方法也是简单的不要不要的啦首先也是获取当前的线程啦然后获取当前线程的ThreadLocalMap如果map不为null那么就往该线程的ThreadLocalMap中set一条数据key为this该threadLocalvalue为传进去的值这里解释一下ThreadLocalMap可以简单的理解成Map虽然和Map不一样它里面使用一个table[]存储键值对Entry。3、remove方法public void remove() { ThreadLocalMap m getMap(Thread.currentThread()); if (m ! null) m.remove(this); }remove也简单同样也是获取当前线程的ThreadLocalMap然后删除该keyThreadLocal对应的键值对4、initialValue()方法4、protected T initialValue() { return null; }initialValue一般用于我们自己初始化比如我们需要存储Integer类型的值那么可以重写initialValue方法然后返回0对应的Integer值。看到上面的四个方法的源码是不是惊呆啦不过如此嘛是不是so easy!但是我们今天学习可不仅仅到这里就结束了哦我们需要继续学习使用ThreadLocal可能会存在什么样的问题—内存泄露这是我们在面试过程中经常被问到的内容同时也让我们学的代码更加安全、健壮、稳定ThreadLocal内存泄漏原因及解决方法在分析ThreadLocal内存泄漏原因之前希望大家最好先了解强引用、软引用、弱引用、虚引用。强引用就是指在程序代码之中普遍存在的类似“Object objnew Object”这类的引用只要强引用还存在垃圾收集器永远不会回收掉被引用的对象实例。软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象在系统将要发生内存溢出异常之前将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存才会抛出内存溢出异常。在JDK 1.2之后提供了SoftReference类来实现软引用。弱引用也是用来描述非必需对象的但是它的强度比软引用更弱一些被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时无论当前内存是否足够都会回收掉只被弱引用关联的对象实例。在JDK 1.2之后提供了WeakReference类来实现弱引用。虚引用也称为幽灵引用或者幻影引用它是最弱的一种引用关系。一个对象实例是否有虚引用的存在完全不会对其生存时间构成影响也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在JDK 1.2之后提供了PhantomReference类来实现虚引用。了解上述内容之后我们来看下ThreadLocal中的内部类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 WeakReferenceThreadLocal? { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal? k, Object v) { super(k); value v; } } //... }可以看到ThreadLocalMap中的Entry的key是用弱引用那么到下一次垃圾回收就会被垃圾收集器回收掉然而我们就永远获取不到该key对应的value然而这个value又被thread中的threadlocal.threadLocalMap给强引用如果线程没有结束那么value将不会被回收又访问不了那么这个时候就产生了内存泄漏啦用下面的图来简单的描述一下。然而在前面的描述中已经提到过当线程结束时对应value也会被删除这个时候不会产生内存泄漏那什么时候我们不会让线程结束呢—就是我们在使用线程池的时候我们是会保留一部分线程放在线程池中这个时候使用threadlocal就要注意啦如果操作不注意就会产生内存泄漏啦。这个时候我们使用完之后可以手动调用threadlocal.remove()就可以将对应value删除掉防止内存泄漏。从上面的分析中貌似是使用了弱引用导致的内存泄漏那为什么jdk不使用强引用呢使用强引用threadlocal使用完被置null时我们结合上面的图来分析一下theadlocalnull但是currentThread还是持有key-value的强引用那么key和value都不会被回收但是又访问不了同样是产生了内存泄漏。使用弱引用当threadlocal对应实例被置null时那么currentthread中keythreadlocal会被垃圾回收器回收掉其对应的value在下一次threadlocal调用你set、get、remove方法时都有机会被回收掉。比较上述两种情况我们可以发现由于ThreadLocalMap的生命周期跟Thread一样长如果都没有手动删除对应key都会导致内存泄漏但是使用弱引用可以多一层保障。因此ThreadLocal内存泄漏的根源是由于ThreadLocalMap的生命周期跟Thread一样长如果没有手动删除对应key就会导致内存泄漏而不是因为弱引用。好了threadlocal就分享到这里如有错误欢迎指正