java线程(六)之ThreadLocal

版权声明:本文是作者在学习与工作中的总结与笔记,如有内容是您的原创,请评论留下链接地址,我会在文章开头声明。 https://blog.csdn.net/usagoole/article/details/89789134

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中元素的键为线程对象,而值对应线程的变量副本。

    扫描二维码关注公众号,回复: 6107225 查看本文章
  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);
         }
    
  4. 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;
        }
    
  5. 从本质来讲,每个线程中都有一个自己的ThreadLocalMap类对象,而这个Map的key就是threadLocal对象,而值就是我们set的值。ThreadLocalMap里的Entry是一个WeakReference<ThreadLocal<?>>。GC的时候会销毁该引用所包裹(引用)的对象,这个threadLocal作为key可能被销毁,但是只要我们定义成他的类不卸载,tl这个强引用就始终引用着这个ThreadLocal的,永远不会被gc掉。

  6. 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)。

猜你喜欢

转载自blog.csdn.net/usagoole/article/details/89789134
今日推荐