ThreadLocal的简单使用及实现的原理

2023/11/30 9:49:12

ThreadLocal简介

ThreadLocal的实例代表了一个线程局部的变量,每条线程都只能看到自己的值,并不会意识到其它的线程中也存在该变量。

它采用采用空间来换取时间的方式,解决多线程中相同变量的访问冲突问题。

ThreadLocal的简单使用

下面的例子中,创建了两个线程,然后线程对各自的局部变量进行递增的操作。每个线程中的局部变量的初始值都是100。

imgThreadLocal中的值

定义对ThreadLocal的操作,也就是在原来的基础上进行加10的操作,然后打印出结果。

img对ThreadLocal的操作

创建两个线程并启动,主线程等待这两个线程执行完成。最值得注意的就是主线程中输出的bank.get(),输出的初始值100。

img创建线程并等待线程执行完成

ThreadLocal的实现原理

每个Thread的对象都有一个ThreadLocalMap,当创建一个ThreadLocal的时候,就会将该ThreadLocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类型。

在该类中,我觉得最重要的方法就是两个:set()和get()方法。当调用ThreadLocal的get()方法的时候,会先找到当前线程的ThreadLocalMap,然后再找到对应的值。set()方法也是一样。

img

为了解释ThreadLocal类的工作原理,必须同时介绍与其工作甚密的其他几个类

  • ThreadLocalMap(内部类)
  • Thread

img

首先,在Thread类中有一行:

    /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

其中ThreadLocalMap类的定义是在ThreadLocal类中,真正的引用却是在Thread类中。同时,ThreadLocalMap中用于存储数据的entry定义:

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

从中我们可以发现这个Map的key是ThreadLocal类的实例对象,value为用户的值,并不是网上大多数的例子key是线程的名字或者标识。ThreadLocal的set和get方法代码:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
 
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

其中的getMap方法:

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

给当前Thread类对象初始化ThreadlocalMap属性:

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

到这里,我们就可以理解ThreadLocal究竟是如何工作的了

  1. Thread类中有一个成员变量属于ThreadLocalMap类(一个定义在ThreadLocal类中的内部类),它是一个Map,他的key是ThreadLocal实例对象。
  2. 当为ThreadLocal类的对象set值时,首先获得当前线程的ThreadLocalMap类属性,然后以ThreadLocal类的对象为key,设定value。get值时则类似。
  3. ThreadLocal变量的活动范围为某线程,是该线程“专有的,独自霸占”的,对该变量的所有操作均由该线程完成!也就是说,ThreadLocal 不是用来解决共享对象的多线程访问的竞争问题的,因为ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。当线程终止后,这些值会作为垃圾回收。
  4. 由ThreadLocal的工作原理决定了:每个线程独自拥有一个变量,并非是共享的,下面给出一个例子:
public class Son implements Cloneable{
    public static void main(String[] args){
        Son p=new Son();
        System.out.println(p);
        Thread t = new Thread(new Runnable(){  
            public void run(){
                ThreadLocal<Son> threadLocal = new ThreadLocal<>();
                System.out.println(threadLocal);
                threadLocal.set(p);
                System.out.println(threadLocal.get());
                threadLocal.remove();
                try {
                    threadLocal.set((Son) p.clone());
                    System.out.println(threadLocal.get());
                } catch (CloneNotSupportedException e) {
                    e.printStackTrace();
                }
                System.out.println(threadLocal);
            }}); 
        t.start();
    }
}

输出:

Son@7852e922
java.lang.ThreadLocal@3ffc8195
Son@7852e922
Son@313b781a
java.lang.ThreadLocal@3ffc8195

也就是如果把一个共享的对象直接保存到ThreadLocal中,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。 所以要在保存到ThreadLocal之前,通过克隆或者new来创建新的对象,然后再进行保存。
  ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用。作用:提供一个线程内公共变量(比如本次请求的用户信息),减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度,或者为线程提供一个私有的变量副本,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

如何实现一个线程多个ThreadLocal对象,每一个ThreadLocal对象是如何区分的呢?
查看源码,可以看到:

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
      return nextHashCode.getAndAdd(HASH_INCREMENT);
}

对于每一个ThreadLocal对象,都有一个final修饰的int型的threadLocalHashCode不可变属性,对于基本数据类型,可以认为它在初始化后就不可以进行修改,所以可以唯一确定一个ThreadLocal对象。
  但是如何保证两个同时实例化的ThreadLocal对象有不同的threadLocalHashCode属性:在ThreadLocal类中,还包含了一个static修饰的AtomicInteger([əˈtɒmɪk]提供原子操作的Integer类)成员变量(即类变量)和一个static final修饰的常量(作为两个相邻nextHashCode的差值)。由于nextHashCode是类变量,所以每一次调用ThreadLocal类都可以保证nextHashCode被更新到新的值,并且下一次调用ThreadLocal类这个被更新的值仍然可用,同时AtomicInteger保证了nextHashCode自增的原子性。

为什么不直接用线程id来作为ThreadLocalMap的key?
  这一点很容易理解,因为直接用线程id来作为ThreadLocalMap的key,无法区分放入ThreadLocalMap中的多个value。比如我们放入了两个字符串,你如何知道我要取出来的是哪一个字符串呢?
  而使用ThreadLocal作为key就不一样了,由于每一个ThreadLocal对象都可以由threadLocalHashCode属性唯一区分或者说每一个ThreadLocal对象都可以由这个对象的名字唯一区分(下面的例子),所以可以用不同的ThreadLocal作为key,区分不同的value,方便存取。

public class Son implements Cloneable{
    public static void main(String[] args){
        Thread t = new Thread(new Runnable(){  
            public void run(){
                ThreadLocal<Son> threadLocal1 = new ThreadLocal<>();
                threadLocal1.set(new Son());
                System.out.println(threadLocal1.get());
                ThreadLocal<Son> threadLocal2 = new ThreadLocal<>();
                threadLocal2.set(new Son());
                System.out.println(threadLocal2.get());
            }}); 
        t.start();
    }
}

ThreadLocal的内存泄露问题

根据上面Entry方法的源码,我们知道ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:
img

如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄露。
  
ThreadLocalMap设计时的对上面问题的对策:
ThreadLocalMap的getEntry函数的流程大概为:

  1. 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (table.length-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
  2. 如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry。否则,如果key值为null,则擦除该位置的Entry,并继续向下一个位置查询。在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。
      但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

关于ThreadLocalMap内部类的简单介绍

初始容量16,负载因子2/3,解决冲突的方法是再hash法,也就是:在当前hash的基础上再自增一个常量。

理解

ThreadLocal是什么意思?
ThreadLocal的实例代表了一个线程局部的变量,每条线程都只能看到自己的值,并不会意识到其它的线程中也存在该变量。它采用采用空间来换取时间的方式,解决多线程中相同变量的访问冲突问题。

先来看一段代码

class Data {
    public Integer count = 0;

    public Integer getNumber() {
        return ++count;
    }

}

public class ThreadLocalDemo extends Thread {
    private Data data;

    public ThreadLocalDemo(Data data) {
        this.data = data;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "," + data.getNumber());
        }
    }

    public static void main(String[] args) {
        Data data= new Data();
        ThreadLocalDemo t1 = new ThreadLocalDemo(data);
        ThreadLocalDemo t2 = new ThreadLocalDemo(data);
        t1.start();
        t2.start();
    }
}

在这里插入图片描述

通过这个图可以看到两个线程操作了一个变量,这样在实际情况下是不行的。一个线程在改变变量的时候另外一个线程也在改变这个变量。这样就会出现多线程中相同变量的访问冲突问题。

我们可以通过创建两个实例对象来给变这样
在这里插入图片描述
这种情况解决了相同变量的访问冲突问题。但是我们还可以使用ThreadLocal来解决这个问题。ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal的四个方法:
  void set(Object value)设置当前线程的线程局部变量的值。
  public Object get()该方法返回当前线程所对应的线程局部变量。
  public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
  protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

使用ThreadLocal来改变刚刚的代码

class Data {

   public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
       protected Integer initialValue() {
           return 0;
       };
   };

   public Integer getNumber() {
       int count = threadLocal.get() + 1;
       threadLocal.set(count);
       return count;

   }

}

public class ThreadLocalDemo extends Thread {
   private Data data;

   public ThreadLocalDemo(Data data) {
       this.data = data;
   }

   @Override
   public void run() {
       for (int i = 0; i < 3; i++) {
           System.out.println(Thread.currentThread().getName() + "," + data.getNumber());
       }
   }

   public static void main(String[] args) {
       Data res = new Data();
       ThreadLocalDemo t1 = new ThreadLocalDemo(res);
       ThreadLocalDemo t2 = new ThreadLocalDemo(res);
       t1.start();
       t2.start();
   }
}

这样就可以解决变量冲突。但是我没有搞懂ThreadLocal 与 给每个线程实例传递一个新的变量,这两种做法的区别。如果有小伙伴知道的话可以帮忙告知一下。

ThreadLocal内存溢出的问题和如何避免
 ThreadLocal的原理:Thread内部维护ThreadLocalMap,它其实是一个Map,这个map的Key是一个弱引用也就是ThreadLocal的本身,Value才是真正存储的线程变量Object.而弱引用的生命周期只能存活到下次GC之前

内存泄漏的原因:因为ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,所以当ThreadLocal没有外部强引用来引用的话,那在下次Gc的时候就会被回收。这个时候Key已经被回收了,出现了null Key。也无法根据Null Key 找到Value。如果当前线程生命周期很长的话就会出现一条强引用链:Thread --> ThreadLocalMap–>Entry–>Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。JVM团队也考虑到了这样的情况,所以每次在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程,这样ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。这样就尽量避免了内存泄漏。

 static class Entry extends WeakReference<ThreadLocal<?>> {

            Object value;
         //ThreadLocal为key,真正需要存储的对象为value
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

可以看下具体的源码
1、set()

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

ThreadLocal在调用set方法时,如果 getMap返回的为null,那么表示该线程的 ThreadLocalMap 还没有初始化,所以调用createMap进行初始化:t.threadLocals = new ThreadLocalMap(this, firstValue);

      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);
        }

初始化16的数组,并将firstKey、firstValue存入map。
如果getMap没有返回NULL

 private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
        //定位hash桶的位置
            int i = key.threadLocalHashCode & (len-1);
      //发生hash碰撞时如果碰撞的位置上已经有Entry,且原有的key没有被回收,就查找数组下一个位
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
          //Key存在就替换原来的value值
                if (k == key) {
                    e.value = value;
                    return;
                }
          //key为空就替换并清除过期的Entry
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
      //在空的位置上放入Entry之前先判断是否需要扩容
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

2、get()

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

同样需要根据map是否为空来进行出来,如果没有初始化ThreadLocalMap就会返回setInitialValue()

/**
* setInitialValue方法很简单,定义一个value指向null,如果ThreadLocalMap 不为空,就插入value;如果ThreadLocalMap为空,先调用createMap初始化ThreadLoaclMap,再插入value。最后返回的就是value。
*/

 private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

调用getEntry()

 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);
        }

先定位hash桶的位置,然后根据桶位置找到Entry,如果Entry不为null且相同就返回对应的值。如果不符合调用getEntryAfterMiss()在进行处理

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
//从当前位置向下寻找
            while (e != null) {
                ThreadLocal<?> k = e.get();
//相同就直接返回结果
                if (k == key)
                    return e;
//如果为null调用expungeStaleEntry()处理
                if (k == null)
                    expungeStaleEntry(i);
//继续寻找下一个位置
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
//最后没找到返回NULL
            return null;
        }

expungeStaleEntry()其实就是将Entry删除。防止内存泄漏。但是这样并不能完全保证内存不发生泄漏,如果使用了static的ThreadLocal,延长了生命周期也是有可能导致内存泄漏的。
3、remove()

  private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

也是找到hash桶的位置在遍历找到key然后找到相应的Entry并清理。最后也是调用了expungeStaleEntry()

但是有个问题,为什么key要使用弱引用那?

表面上看导致内存泄漏是因为key使用了弱引用,使Entry的key为null之后没有主动清理value导致的。

其实可以分成两种情况讨论一下

key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key的value就会导致内存泄漏,而不是因为弱引用。

所以综上所述,每次用完ThreadLocal,都调用remove(),清除数据。

ThreadLocal特点

ThreadLocal实现了线程间数据隔离,ThreadLocal的实例代表了一个线程局部的变量,每条线程都只能看到自己的值,并不会意识到其它的线程中也存在该变量。简单来说就是一个公共的Map,map的key是Thread本身,value是线程携带的数据。

ThreadLocal的简单使用

使用方式一

开启三个新的线程,每个线程对数据进行累加。

public class TestThreadLocal {
 
    //线程本地存储变量
    private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
 
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {//启动三个线程
            Thread t = new Thread() {
                @Override
                public void run() {
                    add10ByThreadLocal();
                }
            };
            t.start();
        }
    }
 
    /**
     * 线程本地存储变量加 5
     */
    private static void add10ByThreadLocal() {
        for (int i = 0; i < 5; i++) {
            Integer n = THREAD_LOCAL_NUM.get();
            n += 1;
            THREAD_LOCAL_NUM.set(n);
            System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
        }
    }
 
}

执行结果

Thread-1 : ThreadLocal num=1
Thread-1 : ThreadLocal num=2
Thread-2 : ThreadLocal num=1
Thread-2 : ThreadLocal num=2
Thread-2 : ThreadLocal num=3
Thread-0 : ThreadLocal num=1
Thread-2 : ThreadLocal num=4
Thread-1 : ThreadLocal num=3
Thread-2 : ThreadLocal num=5
Thread-0 : ThreadLocal num=2
Thread-1 : ThreadLocal num=4
Thread-0 : ThreadLocal num=3
Thread-1 : ThreadLocal num=5
Thread-0 : ThreadLocal num=4
Thread-0 : ThreadLocal num=5

每个线程的值都是1~5,没有出现混加。这就实现了每个线程之间的数据的隔离。

使用方式二

开启一个定长为3的线程池,每个线程对数据进行累加。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class TestThreadLocal2 {
 
    //线程本地存储变量
    private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
 
    public static void main(String[] args) {
        ExecutorService cachedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            cachedThreadPool.execute(() -> add10ByThreadLocal());
        }
    }
 
    /**
     * 线程本地存储变量加 5
     */
    private static void add10ByThreadLocal() {
        for (int i = 0; i < 5; i++) {
            Integer n = THREAD_LOCAL_NUM.get();
            n += 1;
            THREAD_LOCAL_NUM.set(n);
            System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
        }
    }
 
}

执行结果

pool-1-thread-1 : ThreadLocal num=1
pool-1-thread-3 : ThreadLocal num=1
pool-1-thread-1 : ThreadLocal num=2
pool-1-thread-2 : ThreadLocal num=1
pool-1-thread-1 : ThreadLocal num=3
pool-1-thread-3 : ThreadLocal num=2
pool-1-thread-1 : ThreadLocal num=4
pool-1-thread-2 : ThreadLocal num=2
pool-1-thread-1 : ThreadLocal num=5
pool-1-thread-3 : ThreadLocal num=3
pool-1-thread-2 : ThreadLocal num=3
pool-1-thread-1 : ThreadLocal num=6
pool-1-thread-3 : ThreadLocal num=4
pool-1-thread-1 : ThreadLocal num=7
pool-1-thread-2 : ThreadLocal num=4
pool-1-thread-1 : ThreadLocal num=8
pool-1-thread-3 : ThreadLocal num=5
pool-1-thread-1 : ThreadLocal num=9
pool-1-thread-2 : ThreadLocal num=5
pool-1-thread-1 : ThreadLocal num=10
pool-1-thread-3 : ThreadLocal num=6
pool-1-thread-1 : ThreadLocal num=11
pool-1-thread-2 : ThreadLocal num=6
pool-1-thread-2 : ThreadLocal num=7
pool-1-thread-1 : ThreadLocal num=12
pool-1-thread-3 : ThreadLocal num=7
pool-1-thread-3 : ThreadLocal num=8
pool-1-thread-3 : ThreadLocal num=9
pool-1-thread-3 : ThreadLocal num=10
pool-1-thread-1 : ThreadLocal num=13
pool-1-thread-2 : ThreadLocal num=8
pool-1-thread-1 : ThreadLocal num=14
pool-1-thread-3 : ThreadLocal num=11
pool-1-thread-1 : ThreadLocal num=15
pool-1-thread-2 : ThreadLocal num=9
pool-1-thread-1 : ThreadLocal num=16
pool-1-thread-3 : ThreadLocal num=12
pool-1-thread-1 : ThreadLocal num=17
pool-1-thread-2 : ThreadLocal num=10
pool-1-thread-1 : ThreadLocal num=18
pool-1-thread-3 : ThreadLocal num=13
pool-1-thread-1 : ThreadLocal num=19
pool-1-thread-2 : ThreadLocal num=11
pool-1-thread-1 : ThreadLocal num=20
pool-1-thread-3 : ThreadLocal num=14
pool-1-thread-3 : ThreadLocal num=15
pool-1-thread-2 : ThreadLocal num=12
pool-1-thread-2 : ThreadLocal num=13
pool-1-thread-2 : ThreadLocal num=14
pool-1-thread-2 : ThreadLocal num=15

问题就出现了,由于线程池的线程是可以重复使用的,所以就出现了数据错乱的现象。所以在合线程池结合使用时,需要注意及时清理线程的数据。

ThreadLocal方法简介

主要方法如下

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
  • get()方法是用来获取ThreadLocal在当前线程中保存的变量副本
  • set()用来设置当前线程中变量的副本
  • remove()用来移除当前线程中变量的副本
  • initialValue()是一个protected方法,一般是用来在使用时进行重写的,如果在没有set的时候就调用get,会调用initialValue方法初始化内容。

源码分析

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

set方法会获取当前的线程,通过当前线程获取ThreadLocalMap对象。然后把需要存储的值放到这个map里面。如果没有就调用createMap创建对象。

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

getMap方法直接返回当前Thread的threadLocals变量,这样说明了之所以说ThreadLocal是线程局部变量就是因为它只是通过ThreadLocal把变量存在了Thread本身而已。

createMap
void createMap(Thread t, T firstValue) {
   t.threadLocals = new ThreadLocalMap(this, firstValue);
}

在set的时候如果不存在threadLocals,直接创建对象。由上看出,放入map的key是当前的ThreadLocal,value是需要存放的内容,所以我们设置属性的时候需要注意存放和获取的是一个ThreadLocal。

get
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

get方法就比较简单,获取当前线程,尝试获取当前线程里面的threadLocals,如果没有获取到就调用setInitialValue方法,setInitialValue基本和set是一样的,就不累累述了。

remove
    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

使用场景

  1. 比如线程中处理一个非常复杂的业务,可能方法有很多,那么,使用 ThreadLocal 可以代替一些参数的显式传递;
  2. 比如用来存储用户 Session。Session 的特性很适合 ThreadLocal ,因为 Session 之前当前会话周期内有效,会话结束便销毁。
  3. 在一些多线程的情况下,如果用线程同步的方式,当并发比较高的时候会影响性能,可以改为 ThreadLocal 的方式,例如高性能序列化框架 Kyro 就要用 ThreadLocal 来保证高性能和线程安全;
  4. 还有像线程内上线文管理器、数据库连接等可以用到 ThreadLocal;

附加

ThreadLocalMap中使用key为ThreadLocal的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap 中使用这个ThreadLocal的key也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。


http://www.jnnr.cn/a/154130.html

相关文章

canvas学习

canvas 是一块画布&#xff0c;可以设置宽高 &#xff0c;默认 300 * 150 使用方式 1. 声明书写 canvas标签 2. 拿到canvas的dom 3. 调用方法 getContext (注意 此方法在prototype上) 方法集合&#xff1a; 填充&#xff1a; 1. fillStyle&#xff0c; 设置填充颜色 &…

Pip版本问题导致Python模块安装失败

文章目录起因解决方案前言输入 %APPDATA%创建 pip.ini终端执行命令安装包结语起因 今天在视频号平台看到有小姐姐直播讲爬虫技术&#xff0c;我一看这不是挺简单的吗&#xff1f;于是我就想到自己四年前的今天刚进大一的时候最开始学的爬虫技术&#xff0c;后来因为一些事情跑…

NeRF-SLAM 学习笔记

NeRF-SLAM: Real-Time Dense Monocular SLAM with Neural Radiance Fields 主页&#xff1a;https://deepai.org/publication/nerf-slam-real-time-dense-monocular-slam-with-neural-radiance-fields 论文&#xff1a;https://arxiv.org/pdf/2210.13641.pdf Code&#xff1a;…

光环:研发云搭建及人才梯队建设——姚冬

摘要&#xff1a;文章内容主要来源于光环国际2022年第三届中国科创者大会姚冬老师的分享&#xff0c;原分享名称为"数字化时代的研发效能建设"。讲述了华为在研发上整套流程规范&#xff0c;通过云的方式去实现人机协同&#xff0c;保持人去做创新型工作。对人才梯队…

矩阵分解算法

文章目录0 前言1. 矩阵分解原理1.1 LFM公式推导LFM损失函数算法关键代码实现1.2 BiasSVD1.3 SVD参考0 前言 在协同过滤算法中 我们知道近邻协同过滤算法的显著缺点&#xff1a; 没有充分利用物品本身的属性信息处理稀疏矩阵能力很弱&#xff0c;泛化能力很弱 为了解决以上问…

SpringBoot整合JSR-303表单校验

JSR-303表单校验 思考一个问题&#xff0c;引出JSR-303 为什么前端做了参数校验&#xff0c;后端还要进行参数校验&#xff1f; 普通用户通过页面操作&#xff0c;前端可以校验住参数的正确性。但如果有人获取到接口&#xff0c;利用接口调用工具比如&#xff1a;postman对后…

1990-2021年全国各省城市化水平数据

1990-2021年全国各省城市化水平数据 1、包括&#xff1a;全国31省 2、时间&#xff1a;1990-2021年 3、指标包括&#xff1a;城镇常住人口数(万人)、常住人口数(万人)、城市化水平 4、指标衡量&#xff1a;城市化率本地区城镇人口/总人口 5、指标解释&#xff1a; 城市化…

vulnhub靶机ha:wordy

靶机下载链接&#xff1a;HA: Wordy ~ VulnHub 靶机ip&#xff1a;192.168.174.136&#xff08;后面重启后变成192.168.174.137&#xff09; kali ip&#xff1a;192.168.174.128 目录 靶机ip发现: 靶机端口扫描: 子目录扫描&#xff1a; wpscan扫描 漏洞利用1 漏洞利…

【仿牛客网笔记】项目进阶,构建安全高效的企业服务——热帖排行

p:投票数 T&#xff1a;发布时间间隔 G&#xff1a;系数&#xff0c;通常为1.5,1.8 计算帖子的分数 注入RedisTemplate 帖子刷新 实现定时任务 刷新帖子 实现更新帖子分数 刷新帖子分数任务 配置Trigger 注释掉定时任务&#xff0c;注释注解就可以 启动服务之…

Python按单元格读取复杂电子表格(Excel)数据实践

Python读取电子表格方法 本文所使用电子表格的目标是读取、解析来自Excel编制的数据报表&#xff0c;或者软件界面导出的数据报表&#xff0c;这类电子表格报表显著特点是有一定的格式&#xff0c;且数据位置不连续&#xff0c;而非标准二维数据表。 关于电子表格&#xff0c…

【JVM】PC程序计数器和PC寄存器

一、JVM体系结构 本文所讲内容主要是 JVM 体系结构图中 运行时数据区 中的 PC寄存器&#xff0c;如下图所示&#xff1a; 二、PC寄存器是什么&#xff1f; 这里引用别人的一句话&#xff1a; 首先这里的PC寄存器并非广义上所指的物理寄存器&#xff0c;或许将其翻译为PC计数…

框架体系——Spring

Spring IOC IOC控制反转 IOC 控制反转&#xff0c;全称Inverse of Control&#xff0c;是一种设计理念由代理人来创建和管理对象&#xff0c;消费者通过代理人来获取对象Ioc的目的是降低对象之间的耦合通过加入Ioc容器将对象统一管理&#xff0c;将对象关联变为弱耦合。 DI…

常见的数据结构基本介绍

文章目录常见的数据结构介绍栈和队列的介绍数组数据结构链表数据结构二叉树和二叉查找树平衡二叉树红黑树结构常见的数据结构介绍 数据结构是计算机底层存储、组织数据的方式。是指数据相互之间是以什么方式排列在一起的。 通常情况下&#xff0c;精心选择的数据结构可以带来更…

Leetcode刷题Day5休息 Day6----------哈希表

Leetcode刷题Day5休息 & Day6----------哈希表 1. 哈希表理论基础 数组、Set、Map 如果数据量小------------数组 如果数据量大------------Set 如果有Key、value------------Map 文章讲解&#xff1a;https://programmercarl.com/%E5%93%88%E5%B8%8C%E8%A1%A8%E7%90%86…

SpringBoot SpringBoot 原理篇 2 自定义starter 2.1 记录系统访客独立IP访问次数案例介绍

SpringBoot 【黑马程序员SpringBoot2全套视频教程&#xff0c;springboot零基础到项目实战&#xff08;spring boot2完整版&#xff09;】 SpringBoot 原理篇 文章目录SpringBootSpringBoot 原理篇2 自定义starter2.1 记录系统访客独立IP访问次数案例介绍2.1.1 介绍2.1.2 需求…

idea,web开发中jsp页面中不提示控制层的请求地址

随着开发的进行&#xff0c;打开spring配置文件会有如下提示 同时工程管理里如下 删掉后&#xff0c;发现打开sping配置文件不告警了&#xff0c;可是jsp页面中也没有了地址的提示 这个提示没有了 正确的做法是删掉Spring Application Context 因为其他配置文件都导入App…

【小5聊】纯javascript实现图片放大镜效果

实现图片放大镜效果&#xff0c;其实就是一个比例放大的效果 以下通过纯javascript方式对图片进行等比例放大&#xff0c;等比倍数和出界判断可自行实现 文章后面会附上全部代码 放大镜效果 1、 放大镜组成 1&#xff09;目标图片&#xff0c;一般是小图 2&#xff09;鼠标移…

Linux学习

一、linux目录结构 /bin [常用] &#xff1a;是Binary的缩写&#xff0c;这个目录存放着最经常使用的命令/sbin&#xff1a;s就是super user的意思&#xff0c;这里存放的是系统管理员使用的系统管理程序/home [常用]&#xff1a;存放普通用户的主目录&#xff0c;在Linux中每…

MySQL 慢查询日志 使用方法浅析 日志定位与优化技巧

目录 前言 1、如何开启使用慢查询日志&#xff1f; 1.1 开启慢查询日志 1.2 设置慢查询阈值 1.3 确定慢查询日志的文件名和路径 1.3.1 查询MySQL数据目录 1.3.2 查询慢查询日志文件名 1.3.3 查询全局设置变量 1.3.4 查询单个变量命令 1.3.5 其他注意事项 2、如何定位并优…

第八章《Java高级语法》第12节:Lambda表达式

Lambda 表达式是 JDK8 的一个新特性,它可以定义大部分的匿名内部类,从而让程序员能写出更优雅的Java代码,尤其在集合的各种操作中可以极大地优化代码结构。 8.12.1 认识Lambda表达式 一个接口的实现类可以被定义为匿名类。经过大量实践,人们发现定义一个接口的匿名实现类…
最新文章