ThreadLocal的使用和问题

编程入门 行业动态 更新时间:2024-10-24 14:15:58

<a href=https://www.elefans.com/category/jswz/34/1762419.html style=ThreadLocal的使用和问题"/>

ThreadLocal的使用和问题

ThreadLocal和synchronized都用于解决多线程并发访问,可是两者有着本质区别,synchronized使用的锁机制,使变量和代码块能在某一时刻仅仅被某一线程访问。而ThreadLocal为每个线程提供了变量的副本,使得每个线程某一时间操作的不是同一个对象,这样就隔离了多个线程之间的数据共享。

1.ThreadLocal的使用

我们来探究一下ThreadLocal怎么实现的数据隔离,先来看这个小demo

package com.rambo.thread;
/*
threadlocal*/
class User{private String name;public String getName() {return name;}public void setName(String name) {this.name = name;}
}
public class Test {private static final ThreadLocal<User> threadlocal = new ThreadLocal<User>(){@Overrideprotected User initialValue() {return new User();}};public static void main(String[] args) {new Thread(()->{User user = threadlocal.get();threadlocal.set(user);System.out.println(threadlocal.get());}).start();new Thread(()->{User user = threadlocal.get();threadlocal.set(user);System.out.println(threadlocal.get());}).start();}
}

我们这里主要是测试两个线程中操作的User对象是不是同一个,运行结果:

发现每个线程操作的都是自己的user。这也就实现了线程之间的数据隔离,线程A操作user对象,不会影响到B中的user。那么这是为什么呢?

2.ThreadLocal的原理

我们点入threadlocal.get()代码,首先看到的是获取到当前线程,然后从当前线程中getMap()。
Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t);
我们先来探究一下getMap(t);

ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

可以看到直接返回当前thread对象中的threadLocals属性

我们可以看到每个线程都维护着一个ThreadLocalMap对象。我们再来分析ThreadLoaclMap

我们可以看到,ThreadLoaclMap里面有一个内部类Entry,Entry中有两个属性,threadLocal和object,也就是说使用threadlocal但作为key,object作为value。其实此map和hashmap有相似之处。但是它处理hash冲突的方式是在哈希,不是链表法,这里不再详细说明。
我们研究完ThreadLoaclMap后,可以总结出,Thread中维护着一个ThreadLoaclMap,以threadlocal作为key,以我们要存入的数据作为value。具体怎么存入的我们后续用ThreadLocal源码做说明。

然后我们继续来看ThreadLocal的get方法。

public T get() {//得到当前运行的线程Thread t = Thread.currentThread();//从当前线程中拿到mapThreadLocalMap map = getMap(t);//如果map不为空,说明线程的map已经被实例化,里面可能有我们threadlocal对应的数据if (map != null) {//得到key为当前threadlocal的entryThreadLocalMap.Entry e = map.getEntry(this);//如果真的有对应的value,说明之前存入过key为当前threadlocal的对象。这里直接取出来即可if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//如果线程中map还没有实例化,说明还没有在线程的map中放入过东西,此时,返回我们初始化方法中的对象。return setInitialValue();}

接着看setInitialValue方法

private T setInitialValue() {//调用我们实现的initialValue方法。默认实现是返回空值T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);//map已经被实例化if (map != null)//直接存入。map.set(this, value);else//在这里会实例化线程对象中的threadlocalmap。并存入valuecreateMap(t, value);//返回此对象return value;}

到这里,我们自己的代码中的 User user = threadlocal.get();其实就分析完了。当第一次调用到threadlocal.get()时,我们首先得到当前线程,如果map已被实例化,则取出map中以此threadlocal为key的entry,如果取到不为空,直接返回,如果为空,或者map根本就没实例化,就会走到setInitialValue,在这个方法中,会实例化map,并且返回threadlocal中initialValue方法的返回值。所以我们threadl中的user对象其实是在这里被new出来的。这也是数据隔离的原因。

我们再来看下threadlocal的set方法。其实上面分析完,threadlocal的set方法已经很清晰了。

public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}

获取当前线程,获取map,然后设置值。
我们来画个图分析下threadlocal,threadlocalmap和thread的关系

3.threadlocal导致线程不安全问题

/*** 类说明:ThreadLocal的线程不安全演示*/
public class ThreadLocalUnsafe implements Runnable {public static Number number = new Number(0);public void run() {//每个线程计数加一number.setNum(number.getNum()+1);//将其存储到ThreadLocal中value.set(number);//输出num值System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());}public static ThreadLocal<Number> value = new ThreadLocal<Number>() {};public static void main(String[] args) {for (int i = 0; i < 5; i++) {new Thread(new ThreadLocalUnsafe()).start();}}private static class Number {public Number(int num) {this.num = num;}private int num;public int getNum() {return num;}public void setNum(int num) {this.num = num;}@Overridepublic String toString() {return "Number [num=" + num + "]";}}}

运行结果:

可以看到几个线程使用threadlocal并没有实现数据隔离。貌似是操作了同一个number对象,其实也很容易理解,这里Runnable对象中的number是static的,所以threadlocal在set的时候,其实放入的是同一个number对象。解决此例问题的方法是在threadlocal.set()方法传入的是每个thread中new的对象,可以把static去掉,这样每次set都是thread独有的number对象。

4.threadlocal可能造成内存泄露


这是threadlocalmap中栈和堆的对应图,当我们把threadlocal的引用置空Threadlocal value = null;此时线程中entry中的key还对此threadlocal对象有着弱引用。当发生gc的时候,弱引用会被回收,所以此时threadlocal被回收。这时我们的entry中的key会指向空,value也就永远不会被访问到,这是value只能和thrad对象一起回收了,也就发生了内存泄漏。
所以避免这里内存泄露的最好方法就是threadlocal使用完成后,调用remove方法,清除key和value。
因此,使用线程池+ ThreadLocal时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了value可能造成累积的情况。

我们再深入研究,当调用threadlocal的get和set方法时,在map.get方法中,有这样一行代码:

if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
private boolean cleanSomeSlots(int i, int n) {boolean removed = false;Entry[] tab = table;int len = tab.length;do {i = nextIndex(i, len);Entry e = tab[i];//清除key为空的entryif (e != null && e.get() == null) {n = len;removed = true;i = expungeStaleEntry(i);}} while ( (n >>>= 1) != 0);return removed;}

cleanSomeSlots方法就清除了map中所有key为空entry。但是可能在时间上发生滞后,所以并不能完全解决threadlocal的内存泄露问题。
可能还有朋友有这样的疑问,
1.把map中的threadlocal也弄成强引用是不是好一点?其实不是,因为这里讨论的前提是把ThreadLocal外部的强引用已经置空。所以这里的entry已经用不到了。所以这里就应该清除掉。
2.把value也搞成弱引用一起回收咋样?随然回收是方便了点,但是发生gc的时候,当我ThreadLocal外部并没有置空,也就是说这里的threadlocal还有用的时候,先把value干掉了,肯定是不对的,threadlocal.get()取出来直接是空了。

更多推荐

ThreadLocal的使用和问题

本文发布于:2024-03-12 22:55:05,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1732605.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:ThreadLocal

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!