ThreadLocal
基本在项目开发中基本不会用到, 可是面试官是比较喜欢问这类问题的;因此仍是有必要了解一下该类的功能与原理的.
ThreadLocal
是一个将在多线程中为每个线程建立单独的变量副本的类; 当使用ThreadLocal来维护变量时, ThreadLocal
会为每一个线程建立单独的变量副本, 避免因多线程操做共享变量而致使的数据不一致的状况;java
通常来讲, ThreadLocal
在实际工业生产中并不常见, 可是在不少框架中使用却可以解决一些框架问题; 好比Spring中的事务、Spring 中 做用域 Scope
为 Request的Bean
使用ThreadLocal来解决.面试
一、将须要被多线程访问的属性使用ThreadLocal变量来定义; 下面以网上多数举例的DBConnectionFactory类为例来举例sql
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBConnectionFactory { private static final ThreadLocal<Connection> dbConnectionLocal = new ThreadLocal<Connection>() { @Override protected Connection initialValue() { try { return DriverManager.getConnection("", "", ""); } catch (SQLException e) { e.printStackTrace(); } return null; } }; public Connection getConnection() { return dbConnectionLocal.get(); } }
这样在Client获取Connection的时候, 每一个线程获取到的Connection都是该线程独有的, 作到Connection的线程隔离; 因此并不存在线程安全问题数组
一、主要是用到了Thread对象中的一个ThreadLocalMap类型的变量threadLocals, 负责存储当前线程的关于Connection的对象, 以dbConnectionLocal
这个变量为Key, 以新建的Connection
对象为Value; 这样的话, 线程第一次读取的时候若是不存在就会调用ThreadLocal
的initialValue
方法建立一个Connection对象而且返回;缓存
具体关于为线程分配变量副本的代码以下:安全
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(); }
一、首先获取当前线程对象t
, 而后从线程t
中获取到ThreadLocalMap
的成员属性threadLocals
数据结构
二、若是当前线程的threadLocals
已经初始化(即不为null
) 而且存在以当前ThreadLocal对象为Key的值, 则直接返回当前线程要获取的对象(本例中为Connection);多线程
三、若是当前线程的threadLocals
已经初始化(即不为null
)可是不存在以当前ThreadLocal对象为Key的的对象, 那么从新建立一个Connection对象, 而且添加到当前线程的threadLocals Map中,并返回并发
四、若是当前线程的threadLocals
属性尚未被初始化, 则从新建立一个ThreadLocalMap对象, 而且建立一个Connection对象并添加到ThreadLocalMap对象中并返回。框架
若是存在则直接返回很好理解, 那么对于如何初始化的代码又是怎样的呢?
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); return value; }
一、首先调用咱们上面写的重载事后的initialValue
方法, 产生一个Connection对象
二、继续查看当前线程的threadLocals
是否是空的, 若是ThreadLocalMap已被初始化, 那么直接将产生的对象添加到ThreadLocalMap中, 若是没有初始化, 则建立并添加对象到其中;
同时, ThreadLocal还提供了直接操做Thread对象中的threadLocals的方法
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
这样咱们也能够不实现initialValue
, 将初始化工做放到DBConnectionFactory
的getConnection
方法中:
public Connection getConnection() { Connection connection = dbConnectionLocal.get(); if (connection == null) { try { connection = DriverManager.getConnection("", "", ""); dbConnectionLocal.set(connection); } catch (SQLException e) { e.printStackTrace(); } } return connection; }
那么咱们看过代码以后就很清晰的知道了为何ThreadLocal可以实现变量的多线程隔离了; 其实就是用了Map的数据结构给当前线程缓存了, 要使用的时候就从本线程的threadLocals对象中获取就能够了, key就是当前线程;
固然了在当前线程下获取当前线程里面的Map里面的对象并操做确定没有线程并发问题了, 固然能作到变量的线程间隔离了;
如今咱们知道了ThreadLocal究竟是什么了, 又知道了如何使用ThreadLocal以及其基本实现原理了是否是就能够结束了呢? 其实还有一个问题就是ThreadLocalMap是个什么对象, 为何要用这个对象呢?
本质上来说, 它就是一个Map, 可是这个ThreadLocalMap
与咱们平时见到的Map
有点不同
一、它没有实现Map
接口;
二、它没有public的方法, 最多有一个default的构造方法, 由于这个ThreadLocalMap的方法仅仅在ThreadLocal类中调用, 属于静态内部类
三、ThreadLocalMap的Entry实现继承了WeakReference<ThreadLocal<?>>
四、该方法仅仅用了一个Entry数组来存储Key, Value; Entry并非链表形式, 而是每一个bucket里面仅仅放一个Entry;
要了解ThreadLocalMap的实现, 咱们先从入口开始, 就是往该Map中添加一个值:
private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
先进行简单的分析, 对该代码表层意思进行解读:
一、看下当前threadLocal的在数组中的索引位置 好比: `i = 2`, 看 `i = 2` 位置上面的元素(Entry)的`Key`是否等于threadLocal 这个 Key, 若是等于就很好说了, 直接将该位置上面的Entry的Value替换成最新的就能够了; 二、若是当前位置上面的 Entry 的 Key为空, 说明ThreadLocal对象已经被回收了, 那么就调用replaceStaleEntry 三、若是清理完无用条目(ThreadLocal被回收的条目)、而且数组中的数据大小 > 阈值的时候对当前的Table进行从新哈希
因此, 该HashMap是处理冲突检测的机制是向后移位, 清除过时条目 最终找到合适的位置;
了解完Set方法, 后面就是Get方法了:
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
先找到ThreadLocal的索引位置, 若是索引位置处的entry不为空而且键与threadLocal是同一个对象, 则直接返回; 不然去后面的索引位置继续查找;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadLocalDemo { static class LocalVariable { private Long[] a = new Long[1024 * 1024]; } // (1) final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>()); // (2) final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>(); public static void main(String[] args) throws InterruptedException { // (3) Thread.sleep(5000 * 4); for (int i = 0; i < 50; ++i) { poolExecutor.execute(new Runnable() { public void run() { // (4) localVariable.set(new LocalVariable()); // (5) System.out.println("use local varaible" + localVariable.get()); localVariable.remove(); } }); } // (6) System.out.println("pool execute over"); } }
我在网上找到一个样例, 若是用线程池来操做ThreadLocal
对象确实会形成内存泄露, 由于对于线程池里面不会销毁的线程, 里面总会存在着<ThreadLocal, LocalVariable>的强引用, 由于final static
修饰的 ThreadLocal
并不会释放, 而ThreadLocalMap
对于 Key 虽然是弱引用, 可是强引用不会释放, 弱引用固然也会一直有值, 同时建立的LocalVariable
对象也不会释放, 就形成了内存泄露; 若是LocalVariable
对象不是一个大对象的话, 其实泄露的并不严重, 泄露的内存 = 核心线程数 * LocalVariable对象的大小
;
因此, 为了不出现内存泄露的状况, ThreadLocal提供了一个清除线程中对象的方法, 即 remove
, 其实内部实现就是调用 ThreadLocalMap
的remove
方法:
private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }
找到Key对应的Entry, 而且清除Entry的Key(ThreadLocal)置空, 随后清除过时的Entry便可避免内存泄露;