美文网首页
ThreadLocal源码分析

ThreadLocal源码分析

作者: barry_di | 来源:发表于2019-05-15 18:26 被阅读0次

什么是ThreadLocal

ThreadLocal是一个线程独享的本地存放空间,什么是独享的本地空间,也就是时候每个线程都有一份,
线程之间不能共享该区域。主要用于存储线程变量。

什么情况下会使用ThreadLocal

多线程的情况下,如果存在资源的竞争的时候,我们通常使用锁的机制去处理这种资源的竞争,而对于一些资源
每个线程都要有一份的情况下,我们就会使用ThreadLocal.

例如:

  1. Spring的事务管理,是通过C3P0或者其他的数据库连接池中获取一个Connection,然后将Connection存放每个线程中
    的ThreadLocal中,保证每个线程之间使用的数据库连接都是独享的。我们可以想下,如果每个线程都是使用同一个
    数据连接,我们就无法控制事务的提交和回滚。因此每个线程必须独占这个数据库连接。那如果我们在Dao层
    获取Connection,然后执行SQL提交事务。就不会存在这个问题啦,其实并不是,我们通常会在三层中的服务层进行事务
    的开启和事务的提交、回滚。那么为什么会在服务层进行事务的处理,主要是因为事务的边界问题。
    因为Service会调用一系列的DAO对数据库进行多次操作。多个Dao中我们无法确定到底哪个Dao进行事务的提交。
    因此使用ThreadLocal进行存放每个数据库的连接。
  2. 还有一种场景,在多数据源的情况下,我们可以通过一个key获取对应的数据源,而这个key值
    ,从我们ThreadLocal中获取到对应的数据源。从而隔离多线程下数据源的访问。

ThreadLocal的使用

  • ThreadLocal的使用,我们只需要创建一个ThreadLocal并且指定存放的数据类型即可。
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();

  • ThreadLocal类的方法
 public T get() 获取当前线程的变量
 public void set(T value)  设置当前线程的变量
 public void remove() 删除线程变量

ThreadLocal源码分析

一、ThreadLocal结构分析

ThreadLocal的实现原理,我们可以通过ThreadLocal的get方法进行跟踪分析。

1    public T get() {
2        Thread t = Thread.currentThread();
3        ThreadLocalMap map = getMap(t);
4        if (map != null) {
5            ThreadLocalMap.Entry e = map.getEntry(this);
6            if (e != null) {
7                @SuppressWarnings("unchecked")
8                T result = (T)e.value;
9                return result;
10            }
11        }
12        return setInitialValue();
13    }

从ThreadLocal的get方法中我们可以看出,现获取当前的线程,再通过线程去获取
对应线程的ThreadLocalMap.从ThreadLocalMap的名字,我们可以看出他是一个Map的数据结构。
我们就想ThreadLocalMap是存放在哪里的呢?我们可以通过跟中getMap的方法中看出,ThreadLocalMap
是存放到Thread中。

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

而Thread中的ThreadLocalMap是在ThreadLocal中进行初始化。我可以通过ThreadLocal中的set方法和setInitialValue方法查看到初始化ThreadLocalMap.

 public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, 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;
    }

  void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
image.png
二、ThreadLocalMap
  • 1.存储结构分析
    我可以从ThreadLocalMap的源码进行查看,ThreadLocalMap跟HashMap等都是使用Entry数据结构进行存储.
1    static class ThreadLocalMap {
2          static class Entry extends WeakReference<ThreadLocal<?>> {
3              /** The value associated with this ThreadLocal. */
4              Object value;
5  
6              Entry(ThreadLocal<?> k, Object v) {
7                  super(k);
8                  value = v;
9              }
10          }
11
12        private Entry[] table;

那么如果我们在代码中创建两个ThreadLocal会怎样存储的呢?

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

    public static ThreadLocal<String> threadLocal2 = new ThreadLocal<String>(){
        @Override
        protected String initialValue() {
            return "hello";
        }
    };

通过对setInitialValue方法进行分析发现,我们创建两个ThreadLocal,其实内部是将当前的ThreadLocal对象作为键值,initialValue的返回值做为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;
    }

总结:
我们通过对ThreadLocalMap的set和get方法,可以发现,我们ThreadLocalMap是存Thread中,由ThreadLocal的设置相关方法进行创建。存储的数据结构是以ThreadLocal作为键保存对应的Value到ThreadLocalMap中。


image.png

我们可以通过源码查看到Entry是Key是一个弱引用,而不是像HashMap那样使用强引用。那么ThreadLocalMap中的key为弱引用的,那么ThreadLcaoMap的中的值只能存活到下一次的GC。

弱引用:通常用于存储非必需对象的,弱引用指向的对象实例,在下一次垃圾收集发生之前是存活的。

    1. ThreadLocalMap内存泄漏
      从存储的结构我们可以发现,ThreadLocal是ThreadLocalMap的键值。如果在我们的代码中将 ThreadLocalRef设置为null的情况下,我们就不可以通过ThreadLocal进行访问,而ThreadLocalMap还是会保存Entry,而这样就会造成内存的泄漏。Entry只能等待线程结束后才能进行GC。
import org.junit.Test;


public class UseThreadLocal {
    private static int threadCount = 30;

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

    public static ThreadLocal<String> threadLocal2 = new ThreadLocal<String>(){
        @Override
        protected String initialValue() {
            return "hello";
        }
    };

    class ThreadLocalRunable implements Runnable {

        private int threadId;

        public ThreadLocalRunable(int threadId) {
            this.threadId = threadId;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "-" + Thread.currentThread().getId() + ":start"+"thread id  = "+threadLocal.get());
            threadLocal2 = null;
            Integer threadLocalInt = threadLocal.get();
            threadLocal.set(threadLocalInt+this.threadId);
            System.out.println(Thread.currentThread().getName() + "-" + Thread.currentThread().getId() + ":end" + " thread id=" + threadLocal.get());
        }
    }

    @Test
    public void test() throws InterruptedException {
        Thread[] threads = new Thread[threadCount];
        for (int i = 0 ;i<threadCount;i++){
            threads[i] = new Thread(new ThreadLocalRunable(i),"线程"+i);
        }
        for (Thread t :threads){
            t.start();
            t.join();
        }

    }
}

为了防止内存泄漏,我们可以通过ThreadLocal提供的remove移除ThreadlocalMap中ThreadLocal的Value。

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

  • 3.为什么ThreadLocalMap中的ThreadLocal使用弱引用
    上面我们说过当我们为ThreadLocalRef设置为null的时候,ThreadLocal会被回收,而ThreadLocal被调用的前提是没有被其他引用。那么在ThreadLocalMap中会将ThreadLocal作为键值。如果ThreadLocalMap中使用强引用的话,ThreadLocal就一定不会被回收,就会造成ThreadLocal的内存泄漏,但是ThreadLocalMap使用的是弱引用,那也就是说在下一次GC的时候ThreadLocal就会被回收,而不会造成内存泄漏。

    1. ThreadLocalMap内存溢出
      我们使用多线程是为了提高CPU的使用效率或者将一些耗时的工作由其他线程进行处理。
      那么如果我们线程处理的时间非常耗时,存放在ThreadLocal中的数据非常大,并且存放在Threadlocal中的数据没有释放。JVM为了避免内存溢出,
运行时设置堆内存为1m
-Xms=1m
-Xmx=1m


public class ThreadLocalOOM {
    private static int threadCount = 30;

    public static ThreadLocal<HashMap> threadLocal = new ThreadLocal<HashMap>() {
        @Override
        protected HashMap initialValue(){
            HashMap map= new HashMap<>();
            for (int i=0;i<1000000000;i++){
                map.put(i,i);
            }
            return map;
        }
    };



    class ThreadLocalRunable implements Runnable {

        private int threadId;

        public ThreadLocalRunable(int threadId) {
            this.threadId = threadId;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "-" + Thread.currentThread().getId() + ":start"+"thread id  = "+threadLocal.get());
            System.out.println(Thread.currentThread().getName() + "-" + Thread.currentThread().getId() + ":end" + " thread id=" + threadLocal.get());
        }
    }
    @Test
    public void test() throws InterruptedException {
        Thread[] threads = new Thread[threadCount];
        for (int i = 0 ;i<threadCount;i++){
            threads[i] = new Thread(new ThreadLocalRunable(i),"线程"+i);
        }
        for (Thread t :threads){
            t.start();
            t.join();
        }

    }
}

相关文章

网友评论

      本文标题:ThreadLocal源码分析

      本文链接:https://www.haomeiwen.com/subject/yqumaqtx.html