ThreadLocal详解

ThreadLocal是一个关于创建线程局部变量的类。

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。

ThreadLocal是如何为每个线程创建变量的副本的:
  首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
  初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

ThreadLocal详解_第1张图片

以下代码展示了如何创建一个ThreadLocal变量:

    private ThreadLocal myThreadLocal = new ThreadLocal<>();
    @Test
    public void testx() {
        Thread t = new Thread() {
            @Override
            public void run() {
                myThreadLocal.set("icecrea");
                System.out.println(myThreadLocal.get());
            }
        };
        t.start();
        Thread t2 = new Thread() {
            @Override
            public void run() {
                System.out.println("t2------"+myThreadLocal.get());
            }
        };
        t2.start();
    }

我们可以看到,通过这段代码实例化了一个ThreadLocal对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却只能访问到自己通过调用ThreadLocal的set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值,他们仍然无法访问到对方的值。
当然,我们也可以复习方法设置初始值,这样上面的t2线程就会打印出初始值。

    private ThreadLocal myThreadLocal = new ThreadLocal(){
        @Override
        public String initialValue(){
            return "This is the initial value";
        }
    };

源码解读:
ThreadLocal的set方法,分为下面三步:

  • 首先获取当前线程
  • 利用当前线程作为句柄获取一个ThreadLocalMap的对象
  • 如果上述ThreadLocalMap对象不为空,则设置值,否则创建这个ThreadLocalMap对象并设置值

注意: 这里set方法里,第二行获取的是当前线程里的threadlocals这个变量副本,第四行传入了this,即threadlocal对象作为key,之后注入到线程的变量副本里。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。

为什么threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象?因为每个线程中可有多个threadLocal变量。

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

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

同理,ThreadLocal的get方法,
获取当前线程,获取线程持有的ThreadLocalMap,获取值

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

在Thread类中,持有ThreadLocal.ThreadLocalMap的引用变量。实际上ThreadLocal的值是放入了当前线程的一个ThreadLocalMap实例中,所以只能在本线程中访问,其他线程无法访问。

public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

InheritableThreadLocal

是不是说ThreadLocal的值只能被一个线程访问呢?
使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值。
原因是Thread类的Init方法(此处只列相关代码),

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
       ...
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
      ...
    }

可以看出,使用InheritableThreadLocal可以将某个线程的ThreadLocal值在其子线程创建时传递过去。

    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

     private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal key = (ThreadLocal) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }
 
 

下面代码子线程可以访问到父线程中InheritableThreadLocal的值。打印icecrea。(此处如果用threadLocal实例返回的则是Null)

    @Test
    public void testInheritableThreadLocal() {
        final ThreadLocal threadLocal = new InheritableThreadLocal();
        threadLocal.set("icecrea");
        Thread t = new Thread() {
            @Override
            public void run() {
                super.run();
                System.out.println(threadLocal.get());
            }
        };

        t.start();
    }

ThreadLocalMap

ThreadLocal详解_第2张图片

ThreadLocalMap有静态内部类Entry,是ThreadLocal的弱引用类型,持有Object类型的引用。持有Entry[]数组。

 /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */

  static class Entry extends WeakReference> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

   private Entry[] table;

构造方法如下。通过ThreaLocal和Object值来构造ThreadLocalMap,再回顾上面的ThreadLocal的get方法,就是通过获取ThreadLocalMap,在调用它的getEntry方法,计算HASH值,定位Entry在table数组中的位置返回,获取value的值。

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

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


ThreadLocal会内存泄露么

内存泄漏的定义:对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。

threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露. 最好的做法是将调用threadlocal的remove方法.


ThreadLocal详解_第3张图片

  每个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回收.
  所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。

Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。

对于单独的java文件,要如下设置(参数不能放在Test后面)
java -Xms64m -Xmx256m Test


ThreadLocal详解_第4张图片

Linux tomcat下:
在/usr/local/apache-tomcat-5.5.23/bin目录下的catalina.sh添加:JAVA_OPTS='-Xms512m -Xmx1024m'要加“m”说明是MB,否则就是KB了,在启动tomcat时会报内存不足。

初始堆大小-Xms64m
最大堆大小 -Xmx256m

不知道springboot下如何设置tomcat?试了下面这个和类似的都没成功
mvn spring-boot:run -DXms=64m -DXmx=256m
我的解决方案是:mvn package, 然后java -jar -Xms64m -Xmx256m xxx.war 这样设置成功
测试代码如下:

    private ThreadLocal> buffer=new ThreadLocal<>();
    @RequestMapping("threadLocal")
    @ResponseBody
    public String threadLocal(){
        List list= Lists.newArrayList();
        for(int i=0;i<1024000;i++){
            list.add(String.valueOf(i));
        }
        buffer.set(list);
        return "success";
    }

报错信息如下

Exception in thread "http-nio-8080-exec-4" java.lang.OutOfMemoryError: GC overhead limit exceeded
        at javax.management.ObjectName.quote(ObjectName.java:1832)
        at org.apache.coyote.AbstractProtocol.getName(AbstractProtocol.java:385)
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.register(AbstractProtocol.java:1087)
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:857)
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1459)
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:748)

观察发现,value大内存并没有回收


ThreadLocal详解_第5张图片

解决方案:添加 buffer.remove();方法手动回收Entry,解决了value无法回收的问题。

  /**
         * Remove the entry for key.
         */
        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;
                }
            }
        }

应用场景

解决 数据库连接、Session管理问题
数据库连接如果用常规方式,多线程访问要加锁,要互相等待,降低效率。可以使用threadlocal,线程不用相互等待,且他们之间没有关联。但增加了内存开销。

private static ThreadLocal connectionHolder= new ThreadLocal() {
  public Connection initialValue() {
      return DriverManager.getConnection(DB_URL);
  }
};
 
public static Connection getConnection() {
  return connectionHolder.get();
}
private static final ThreadLocal threadSession = new ThreadLocal();
 
public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}

参考文章:
https://www.cnblogs.com/dolphin0520/p/3920407.html
https://www.cnblogs.com/onlywujun/p/3524675.html

你可能感兴趣的:(ThreadLocal详解)