[TOC]

ThreadLocal

简介

  1. ThreadLocal不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。

  2. ThreadLocal代表一个线程局部变量,通过把数据放在ThreadLocal中可以让每个线程创建一个该变量的副本,从而避免并发访问的线程安全问题。

  3. ThreadLocal是Thread Local Variable(线程局部(本地)变量)的意思。线程局部变量的功用其实很简单,就是为每一个使用该变量的线程都提供一个变量的副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量一样。

  4. ThreadLocal从另一个角度上解决多线程的并发问题,ThreadLocal将需要并发访问的资源复制多份,每个线程拥有一份资源,每个线程都拥有自己的资源副本

  5. ThreadLocal与同步机制面向问题的领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式,而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源(变量)的竞争,也就不需要对多个线程进行同步了。

  6. 一般情况下,如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制。如果仅仅需要隔离多个线程之间的共享冲突,则使用ThreadLocal

  7. 如果ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。

  8. 将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。

方法

  1. ThreadLocal提供了三个方法
    • T get():返回此线程局部变量中当前线程副本中的值
    • void remove():删除此线程局部变量中当前线程的值.目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
    • void set(T value):设置此线程局部变量中当前线程副本中的值。
    • protected T initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

原理分析

  1. ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。
  2. set分析 获取当前线程,以当前线程作为key获取ThreadLocalMap,ThreadLocalMap的key是ThreadLocal对象。

     public void set(T value) {
             Thread t = Thread.currentThread();
             ThreadLocalMap map = getMap(t);
             if (map != null)
             //每个线程中都有一个自己的ThreadLocalMap类对象
                 map.set(this, value);
             else
                 createMap(t, value);
     }
    
     ThreadLocalMap getMap(Thread t) {
         return t.threadLocals;
     }
    
     void createMap(Thread t, T firstValue) {
         t.threadLocals = new ThreadLocalMap(this, firstValue);
     }
    
  3. get和remove分析

     public T get() {
             Thread t = Thread.currentThread();
               //取得当前线程的ThreadLocalMap实例
             ThreadLocalMap map = getMap(t);
              //如果map不为空,说明该线程已经有了一个ThreadLocalMap实例
             if (map != null) {
                 ThreadLocalMap.Entry e = map.getEntry(this);
                 if (e != null) {
                     @SuppressWarnings("unchecked")
                     T result = (T)e.value;
                     return result;
                 }
             }
             return setInitialValue();
         }
    
         public void remove() {
              //获取当前线程的ThreadLocalMap对象
              ThreadLocalMap m = getMap(Thread.currentThread());
              //如果map不为空,则删除该本地变量的值
              if (m != null)
                  m.remove(this);
          }
    
  1. setInitialValue

      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;
         }
    
  2. 从本质来讲,每个线程中都有一个自己的ThreadLocalMap类对象,而这个Map的key就是threadLocal对象,而值就是我们set的值。ThreadLocalMap里的Entry是一个WeakReference<ThreadLocal<?>>。GC的时候会销毁该引用所包裹(引用)的对象,这个threadLocal作为key可能被销毁,但是只要我们定义成他的类不卸载,tl这个强引用就始终引用着这个ThreadLocal的,永远不会被gc掉。
  3. ThreadLocalMap

          static class ThreadLocalMap {
           //map中的每个节点Entry,其键key是ThreadLocal并且还是弱引用,这也导致了后续会产生内存泄漏问题的原因。
          static class Entry extends WeakReference<ThreadLocal<?>> {
                    Object value;
                    Entry(ThreadLocal<?> k, Object v) {
                        super(k);
                        value = v;
            }
             /**
              * 初始化容量为16,以为对其扩充也必须是2的指数
              */
             private static final int INITIAL_CAPACITY = 16;
             /**
              * 真正用于存储线程的每个ThreadLocal的数组,将ThreadLocal和其对应的值包装为一个Entry。
              */
             private Entry[] table;
    
             //....其他的方法和操作都和map的类似
         }
    

线程池与ThreadLocal

  1. 线程池中的线程是会重用的,如果异步任务使用了ThreadLocal,会出现什么情况呢?
  2. 代码

     public class ThreadPoolProblem {
         static ThreadLocal<AtomicInteger> sequencer = new ThreadLocal<AtomicInteger>() {
    
             @Override
             protected AtomicInteger initialValue() {
                 return new AtomicInteger(0);
             }
         };
         static class Task implements Runnable {
    
             @Override
             public void run() {
                 System.out.println(Thread.currentThread().getName());
                 AtomicInteger s = sequencer.get();
                 int initial = s.getAndIncrement();
                 // 期望初始为0
                 System.out.println(initial);
             }
         }
    
         public static void main(String[] args) {
             ExecutorService executor = Executors.newFixedThreadPool(2);
             executor.execute(new Task());
             executor.execute(new Task());
             executor.execute(new Task());
             executor.shutdown();
         }
     }
    

    对于异步任务而言,期望的初始值应该总是0,但是运行结果

2 1
pool-1-thread-2 pool-1-thread-1
pool-1-thread-1 0
0 pool-1-thread-1
0 1
pool-1-thread-2 pool-1-thread-1
1 2

可以看到第三次运行的结果就不对了(线程被复用了)。因为线程池中的线程在执行完一个任务,执行下一个任务时,其中的ThreadLocal对象并不会被清空欧冠,修改后的值带到下一个异步任务 解决方案

* 第一次使用ThreadLocal对象时,总是先调用set设置初始值,或者如果ThreaLocal重写了initialValue方法,先调用remove
* 使用完ThreadLocal对象后,总是调用其remove方法
* 使用自定义的线程池

第一种

    static class Task implements Runnable {

        @Override
        public void run() {
            sequencer.set(new AtomicInteger(0));
            //或者 sequencer.remove();

            AtomicInteger s = sequencer.get();
            //...
        }
    }

第二种

    static class Task implements Runnable {

        @Override
        public void run() {
            try{
                AtomicInteger s = sequencer.get();
                int initial = s.getAndIncrement();
                // 期望初始为0
                System.out.println(initial);    
            }finally{
                sequencer.remove();
            }
        }
    }

第三种 扩展线程池ThreadPoolExecutor

    protected void beforeExecute(Thread t, Runnable r) { }

在线程池将任务r交给线程t执行之前,会在线程t中先执行beforeExecure,可以在这个方法中重新初始化ThreadLocal。如果知道所有需要初始化的ThreadLocal变量,可以显式初始化,如果不知道,也可以通过反射,重置所有ThreadLocal

  static class MyThreadPool extends ThreadPoolExecutor {
    public MyThreadPool(int corePoolSize, int maximumPoolSize,
            long keepAliveTime, TimeUnit unit,
            BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        try {
            //使用反射清空所有ThreadLocal(使用反射,找到线程中存储ThreadLocal对象的Map变量threadLocals,重置为null)
            Field f = t.getClass().getDeclaredField("threadLocals");
            f.setAccessible(true);
            f.set(t, null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        super.beforeExecute(t, r);
    }
}

内存泄露

  1. 在threadlocal的生命周期中,都存在这些引用. 看下图: 实线代表强引用,虚线代表弱引用. 172259164557.jpg
  2. 每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key.每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.
  3. 所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露.最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的,就可能出现内存泄露。  由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
  4. 我们在使用完ThreadLocal里的对象后最好能手动remove一下,或者至少调用下ThreadLocal.set(null)。

results matching ""

    No results matching ""