【JDK源码】ThreadLocal浅析

一、官方注释

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID). For example, the class below generates unique identifiers local to each thread. A thread's id is assigned the first time it invokes ThreadId.get() and remains unchanged on subsequent calls.

此类提供线程局部变量。 这些变量不同于它们的正常对应变量,因为每个访问一个(通过它的 get 或 set 方法)的线程都有它自己的、独立初始化的变量副本。 ThreadLocal 实例通常是希望将状态与线程相关联的类中的私有静态字段(例如,用户 ID 或事务 ID)。 例如,下面的类生成每个线程本地的唯一标识符。 线程的 id 在第一次调用 ThreadId.get() 时被分配,并且在后续调用中保持不变。

import java.util.concurrent.atomic.AtomicInteger;
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);  
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
 new ThreadLocal<Integer>() {
     @Override protected Integer initialValue() {
         return nextId.getAndIncrement();
 }
};

// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
 return threadId.get();
  }
}

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

只要线程处于活动状态且 ThreadLocal 实例可访问,每个线程都持有对其线程局部变量副本的隐式引用; 在线程消失后,它的所有线程本地实例副本都将受到垃圾回收(除非存在对这些副本的其他引用)。

二、简介

1.什么是ThreadLocal?

​ 我们知道多线程访问共享资源会出现并发问题,特别是多线程进行写数据的情况。那么能不能有这样一个东西,他可以让每个线程都私有自己的数据,这个数据别人是看不到、拿不到的,这样是不是能不用加锁而去解决多线程修改共享资源的并发问题。

ThreadLocal 就是除了加锁这种同步方式以外的一种保证一种规避多线程访问出现线程不安全的方法,当我们创建一个共享变量后,如果每个线程对其进行访问的时候访问都是线程自己的变量就不会存在线程不安全的问题。

​ ThreadLocal 是 JDK包提供的,它提供线程的本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每一个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而避免了线程安全性问题。

2.为什么需要ThreadLocal?有什么好处

比如服务器保存用户的会话信息这一个场景:

​ 浏览器访问服务器是多对一并发的,一个服务器需要处理多个客户端请求,每个客户端的请求服务端会创建一个独立的线程去做处理,也就是说服务器处理客户端请求是一个多线程的环境,如果只是存在一个普通的变量当中是可能引起冲突的,比如A用户的数据可能在B线程中进行了存放。

ThreadLocal就可起到一个每个线程存一份的作用,起到线程隔离作用。

​ 在分布式的环境下,使用session存在共享数据的问题。通常的解决方案,是将共享数据存入数据库,所有的应用服务器都去数据库获取共享数据。对于每一次请求,开始时从数据库里取到数据,然后将其临时存放在本地的内存里,考虑到线程之间的隔离,所以用ThreadLocal,这样在本次请求的过程中,就可以随时获取到这份共享数据了。所以,session的替代方案是数据库,ThreadLocal只是打了个辅助。

3.怎么用ThreadLocal

copy一段之前项目的代码hh

/**
 * 作为容器持有用户信息,用于代替session对象
 */
public class HostHolder {
    private ThreadLocal<User> users = new ThreadLocal<>();//线程隔离
    public void setUser(User user){
        users.set(user);
    }

    public User getUser(){
        return users.get();
    }
    public void clear(){
        users.remove();
    }
}
-------------------------------------------------------------------------------
public class LoginTicketInterceptor implements HandlerInterceptor {

    @Autowired
    private UserService userService;

    @Autowired
    private HostHolder hostHolder;

    //在Controller之前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 从cookie中获取凭证
        String ticket = CookieUtil.getValue(request, "ticket");

        if (ticket != null) {
            // 查询凭证
            LoginTicket loginTicket = userService.findLoginTicket(ticket);
            // 检查凭证是否有效
            if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
                // 根据凭证查询用户
                User user = userService.findUserById(loginTicket.getUserId());
                // 在本次请求中持有用户,访问一个网页就是一个请求,如/login,/register
                hostHolder.setUser(user);
            }
        }
        return true;
    }

    //在Controller之后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        User user = hostHolder.getUser();
        if (user != null && modelAndView != null) {
            modelAndView.addObject("loginUser", user);
        }
    }

    //在TemplateEngine之后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //在线程中把数据清掉,防止内存泄漏
        hostHolder.clear();
    }
}

三、原理

结构图

alt alt

​ 可以看到,一个线程Thread中存在一个ThreadLocalMap,ThreadLocalMap中的key对应ThreadLocal,在此处可见Map可以存储多个key即(ThreadLocal)。另外Value就对应着在ThreadLocal中存储的Value。因此总结出:每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key的键值对。这里解释了为什么每个线程访问同一个ThreadLocal,得到的确是不同的数值。

1.java.lang.Thread

​ ThreadLocal既然是线程独有的,那么他可能就会在一个线程初始化时初始化。我们看Thread类源码可以看到有两个变量threadLocalsinheritableThreadLocals 。两者都是ThreadLocal内部类 ThreadLocalMap 类型的变量。两者都是ThreadLocal内部类 ThreadLocalMap 类型的变量。

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

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  • 这两个变量在线程初始化时都是null,也就是所ThreadLocal是懒加载,我们用到的时候才进行初始化。ThreadLocalMap 实际上类似于一个 HashMap. 这两个变量只有当线程第一次调用ThreadLocal的 set和 get方法的时候才会创建它们。
  • 需要注意的是:每个线程的本地变量不是存放在 new 出来的TheadLocal 实例中,而是存放在调用线程的ThreadLocals 变量中。(前面说过,这个变量在 java.lang.Thread类中)
  • 也就是说,ThreadLocal 类型的本地变量实际是存放在具体的线程空间上,其本身相当于装在本地变量的工具壳,通过 set 方法将value 添加到 threadlocals 变量中,当调用线程的时候能够将它从 threadLocals变量中取出。
  • 如果调用的线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals 中,所以不使用本地变量的时候需要从它的threadLocals 取出变量。 alt

2.set方法

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        //获取当前线程(调用者线程)
        Thread t = Thread.currentThread();
        //以当前线程作为key值,去查找对应的线程变量,找到对应的map
        ThreadLocalMap map = getMap(t);
        if (map != null)
        //如果map不为null,就直接添加本地变量,key为当前线程的引用,值为添加的本地变量值
            map.set(this, value);
        else
        //如果map为null,说明首次添加,需要首先创建出对应的map(懒加载形式)
            createMap(t, value);
    }

	/**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
         //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
        return t.threadLocals;
    }

3.get方法

  • 在get 方法的实现中,首先获取当前调用者线程
    • 如果当前线程的threadLocals 不为 null ,就直接返回当前线程绑定的本地变量值,
    • 否则执行 setInitialValue的方法初始化 threadLocals 变量。
  • setInitialValue 方法中,类似于 set 方法的实现,都是判断当前线程的threadLocals 变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值是null),否则创建的是threadLocals变量,同样添加的是null
/**
* Returns the value in the current thread's copy of this
* thread-local variable.  If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
    //(1)获取当前线程
    Thread t = Thread.currentThread();
    //(2)获取当前线程的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
    return setInitialValue();
}
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
    //protected T initialValue() {return null;}
    T value = initialValue();
    //获取当前线程
    Thread t = Thread.currentThread();
    //以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
    return value;
}

3. remove 方法

  • remove 方法判断该当前线程对应的threadLocals变量是否是null,不是 null 的话就直接删除当前线程中的threadLocals变量。
public void remove() {
    //获取当前线程绑定的threadLocals
     ThreadLocalMap m = getMap(Thread.currentThread());
     //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
     if (m != null)
         m.remove(this);
 }

4ThreadLocalMap

ThreadLocalMap是ThreadLocal的内部静态类,实现了一套自己的Map结构,其很多设计和HashMap类似

4.1成员属性

        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        //初始容量16
        private static final int INITIAL_CAPACITY = 16;
        //散列表
        private Entry[] table;
        //entry 有效数量 
        private int size = 0;
        //负载因子
        private int threshold; 

4.2构造函数

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //初始化table数组,INITIAL_CAPACITY默认值为16
        table = new Entry[INITIAL_CAPACITY];
        //key和16取得哈希值
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //创建节点,设置key-value
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        //设置扩容阈值
        setThreshold(INITIAL_CAPACITY);
}
		
		//默认扩容负载因子2 / 3
		/**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
	

4.3 HashCode 计算

​ ThreaLocalMap中没有采用传统的调用ThreadLocal的hashcode方法(继承自object的hashcode),而是调用nexthashcode

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();

//HASH_INCREMENT = 0x61c88647是一个魔法数,可以减少hash冲突,通过//nextHashCode.getAndAdd(HASH_INCREMENT)方法会转化为二进制数据,主要作用是增加哈希值,减少哈希冲突
private static final int HASH_INCREMENT = 0x61c88647; 
private static int nextHashCode() {
      return nextHashCode.getAndAdd(HASH_INCREMENT);
}

Hash冲突

和HashMap的最大的不同在于,ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1及线性探测,寻找下一个相邻的位置。

/**
 * Increment i modulo len.
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}
/**
 * Decrement i modulo len.
 */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}
ThreadLocalMap使用**线性探测法**解决哈希冲突的。Entry[] table可以看作是一个环形数组。但是解决Hash冲突的效率很低,如有大量不同的ThreadLocal对象放入map中时发送冲突。所以建议每个线程只存一个变量(一个ThreadLocal)就不存在Hash冲突的问题,如果一个线程要保存set多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

4.4 set

  private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
          
            //与运算 & (len-1) 和HashMap一样的设计,容量需要是2的整数幂
            //如果是hashmap 计算完下标后 会增加链表 或红黑树的查找计算量 
            int i = key.threadLocalHashCode & (len-1);
            
            // 从下标位置开始向后循环搜索  不会死循环  有扩容因子 必定有空余槽点
            for (Entry e = tab[i];   e != null;  e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
               //如果key是相同,则替换,并return
                if (k == key) {
                    e.value = value;
                    return;
                }
                //e!=null,key==null,因为key是弱引用,
                //所以key已经被gc回收了,replaceStaleEntry方法就是用来解决内存泄露问题
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			//槽点为空 设置value
            tab[i] = new Entry(key, value);
            //设置ThreadLocal数量
            int sz = ++size;
			//没有可清理的槽点 并且数量大于负载因子 rehash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

4.5 getEntry

 	private Entry getEntry(ThreadLocal<?> key) {
        //计算下标位置
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        //没有hash冲突,entry存在,并且key未被回收
        if (e != null && e.get() == key)
            return e;
        else
            //hash冲突,通过线性探测查找,可能查询到
            return getEntryAfterMiss(key, i, e);
    }

    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
		//循环查找,直到为null
        while (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == key)
                return e;
            if (k == null)
                //被回收了,清除
                expungeStaleEntry(i);
            else
                //循环下一个
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

4.6 resize

private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

4.7 rehash

rehash()方法:先进行一次全表扫描清理,清理过期的Entry。清理之后长度大于等于原长度的1/2就会进行2倍扩容。

/**
*Re-pack and/or re-size the table. First scan the entire table removing stale entries. If * this doesn't sufficiently shrink the size of the table, double the table size.
*/
private void rehash() {
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }
全部评论

相关推荐

点赞 评论 收藏
分享
11-05 07:29
贵州大学 Java
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务