ThreadLocal解析

starlin 1,081 2018-07-18

ThreadLocal理解

网上有很多常见对于ThreadLocal的介绍,大概如下:

ThreadLocal为解决多线程程序的并发问题提供了一种新的思路
ThreadLocal的目的是为了解决多线程访问资源时的共享问题

既然ThreadLocal它并不是解决多线程共享变量的问题,那么ThreadLocal到底是起什么作用的?
API介绍是这样的:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code 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).

核心意思是:

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景
另外,该场景下,并非必须使用 ThreadLocal ,其它方式完全可以实现同样的效果,只是 ThreadLocal 使得实现更简洁

另外ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每一个线程创建一个单独的变量副本,故每个线程都可以独立的改变,自己所拥有的变量副本,而不会影响其他线程所对应的副本

拿最常用的 Spring 来说,它事务管理的传播机制,就是使用 ThreadLocal 实现的。因为 ThreadLocal 是线程私有的,所以 Spring 的事务传播机制是不能够跨线程的

ThreadLocal和Synchonized对比

ThreadLocal为解决并发编程提供了新的思路,synchronized是共享线程间的数据,而ThreadLocal是隔离线程间的数据

synchronized是利用锁的机制,使变量或代码块在某一时该只能被同一个线程访问。
ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

ThreadLocal源码分析

ThreadLocal定义了四个方法:

  • get():返回此线程局部变量的当前线程副本中的值
  • initialValue():返回此线程局部变量的当前线程的“初始值”
  • remove():移除此线程局部变量当前线程的值
  • set(T value):将此线程局部变量的当前线程副本中的值设置为指定值

除了这4个方法,ThreadLocal里面还有一个静态内部类ThreadLocalMap,该内部类才是实现线程隔离机制的关键。ThreadLocalMap提供了一种用键值对方式存储每一个线程变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本。

ThreadLocalMap

ThreadLocalMap其内部利用Entry来实现key-value的存储,如下:
这里有一点要注意的是:涉及到了弱引用WeakReference

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

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

从上面的源码可以看出Entry的key就是就是ThreadLocal,而value就是值

getEntry()方法

在来看看ThreadLocalMap中的getEntry()方法,源码如下:

    /**
     * Get the entry associated with key.  This method
     * itself handles only the fast path: a direct hit of existing
     * key. It otherwise relays to getEntryAfterMiss.  This is
     * designed to maximize performance for direct hits, in part
     * by making this method readily inlinable.
     *
     * @param  key the thread local object
     * @return the entry associated with key, or null if no such
     */
    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);
    }

首先取一个探测数(key的散列值),如果所对应的key就是我们所要找的元素,则返回,否则调用getEntryAfterMiss(),如下:

        /**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

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

这里有一个重要的地方,当key == null时,调用了expungeStaleEntry()方法,该方法用于处理key == null,有利于GC回收,能够有效地避免内存泄漏。

get()方法

THreadLocal中的get方法主要作用是,返回当前线程所对应的线程变量,其源码如下:

    /**
     * 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() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 从当前线程的ThreadLocalMap获取相对应的Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 从 Entry 中取出值即为所需访问的本线程对应的实例
                T result = (T)e.value;
                return result;
            }
        }
        //如果获取到的 Entry 为 null,则通过setInitialValue()方法设置
        //该 ThreadLocal 变量在该线程中对应的具体实例的初始值
        return setInitialValue();
    }


    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

设置初始值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() {
        //通过initialValue方法获取初始值
        T value = initialValue();
        //获取当前线程
        Thread t = Thread.currentThread();
        //拿到该线程对应的 ThreadLocalMap 对象
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //直接将该 ThreadLocal 对象与对应实例初始值的映射添加进该线程的 ThreadLocalMap中
            map.set(this, value);
        else
            //若为 null,则先创建该 ThreadLocalMap 对象再将映射添加其中
            createMap(t, value);
        return value;
    }

这里并不需要考虑 ThreadLocalMap 的线程安全问题。因为每个线程有且只有一个 ThreadLocalMap 对象,并且只有该线程自己可以访问它,其它线程不会访问该 ThreadLocalMap,也即该对象不会在多个线程中共享,也就不存在线程安全的问题。

设置实例set()

除了通过initialValue()方法设置实例的初始值,还可通过 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();
        ////拿到该线程对应的 ThreadLocalMap 对象
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //直接将该 ThreadLocal 对象与对应实例初始值的映射添加进该线程的 ThreadLocalMap中
            map.set(this, value);
        else
            //若为 null,则先创建该 ThreadLocalMap 对象再将映射添加其中
            createMap(t, value);
    }

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

适用场景

ThreadLocal 适用于如下两种场景:

  • 每个线程需要有自己单独的实例
  • 实例需要在多个方法中共享,但不希望被多线程共享

ThreadLocal使用示例

对于 Java Web 应用而言,Session 保存了很多信息。很多时候需要通过 Session 获取信息,有些时候又需要修改 Session 的信息。一方面,需要保证每个线程有自己单独的 Session 实例。另一方面,由于很多地方都需要操作 Session,存在多方法共享 Session 的需求。如果不使用 ThreadLocal,可以在每个线程内构建一个 Session实例,并将该实例在多个方法间传递,如下所示:

package cn.lxh.java.ManyThread.ThreadLocal;

import lombok.Data;

public class SessionHandler {
  @Data
  public static class Session {
    private String id;
    private String user;
    private String status;
  }
  public Session createSession() {
    return new Session();
  }
  public String getUser(Session session) {
    return session.getUser();
  }
  public String getStatus(Session session) {
    return session.getStatus();
  }
  public void setStatus(Session session, String status) {
    session.setStatus(status);
  }
  public static void main(String[] args) {
    new Thread(() -> {
      SessionHandler handler = new SessionHandler();
      Session session = handler.createSession();
      handler.getStatus(session);
      handler.getUser(session);
      handler.setStatus(session, "close");
      handler.getStatus(session);
    }).start();
  }
}

该方法是可以实现需求的。但是每个需要使用 Session 的地方,都需要显式传递 Session 对象,方法间耦合度较高。

这里使用 ThreadLocal 重新实现该功能如下所示:

/**
 * Created by starlin
 * on 2018/7/18 18:03.
 * 使用ThreadLocal
 */
public class SessionHandlerInThreadLocal {
    public static ThreadLocal<Session> session = new ThreadLocal<Session>();
    @Data
    public static class Session {
        private String id;
        private String user;
        private String status;
    }
    public void createSession() {
        session.set(new Session());
    }
    public String getUser() {
        return session.get().getUser();
    }
    public String getStatus() {
        return session.get().getStatus();
    }
    public void setStatus(String status) {
        session.get().setStatus(status);
    }

    public static void main(String[] args) {
        new Thread(() -> {
            SessionHandlerInThreadLocal handler = new SessionHandlerInThreadLocal();
            handler.getStatus();
            handler.getUser();
            handler.setStatus("close");
            handler.getStatus();
        }).start();
    }
}

使用 ThreadLocal 改造后的代码,不再需要在各个方法间传递 Session 对象,并且也非常轻松的保证了每个线程拥有自己独立的实例。

如果单看其中某一点,替代方法很多。比如可通过在线程内创建局部变量可实现每个线程有自己的实例,使用静态变量可实现变量在方法间的共享。但如果要同时满足变量在线程间的隔离与方法间的共享,ThreadLocal再合适不过。

利用ThreadLocal将SimpleDateFormat变成线程安全

众所周知,SimpleDateFormat 是我们经常用到的日期处理类,但它本身不是线程安全的,在多线程运行环境下,会产生很多问题
下面代码演示SimpleDateFormat在多线程环境下安全问题:

public class SimpleDateFormatTest {
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");

    public static void main(String[] args) {
        final SimpleDateFormatTest simpleDateFormatTest = new SimpleDateFormatTest();
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 100; i++) {
            executorService.submit(()->{
                try {
                    System.out.println(simpleDateFormatTest.dateFormat.parse("2020-08-26 18:41:10"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}

运行结果:

Sun Jan 26 18:41:10 CST 2020
Sun Jan 26 18:41:10 CST 2020
Sun Jan 26 18:41:10 CST 2020
Wed Jan 08 18:59:20 CST 2020
Sun Jan 26 18:41:10 CST 2020
Sun Jan 26 18:41:10 CST 2020
Tue Jan 20 18:41:10 CST 1
Sun Jan 26 18:41:10 CST 2020
Sun Jan 27 17:00:10 CST 2802
Sun Jan 26 18:41:10 CST 2020
Sun Jan 26 18:41:08 CST 2020
Sun Jan 26 18:41:10 CST 2020
Thu May 30 13:21:00 CST 2819

可以看到,时间已经错乱了

其中之一的解决方式就是可以使用ThreadLocal局部变量,解决线程安全问题,代码如下:

public class SimpleDateFormatThreadLocalTest {
    ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
        }
    };

    public static void main(String[] args) {
        final SimpleDateFormatThreadLocalTest simpleDateFormatTest = new SimpleDateFormatThreadLocalTest();
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 100; i++) {
            executorService.submit(()->{
                try {
                    System.out.println(simpleDateFormatTest.dateFormat.get().parse("2020-08-26 18:41:10"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}

运行结果:

Sun Jan 26 18:41:10 CST 2020
Sun Jan 26 18:41:10 CST 2020
Sun Jan 26 18:41:10 CST 2020
Sun Jan 26 18:41:10 CST 2020
Sun Jan 26 18:41:10 CST 2020
Sun Jan 26 18:41:10 CST 2020
Sun Jan 26 18:41:10 CST 2020
Sun Jan 26 18:41:10 CST 2020
Sun Jan 26 18:41:10 CST 2020
Sun Jan 26 18:41:10 CST 2020
Sun Jan 26 18:41:10 CST 2020

可以看出时间一致。

以上只是用来说明ThreadLocl可以保证线程安全,当然JDK8已经有了线程安全时间处理类LocalDateTime

ThreadLocal最佳实践

由于线程的生命周期很长,如果我们往ThreadLocal里面set了很大很大的Object对象,虽然set、get等等方法在特定的条件会调用进行额外的清理,但是ThreadLocal被垃圾回收后,在ThreadLocalMap里对应的Entry的键值key会变成null,但是后续在也没有操作set、get等方法了,就无法被回收,进而造成内存泄漏。
所以最佳实践,应该在我们不使用的时候,主动调用remove方法进行清理;
将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而被清除

代码示例如下:

public class ThreadLocalTest {

    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                for (int i = 0; i < 100; i++) {
                    threadLocal.set(i);
                    System.out.println(Thread.currentThread().getName() + "--->" + threadLocal.get());
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                // 主动调用remove方法进行清理
                threadLocal.remove();
            }
        },"thread1").start();

        new Thread(() -> {
            try {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + "--->" + threadLocal.get());
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                // 主动调用remove方法进行清理
                threadLocal.remove();
            }
        },"thread2").start();
    }
}

FastThreadLocal

该类是Netty框架中,在ThreadLocal基础上进行改造而来,那为什么Netty还要创建一个这样的类了?
先来看一下 ThreadLocal 的实现
Thread 类中,有一个成员变量 ThreadLocals,存放了与本线程相关的所有自定义信息。对这个变量的定义在 Thread 类,而操作却在 ThreadLocal 类中
问题就出在 ThreadLocalMap 类上,它虽然叫 Map,但却没有实现 Map 的接口。如下图,ThreadLocalMap 在 rehash 的时候,并没有采用类似 HashMap 的数组+链表+红黑树的做法,它只使用了一个数组,使用开放寻址(遇到冲突,依次查找,直到空闲位置)的方法,这种方式是非常低效的。
ThreadLocalMap中的rehash方法

由于 Netty 对 ThreadLocal 的使用非常频繁,Netty 对它进行了专项的优化。它之所以快,是因为在底层数据结构上做了文章,使用常量下标对元素进行定位,而不是使用JDK 默认的探测性算法。

spring如何处理bean多线程下的并发问题

ThreadLocal天生为解决相同变量的访问冲突问题, 所以这个对于spring的默认单例bean的多线程访问是一个完美的解决方案。spring也确实是用了ThreadLocal来处理多线程下相同变量并发的线程安全问题。

spring 如何保证数据库事务在同一个连接下执行的

要想实现jdbc事务, 就必须是在同一个连接对象中操作, 多个连接下事务就会不可控, 需要借助分布式事务完成。那spring 如何保证数据库事务在同一个连接下执行的呢?

DataSourceTransactionManager 是spring的数据源事务管理器, 它会在你调用getConnection()的时候从数据库连接池中获取一个connection, 然后将其与ThreadLocal绑定, 事务完成后解除绑定。这样就保证了事务在同一连接下完成。

概要源码:
1.事务开始阶段:
org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin->TransactionSynchronizationManager#bindResource->org.springframework.transaction.support.TransactionSynchronizationManager#bindResource
![](https://raw.githubusercontent.com/smartlin/pic/main/_posts/java%E5%B9%B6%E5%8F%91/java%E5%B9%B6%E5%8F%91%E4%B9%8Bthreadlocal.md/5642368830941.png =709x)

2.事务结束阶段:
org.springframework.jdbc.datasource.DataSourceTransactionManager#doCleanupAfterCompletion->TransactionSynchronizationManager#unbindResource->org.springframework.transaction.support.TransactionSynchronizationManager#unbindResource->TransactionSynchronizationManager#doUnbindResource
![](https://raw.githubusercontent.com/smartlin/pic/main/_posts/java%E5%B9%B6%E5%8F%91/java%E5%B9%B6%E5%8F%91%E4%B9%8Bthreadlocal.md/1358433779345.png =702x)

总结

  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题
  • ThreadLocalMap 的 set 方法通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景

# java并发