ThreadLocal 线程隔离

ThreadLocal而是一个java.lang 包下的线程内部的存储类,可以在线程内存储数据,数据存储以后,只有指定线程可以得到存储数据,实现线程隔离。

ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。且在线程内部任何地方都可以使用,线程之间互不影响

ThreadLocal 提供程内的局部变量,不同的线程之间不会相互程的生命周明内起作用,减少同一个程内多个函数或组件之间一些公共变量传详的复杂度

在多线程并发下我们可以通过过 Threadlocal在同一线程,不同组件中传通公共变量.每个线程的变量都是独立的,不会互相影响

内部结构

ThreadLocal 线程隔离_第1张图片
每个线程持有一个ThreadLocalMap对象。这个Map的key是 Threadlocal实例本身, valueオ是真正要存储的值 object每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。

  1. 每个 Threads程内部都有一个Map( Threadlocalmap),thread 销毁的时候map也会销毁
  2. (Map里面存储 Threadlocall对象(key)和线程的变星副本( value)
  3. Thread内部的Map是由 Threadlocal维护的,由 Threadlocal负责向map获取和设置程的变量值
  4. 对于不同的线程,每次获取本值时,别的线程并不能取到当前线程的副本值,形成了副本的隔离互不干抗

ThreadLocal 线程隔离_第2张图片
空参数构造

  • Threadlocal创建 Threadlocal对象

通过get和set方法就可以得到当前线程对应的值。

  • public void set()

该方法会检查当前调用线程,默认以该线程的Thread.currentThread()值作为键,来保存指定的值。将变量绑定到线程中

  • public Object get()

该方法会检查当前调用线程,默认以该线程的Thread.currentThread()值作为键,获取线程绑定保存的指定值。

移除当前程绑定的局部变量

  • public void remove()

ThreadLocal主要用于:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

2、线程间数据隔离

3、进行事务操作,用于存储线程事务信息。

4、数据库连接,Session会话管理

get方法,获取线程本地副本变量

public T get() {
    Thread t = Thread.currentThread();
    //获取此线程对象的threadLocalMap
    ThreadLocalMap map = getMap(t);
    //map 存在
    if (map != null) {
        //以当前的 Threadlocal为key,调用 getentry获取对应的存储实体
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            //获取存储实体e对应的 value值即为我们想要的当前线程对应此 Threadlocal的值
            T result = (T)e.value;
            return result;
        }
    }
    //初始化:有两种情况有执行当前代码
    //第一种:map不存在,表示此线程没有健护的 Threadlocalmap对象
    //第二种情况:map存在,但是有与当前 Threadloca1关联的 entry
    return setInitialValue();
}
private T setInitialValue() {
        //调用 initialvalue获取初始化的值此方法可以被子类重写,如果不重写默认返nul1
        T value = initialValue();
        Thread t = Thread.currentThread();
    //获取此线程对象中维护的 Threadlocalmap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //存在则用map,set设置此实体 entry
            map.set(this, value);
        } else {
            //当前线程 Thread不存在 Threadlocalmap对象,则调用用createmap进行 Threadlocalmap对象的初始化
           //并将t(当前线程)和 value(t対应的值)作为第一个 entry存放至 Threadlocalmap中
            createMap(t, value);
        }
    //注册TerminatingThreadLocal
    //TerminatingThreadLocal是ThreadLocal的又一个扩展。该ThreadLocal关联的值在线程结束前会被特殊处理,处理方式取决于回调方法threadTerminated(T value)
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

set 方法设置线程本地副本变

public void set(T value) {
     //获取当前线程
     Thread t = Thread.currentThread();
     //获取该线程对象维护的threadLocalMap
     ThreadLocalMap map = getMap(t);
     //如果存在map就直接set,没有则创建map并set 设置此entry
     if (map != null)
         map.set(this, value);
     else
         //1)当前线程 Thread不存在 Threadlocalmap对象
         //2)则用 createMap进行 Threadlocalmap.对象的初始化
         //3)并将t(当前程)和 value(t対应的值)作为第一个 entry存放至 Threadlocalmap中
         createMap(t, value);
}
//获取当前线程 Thread对应护的 Threadlocalmap
 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
  }
//创建当前线程 Thread対应维护的 Threadlocalmap
//  t当前线程   firstvalue存放到map中第一个 entry的值
 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

ThreadLocalMap的键值为set方法设置的ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中

ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

remove方法,移除线程本地副本变量

public void remove() {
 ThreadLocalMap m = getMap(Thread.currentThread());
 if (m != null)
     m.remove(this);//删除map对应的entry
}

让每个Thread对象,自身持有一个Map,这个Map的Key就是当前ThreadLocal对象,Value是本地线程变量值。相对于加锁的实现方式,这样做可以提升性能,其实是一种以时间换空间的思路

ThreadLocalMap

threadLocals是一种ThreadLocal.ThreadLocalMap类型的数据结构,作为内部类定义在ThreadLocal类中,其内部采用一种WeakReference的方式保存键值对。

ThreadLocal 线程隔离_第3张图片

成员变量

//初始容里一一必须是2的整次吊
private static final int INITIAL_CAPACITY 16:
//存放数据的 table, Entry类的定义在下面分析
//同样,数组长度必须是2的整次
private Entry[] table;
//数组里面 entry的个数,可以用于判断 tablel当前使用量是否超过國值
private int size =0
//进行扩容的值,表使用量大于它的时候进行扩容
private int threshold; //Default to 0

跟 Hashmaps类似, INITIAL CAPACITY代表这个Map的初始容量; table是一个 Entry类型的数组,用于存储数据;Slze代表表中的存储数目; threshold代表需要容时对应size的阈值

Entry

/*
Entry继 weakreffrence,井且用 Threadloca作为key
如果key为null( entry.get(O)==nul1),意味若key不再被引用
因此这时候 entry也可以从 tablel中清除
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

在 threadLocalmap中,也是用entry来保存K-V结构数据的。不过 Entry中的key只能是 Threadlocal对象,这点在构造方法中已经限定死了
另外, Entry继承 Weakreference,也就是key( Threadlocal)是弱引用,其目的是将 Threadlocal对象的生命周期和线程生命周期解绑

Thread threadLocal ThreadLocalMap三者关系:

  • 每一个thread都有threadLocals其类型为threadLocal.ThreadLocalMap,threadLocal是线程Thread中属性threadLocals的管理者。
  • threadLocalMap对象存放于thread对象中,其名称叫threadLocals是一个数组,数据是Entry类型,维护者一个或者多个Entry,Entry的key是ThreadLocal实例的弱引用,value是object类型,就是线程专属变量
  • 通过threadLocal访问副本数据时,实际上时通过thread来获取theadLocalMap,在通过threadLocalMap的key来获取副本的数据
  • threadLocalMap的key是threadLocal对象的弱引用,其目的是为了更好的对threadLocal进行回收。如果key被设置为强引用则该threadLocal则不能被回收

ThreadLocal与Synchronized

ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,

不同的点是Synchronized是通过线程等待,牺牲时间来解决访问冲突只提供了一份变量让不同的线程排队访问,来保证多个线程之间访问资源的同步

ThreadLocal采用以空间换时间的方式为每一个线程都提供了一份变量的副本从而实现同时访问而相不干抗
并且相比于Synchronized,ThreadLocal具有线程隔离的效果,多线程中让每个线程之间的数据相互隔离,可以让线程之间并发执行

哈希冲突解决

构造函数首先创建一个长度为16的Enty数组,然后计算出 firstKey对应的索引,然后存储到table中并设置size和阈值

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    //计算索引
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);//设置初始值
    size = 1;
    setThreshold(INITIAL_CAPACITY);//设置阈值
}

计算索引

int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);


firstKey.threadLocalHashCode

private final int threadlocalhashcode -nexthashcodeo
private static int nexthashcodeo
return pexthashcode. getandadd(HASH_INCREMENT);
//Atomicinteger是一个提供原子操作的 Integer美,通过线程安全的方式提作加减,适台高并发情兄下的使用
private static Atomicinteger nexthashcode = new Atomicinteger O:
//特的hash值
private static final int HASH_INCREMENT =0x61c88647

这里定义了一个 Atomiclntegers类型,每次获取当前值井加上 HASH INCREMENT=
0x61c88647这个值跟要波那列(金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里也就是 Entry table中,这样做可以尽量避免hash冲突

& (INITIAL_CAPACITY - 1)

计算hash的时候里面采用了 hash Code&(size-1)的算法,这相当于取模运算 hashcode%size的个更高效的实现,正是因为这种算法,我们求size必须是2的整次幕,这也能保证保证在素引不越界的前提下,使得hash发生冲突的次数减小

set方法

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);//计算索引
    //threadLocakMap使用线性探测法查找元素  
    /*
    该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出
    假设当前 table长度为16,也就是说如果计算出来key的hash值为14,如果 table[14]上已经有值
  且其key与当前key不一数,那么就发生了hash冲突,这个时候将14加1得别15,取 table[15]进行判断,这个时如果还是冲突会回到0,取  table[0]以此类推,直到可以插入
    */
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        // Threadlocal对应的key存在,直接盖之前的值
        if (e.refersTo(key)) {
            e.value = value;
            return;
        }
        //key为nul1,但是值不为nul1,说明之前的 Threadlocal刘象已经被回收了   
        if (e.refersTo(null)) {
            //用新元素皆换旧的元素,这个方法进行了不少的垃级清理动作,防止内存泄漏
            replaceStaleEntry(key, value, i);
            return;
        }
    }
   // Threadlocal对应的key不存在并且没有找到旧的元素。则在空元素的位置创一个新的 Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    /*
     cleansomeslots用于清除形些e.get()==nu11的元素,
     这种数key关联的对象已经被回收,所以这个 Entry( table[ index])可以被置null
     如果没有清除任何 entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行rehash(执行一次全表的扫描的清理工作)
    */
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
 }

A 首先还是根据key计算出索引1,然后查找位上的 Entry
B. 若是 Entry已经存在并且key等于传入的key,那么这时候直接给这个 Entry赋新的value值
C.若是 Entry存在,但是key为nul,则调用 replacestaleentry来更换这个key为空的 Entry
D.不断循环检查,直到遇到为nul的地方,这时候要是还没在循环过程中 return,那么就在这个nul的位新建一
个 Entry,井且插入,同时slze增加1
最后调用 cleansomeslots,清理key为null的 Entry,最后返回是否清理了 Entry,接下来再判断size是否>=阈值达到了 rehash的条件,达到的话就会调用 rehash函数执行一次全表的扫描清理

弱引用与内存泄漏

基本引用

Java中的引用有4种类型:强、软、弱、虚

强引用(StrongReference)

我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还活着,垃圾回收器就不会回收这种对象

弱引用( Weakreference)

垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存

软引用(SoftReference)

是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等

虚引用(PhantomReference)

虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收的活动

虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

内存泄漏

  • Memory overflow 内存溢出,没有足够的内存提供申请者使用
  • Memory leak:内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存
    的浪费,导致程序运行速度减儒甚至系统期濡等严重后果工内存泄漏的堆积终将导数内存溢出

如果key使用强引用
ThreadLocal 线程隔离_第4张图片
key使用的是弱引用

ThreadLocal时会发生内存泄漏的前提条件:

  1. ThreadLocal引用被设置为null,且后面没有set,get,remove操作。
  2. 线程一直运行,不停止。(线程池)
  3. 触发了垃圾回收。(Minor GC或Full GC)

ThreadLocal是一个弱引用,但value 是强引用,当key为null时,ThreadLocal会被当成垃圾回收;但是此时ThreadLocalMap生命周期和Thread的一样,只有当前Thread结束之后,所有与当前线程有关的资源才会被GC回收。除非手动删除,否则它不会回收,这时候就出现一条强引用链Threadref–>Thread–>ThreadLocalMap–>Entry(key,value),value不会被回收,而这块 value永远不会被访向到了,导致valuel内存泄漏。

ThreadLocal 线程隔离_第5张图片
既然key是强引用还是弱引用都会内存泄漏,那为啥还要使用弱引用呢?

事实上,在 Threadlocalmap中的 set/getentry方法中,会对key为null(也即是 Threadlocal为null)进行判断,

如果为null的话,那么是会对 value置为null
这就意味着使用完 Threadlocal,线程依然运行的前提下,就算忘记调用 remove方法,弱引用比强引用可以多一层保障:弱引用的 Threadlocal会被回收,对应的 value在下一次 Threadlocalmap调用set. get, remove中的任一方法的时候会被清除,从而避免内存泄漏

但这仅仅是多一层保障,若永远解决内存泄漏请使用完调用remove方法

根本原因

由于ThreadLocalMapl的生命周期跟Thread样长,只要线程一直运行,如果没有手动删除对应key,因为value是强引用(即使key是弱引用)导致entry 的value无法被回收,所以强引用链Threadref–>Thread–>ThreadLocalMap–>Entry(key,value)一直存在,进而导致value的内存泄漏

解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况

使用ThreadLocal时遵守以下两个小原则

  • ThreadLocal申明为private static final。Private与final 尽可能不让他人修改变更引用,
  • Static 表示为类属性,只有在程序结束才会被回收。ThreadLocal使用后务必调用remove方法。最简单有效的方法是使用后将其移除。

InheritableThreadLocal

ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能

public class Test {

    public static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<Integer>();

    public static void main(String args[]) {
        threadLocal.set(new Integer(456));
        Thread thread = new MyThread();
        thread.start();
        System.out.println("main = " + threadLocal.get());
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("MyThread = " + threadLocal.get());
        }
    }
}

InheritableThreadLocal类重写了ThreadLocal的3个函数:

 /**
     * 该函数在父线程创建子线程,向子线程复制InheritableThreadLocal变量时使用
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }
    /**
     * 由于重写了getMap,操作InheritableThreadLocal时,
     * 将只影响Thread类中的inheritableThreadLocals变量,
     * 与threadLocals变量不再有关系
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * 类似于getMap,操作InheritableThreadLocal时,
     * 将只影响Thread类中的inheritableThreadLocals变量,
     * 与threadLocals变量不再有关系
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }

 

子线程访问父线程的本地变量的根本原因:在构造Thread对象的时候对父线程的InheritableThreadLocal进行了复制

public class Thread implements Runnable {
      //默认人构造方法,会调用init方法进行初使化
      public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null);
    }

//最终会调用到当前这个方法
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name.toCharArray();
// parent为当前线程,也就是调用了new Thread();方法的线程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            if (security != null) {
                g = security.getThreadGroup();
            }
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
        g.checkAccess();
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
//在这里会继承父线程是否为后台线程的属性还有父线程的优先级
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
//这里是重点,当父线程的inheritableThreadLocals 不为空的时候,会调用 ThreadLocal.createInheritedMap方法,传入的是父线程的inheritableThreadLocals。 
        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

}

只要父线程在构造子线程(调用new Thread())的时候inheritableThreadLocals变量不为空。新生成的子线程会通过ThreadLocal.createInheritedMap方法将父线程inheritableThreadLocals变量有的对象复制到子线程的inheritableThreadLocals变量上。这样就完成了线程间变量的继承与传递。

InheritableThreadLocal之所以能够完成线程间变量的传递,是在new Thread()的时候对inheritableThreadLocals对像里的值进行了复制

子线程通过继承得到的InheritableThreadLocal里的值与父线程里的InheritableThreadLocal的值具有相同的引用,如果父子线程想实现不影响各自的对象,可以重写InheritableThreadLocal的childValue方法。

你可能感兴趣的:(JUC,java,线程安全,ThreadLocal,线程隔离)