介绍Java的ThreadLocal
ThreadLocal的应用场景:
1、实现了每个线程都有完全独立的变量,该变量只在当前线程内可见,可供多个方法之间共享,且其他线程不可见。一个最典型的场景,同一网站下,每一个用户保留着一个Session,Session可以使用ThreadLocal保存,使其只对当前用户可见,用户与用户之间是完全独立的。
2、变量不是线程安全的,然而我们还不希望通过同步的方式来实现,因为本身采用同步锁就影响性能。
在阿里巴巴的开发售测提到了SimpleDateFormat,该类就不是线程安全的。我们就完全可以使用ThreadLocal使之线程安全。
private static final ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue(){
System.out.println("Initializing SimpleDateFormat for - " + Thread.currentThread().getName() );
return new SimpleDateFormat("dd/MM/yyyy");
}
};
ThreadLocal原理
ThreadLocal的内部原理是每个线程内部都维护了一个Map,ThreadLocalMap,该Map比较特殊,使用线性探测法解决冲突,每个访问ThreadLocal变量的线程本地都会有一个副本,Map中的key是ThreadLocal实例,value就是当前ThreadLocal对象对应的变量值。
该hash Map是由多个Entry组成的:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
一个调用关系:Thread->ThreadLocalMap->ThreadLocal对象->Entry->value引用->Object对象。
问题来了,不断地向Map增加元素,如果旧数据得不到及时回收,非常容易造成内存泄漏。针对这个问题,ThreadLocal也做了很多。
内存泄漏解决方案
1、上述Entry的代码也能看出来,其继承自 WeakReference<ThreadLocal<?>>,ThreadLocal对象本身是个key,也就是说key是一个弱引用。用弱引用的原因就是使得当没有强引用只向ThreadLocal对象时,会被JVM回收,及时
回收内存。下面图形是我从网上找的,我认为是比较清晰的。
假如,我们正常写了如下的代码:
public void execute() {
doOne();
doTwo();
}
=public void doOne() {
ThreadLocal<Long> tl = new ThreadLocal<>();
tl.set(2L);
}
public void doTwo() {
ThreadLocal<Integer> tl = new ThreadLocal<>();
tl.set(99);
}
上面在线程中执行完set操作之后,栈中对ThreadLocal的引用就不存在了。目前仅剩下Entry的key 对ThreadLocal的引用,如果key是强引用的话,只要线程还存在,就算是ThreadLocal对象没有其他引用,也不会被回收。但key此时设计成弱引用,那么当发生GC时,如果内存不足,ThreadLocal对象是可以回收的。
2、上述方案中,只有key是弱引用,然而value并不是,如果不删除value和指向该value的对象的关系,value永远不会被回收。针对该问题,ThreadLocal,在get,set和remove方法每次执行的时侯,都会把key时null的Entry的删除。
if (k == null) {
e.value = null;
tab[i] = null;
size--;
}
详细介绍一下弱引用,JAVA中包括强引用,软引用,弱引用和虚引用。强引用即是new 创建的引用,如果存在引用,则不会被回收。软引用是当发生GC时,如果内存不足,则需要回收软引用;弱引用是发生GC时,如果只存在弱引用,则直接被回收。虚引用可以理解为没有引用。
弱引用常常用来实现本地缓存,即可以增大效率,又不会导致占用过多内存,可随时被回收。
其基础类是WeakReference
public class WeakReference<T> extends Reference<T> {
/**
* Creates a new weak reference that refers to the given object. The new
* reference is not registered with any queue.
*
* @param referent object the new weak reference will refer to
*/
public WeakReference(T referent) {
super(referent);
}
/**
* Creates a new weak reference that refers to the given object and is
* registered with the given queue.
*
* @param referent object the new weak reference will refer to
* @param q the queue with which the reference is to be registered,
* or <tt>null</tt> if registration is not required
*/
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
在使用时,可以直接使用如下:
String s= new String("dd");
WeakReference<String> weakR = new WeakReference<String>(s);
获取强引用对象:
weakR.get()
s = null
比较常用的是WeakHashMap,其中Entry是extends WeakReference,其中key是弱引用。
Tomcat中即使用了WeakHashMap,其设置了两级本地缓存,第一层是比较热点的缓存数据,第二层则是相对不是很热点的数据。第二层的数据在只有弱引用时就会被回收。
public final class ConcurrentCache<K, V> {
private final int size;
private final Map<K, V> eden;
private final Map<K, V> longterm;
public ConcurrentCache(int size) {
this.size = size;
this.eden = new ConcurrentHashMap(size);
this.longterm = new WeakHashMap(size);
}
public V get(K k) {
V v = this.eden.get(k);
if (v == null) {
synchronized(this.longterm) {
v = this.longterm.get(k);
}
if (v != null) {
this.eden.put(k, v);
}
}
return v;
}
public void put(K k, V v) {
if (this.eden.size() >= this.size) {
synchronized(this.longterm) {
this.longterm.putAll(this.eden);
}
this.eden.clear();
}
this.eden.put(k, v);
}
}
上述所说ThreadLocal实现了线程隔离,但有的场景就是需要部分线程的共享,比如全局链路跟踪,主子线程需要传递traceId,这时使用InheritableThreadLocal即可实现,主线程在创建子线程的时候,会拷贝主线程中的变量,他是在线程的创建中实现的。因此,InheritableThreadLocal不适用于线程池中,因为已创建的线程池无法执行相应的拷贝逻辑。针对于此,阿里开发的一个支持线程池的ThreadLocal,TransmittableThreadLocal 。感兴趣的可以查阅相关资料。其基本思想就是改掉InheritableThreadLocal只在创建时进行变量拷贝。他在线程执行run方法时会进行一次变量拷贝。
微信分享/微信扫码阅读