从了解了多线程后,我们一直在学习如何实现线程安全?可以通过加锁(ReentrantLock、synchronized、)或者采用具有原子性操作的类型定义共享变量。我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?
JDK 中自带的ThreadLocal类正是为了解决这样的问题。ThreadLocal被称为线程局部变量,用于在线程中保存数据。由于在ThreadLocal中保存的数据仅属于当前线程,所以该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。
下面看一个情景:两个线程分别要进行角色分配
public class Demo01 {
protected static String role;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
role="苏妲己";
show();
Sample.dosth();
}
},"线程t1");
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
role="姜子牙";
show();
Sample.dosth();
}
},"线程t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("THE END!");
}
// 线程1或线程2
public static void show() {
System.out.println("show:"+Thread.currentThread().getName()+"分配角色:"+role);
}
}
class Sample {
// 线程1或线程2
public static void dosth() {
System.out.println("dosth:"+Thread.currentThread().getName()+"分配角色:"+Demo01.role);
}
}
按照我们所想的本应该是线程t1分配的角色为苏妲己,t2分配的角色为姜子牙,但是变量role为一个共享变量,线程t1、t2均可访问,在t1访问赋值后t2可能就再次赋值了,对于2个线程来说role是共享的,不独属于任何一个线程,结果就是后执行的线程会覆盖前一个线程的数据。
为了避免上述情况的发生,我们引入对ThreadLocal类的使用,修改代码如下
public class Demo01 {
protected static ThreadLocal<String> roleThreadLocal=new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
roleThreadLocal.set("苏妲己");
show();
Sample.dosth();
}
},"线程t1");
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
roleThreadLocal.set("姜子牙");
show();
Sample.dosth();
}
},"线程t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("THE END!");
}
public static void show() {
System.out.println("show:"+Thread.currentThread().getName()+"分配角色:"+roleThreadLocal.get());
}
}
class Sample {
// 线程1或线程2
public static void dosth() {
System.out.println("dosth:"+Thread.currentThread().getName()+"分配角色:"+Demo01.roleThreadLocal.get());
}
}
运行结果如下:
通过使用ThreadLocal让线程之间有了隔离,每个线程都有自己的“私域”,不会被别的线程共享,下面我们通过读ThreadLocal的部分源代码让我们更深入的了解一下它的实现原理
public class Thread implements Runnable {
//......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//......
}
从上面
Thread
类 源代码可以看出Thread
类中有一个threadLocals
和 一个inheritableThreadLocals
变量,它们都是ThreadLocalMap
类型的变量,ThreadLocalMap为ThreadLocal的静态内部类,我们可以把ThreadLocalMap
理解为ThreadLocal
类实现的定制化的HashMap
。默认情况下这两个变量都是 null,只有当前线程调用ThreadLocal
类的set
或get
方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap
类对应的get()
、set()
方法
1、存储数据至当前线程的ThreadLocalMap
public void set(T value) {
//获取当前请求的线程
Thread t = Thread.currentThread();
//取出 Thread 类内部的 threadLocals 变量(ThreadLocalMap)
ThreadLocalMap map = getMap(t);
if (map != null)
// 将需要存储的值放入到这个哈希表中
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
2、从当前线程的ThreadLocalMap中获取数据
public T get() {
// 获取当前线程的ThreadLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 使用ThreadLocal对象做key,获取数据(Entry类型)
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
我们发现最终的变量是放在了当前线程的
ThreadLocalMap
中,并不是存在ThreadLocal
上,ThreadLocal
可以理解为只是ThreadLocalMap
的封装,传递了变量值。ThrealLocal
类中可以通过Thread.currentThread()
获取到当前线程对象后,直接通过getMap(Thread t)
可以访问到该线程的ThreadLocalMap
对象。
ThreadLocal与线程Threa及ThreadLocalMap的关系如下图所示:
每个Thread
中都具备一个ThreadLocalMap
,而ThreadLocalMap
可以存储以ThreadLocal
为 key ,Object 对象为 value 的键值对。ThreadLocalMap部分源码:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//数组初始大小
private static final int INITIAL_CAPACITY = 16;
//数组
private Entry[] table;
}
}
ThreaLocalMap用于存值的数组内部基于一个个Entry对象所实现,它的数组初始容量为16,与我们所了解的HashMap这一点极为相似,下面进入ThreaLocalMap看一下set方法
private void set(ThreadLocal<?> key, Object value) {
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类型数据作为key,要存入的数据
1、把用于存储数据的table数组复制,存储长度
2、取key的hashcode与数组长度-1做与运算获取key在哈希表中的位置【同key的hashcode%(数组长度)】扰动函数
3、获取所计算出来的下标在map中的元素值,判断是否为null
若不为null,则说明当前位置已有元素,获取它的key,继续判断它的key与我传进来的key是否相等,相同则直接覆盖它原来的value并返回。
若为null,则说明当前位置没有元素,直接添加,同时size++。
上面我们看了向ThreadLocal中存数据和取数据。还有一点要注意:
使用完成后从当前线程的ThreadLocalMap中删除数据,在线程池的线程复用场景中,线程执行完毕时一定要调用remove(),避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。
ThreadLocalMap m = getMap(Thread.currentThread());
// 使用当前ThreadLocal对象做key,删除数据
if (m != null)
m.remove(this);
}
下面思考一下:对于父线程往ThreadLocal中存储的数据,子线程能否获取到呢?
答案是否,因为线程在ThreadLocal.set()存储数据时,实际是往当前线程中的ThreadLocalMap中存,每个线程都有自己的ThreadLocalMap,通过ThreadLocal.get()实际上是使用同一个键ThreadLocal在不同的ThreadLocalMap取值。
但是在实际工作中,有可能需要在父子线程中共享数据的,在这种情况下使用ThreadLocal是行不通的。main方法是在主线程中执行的,相当于父线程。在main方法中开启了另外一个线程,相当于子线程。两个线程对象,各自拥有不同的ThreadLocalMap。应该使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类。
小结:
- 每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对.
- ThreadLocal.get()实际上是使用同一个键ThreadLocal在不同的ThreadLocalMap取值
- 对于父子线程想要共享使用InheritableThreadLocal
最后特别注意的地方是:一定要在finally代码块中,调用remove()方法清理没用的数据。如果业务代码出现异常,也能及时清理没用的数据。remove()方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。