介绍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方法时会进行一次变量拷贝。

--------EOF---------
微信分享/微信扫码阅读