【JDK1.8源码学习】ThreadLcoal

前言

线程封闭:当访问共享的可变数据时,通常需要同步。一种避免同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步,这种技术称为线程封闭(thread confinement)【摘自《Java并发编程实战》】

实现线程封闭的三种手段:
(1)Ad-hoc线程封闭: 开发者自己实现线程封闭,这种方式严重依赖开发者的水平,最后效果可能会相当的脆弱,因此基本不被推荐;
(2)栈封闭: JVM为我们实现的线程封闭,对应的是JVM区域中的栈区,当一个方法被多个线程调用时,这个方法内部的局部变量在每个线程内都是独立不被共享的,达到了线程封闭的效果,这是JVM的固有特性;
(3)ThreadLocal线程封闭: Java针对线程封闭规范封装的一个关键字,本文就针对这一关键字的实现原理做简单的阅读和分析。

介绍

ThreadLocal,线程私有变量,JDK提供的该工具类可以使得一种类型的对象单独存放在各自的线程里,不与其他线程共享,从而达到线程封闭的效果,因此存放在其中的变量也是线程安全的。
通常情况下,ThreadLocal变量设置为一个全局静态变量,本次学习基于JDK1.8

设计原理

  • ThreadLocal内部定义了一个ThreadLocalMap静态内部类,ThreadLocalMap内部又维护了一个Entry extends WeakReference>类型的table数组,该Entry类并非HashMap中的Entry键值对,但是具有类似的功能,即键-值存储功能;该Entry类内部有一个Object value字段,该value存放的就是实际每个线程需要存放的ThreadLocal对象;Entry继承自弱引用类WeakReference,此设定存在内存泄漏风险,关于这点后面后有说明;
static class ThreadLocalMap {
    // 定义一个嵌套类
    static class Entry extends WeakReference> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
    // 存放变量的数组
    private Entry[] table;
}
  • 同时,Thread线程类也维护了一个属性threadLocals:ThreadLocal.ThreadLocalMap threadLocals = null;该属性的作用就是维护一个Entry数组table,每个元素的referent对象为一个ThreadLocal对象,value为实际需要线程封闭的对象;Thread对象在初始化时,threadlocals属性赋值为null,所以第一次调用时获取到的threadLocals对象必然为null,这个时候就会去初始化这个threadLocals对象,并且赋予初值,如果重写了initialValue方法就返回重写方法体内构造的对象,否则就默认返回 null
public class Thread implements Runnable {
    // 当前线程占有的线程局部变量值,map由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

源码解读

  1. get()方法:获得当前线程的局部变量值。
// 返回当前当前线程局部变量值,如果当前线程的局部变量map还未被初始化,那么执行setInitialValue方法进行初始化
public T get() {
    // 首先获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程中维护的ThreadLocal.ThreadLocalMap threadLocals变量
    ThreadLocalMap map = getMap(t);
    if (map != null) { // threadLocals变量未被初始化
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    
    // threadLocals变量未被初始化
    return setInitialValue();
}

get方法流程图

get方法流程图

在执行get方法时,有一个重要的方法也必须被提及,这个方法由ThreadLocal.ThreadLocalMap来维护,即getEntry()方法,

/**
 * 获取当前ThreadLocal对象对象的entry,如果存在对应的entry,对应的entry会被命中,且该过程将是快速的;
 * 如果不存在对应的entry,那么将出发getEntryAfterMiss方法;
 * 
 * @param  key ThreadLocal对象
 * @return 与传入key关联的Entry对象,不存在则返回null
 */
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);
}

getEntry方法流程图:


getEntry方法流程图
  1. set()方法:为当前线程指定一个局部变量值。
/**
 * 为当前线程指定一个局部变量值.
 */
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程维护的ThreadLocal.ThreadLocalMap threadLocals变量
    ThreadLocalMap map = getMap(t);
    if (map != null) // threadLocals变量已被初始化过则存放值
        map.set(this, value);
    else // threadLocals变量未被初始化,出发创建流程:createMap
        createMap(t, value);
}

set方法流程图:


set方法流程图

关于内存泄漏

学习借鉴于(侵删):深入分析 ThreadLocal 内存泄漏问题

  • 为什么会内存泄漏?
    ThreadLocalMap内维护的Entry数组映射表,key为ThreadLocal对象本身,value为实际需要被隔离的线程私有对象;

    由于Entry继承自弱引用类WeakReference,使得key的引用为弱引用,弱引用的特性(引用强度比强引用和软引用都更弱,被其关联的对象只能生存到下一次GC之前,JDK1.2之后提供WeakReference来实现软引用),当线程存活时间足够长,直到下一次GC发生,key就会被回收,如果GC后线程依然存活,那么就会存在key为null的Entry.value,这个value就永远不会被访问到,导致内存泄漏。

  • 如何防止内存泄漏?
  1. ThreadLocal在设计时,加上了一些防护措施,即ThreadLocal的get()set()remove()方法都会清除key为null的entry;
  2. 在使用完后及时调用remove()方法;
  • Entry.key为何不使用强引用?
    从内存泄漏的原因分析来看,似乎是因为key使用了弱引用WeakReference,那么key为什么不使用引用强度更强的强引用来避免内存泄漏呢?
         我们可以确定的是,ThreadLocalMap的生命周期和Thread一样长,假设key为强引用对象,如果ThreadLocal对象不需要了被回收,但是由于ThreadLocalMap还持有ThreadLocal对象的强引用,是不会被GC回收掉的,而且ThreadLocal自身的机制也不能清除掉这个Entry,除非线程结束或者手动置null,否则ThreadLocal对象和对应的Entry会一直占用内存也造成内存泄漏;
         反之,如果是弱引用,那么至少在下次GC时ThreadLocal对象就会被回收,再辅以ThreadLocal自身的清除机制,整个Entry也会被清除。

总结

  • ThreadLocal能够实现线程内,局部变量读写的功能(线程封闭);
  • 其原理,较之HashMap容易理解得多,主要是利用内部定义的ThreadLocalMap内部类以及Thread线程类使用并维护这个内部类共同实现,其中ThreadLocal类并不起到存储的作用,真正实现存储功能的是内部定义的ThreadLocalMap类,ThreadLocal对象作为key来获取对应的局部变量值;
  • 由于ThreadLocalMap内Entry继承自弱引用WeakReference,如果ThreadLocal没有外部引用来引用它,那么GC的时候就会被回收掉;因此使用ThreadLocal存在内存泄漏的风险,所以每次使用完成之后,尽量都调用remove()方法来避免内存泄漏;

你可能感兴趣的:(【JDK1.8源码学习】ThreadLcoal)