分析ThreadLocal如何做到单个线程独享
前情概要
- 我们可能都知道SimpleDateFormat这个类的实例它不是线程安全的,如果不知道,我把代码贴这儿:
// 类的成员变量 protected Calendar calendar; // 这个私有方法会对calendar对象进行赋值,但是没有加锁,在多并发场景下,就造成了问题。 // Called from Format after creating a FieldDelegate private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) { // Convert input date to time field list calendar.setTime(date); // ....其他代码 }
- 我们往往会用ThreadLocal进行解决这种一个线程应该持有一个对象的问题。使用示例代码如下:摘自拉勾教育的《Java 并发编程 78 讲》
public class ThreadLocalDemo06 { public static ExecutorService threadPool = Executors.newFixedThreadPool(16); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { final int finalI = i; threadPool.submit(new Runnable() { @Override public void run() { String date = new ThreadLocalDemo06().date(finalI); System.out.println(Thread.currentThread().getName() + " " + date); } }); } threadPool.shutdown(); } public String date(int seconds) { Date date = new Date(1000 * seconds); SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get(); return dateFormat.format(date); } } class ThreadSafeFormatter { // public final static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal 是不是会更好呢! public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("mm:ss"); } }; }
- 这里使用了dateFormatThreadLocal.get()来保证获取到的一定是一个线程一个。
代码分析
java.lang.ThreadLocal#initialValue
- 这是一个protected 修饰的方法,默认返回null。具体实现交给子类,返回类型T。
protected T initialValue() { return null;}
java.lang.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() { // 1. 获取当前线程的引用 Thread t = Thread.currentThread(); // 2. 获取这个线程内部维护的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { // 3. 以当前的ThreadLocal对象作为key去获取键值对Entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") // 4. 获取值 T result = (T)e.value; return result; } } return setInitialValue(); }
- 从这个方法中可以看到,当已经存在对象时,获取对象分四步走。
- 当这个对象还未存在时,调用setInitialValue()进行初始化。
java.lang.ThreadLocal#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() { T value = initialValue(); // 1. 获取当前线程 Thread t = Thread.currentThread(); // 2. 获取线程中维护的ThreadLocalMap对象。 ThreadLocalMap map = getMap(t); if (map != null) // 3. 将该线程中的map,以ThreadLoacal本身作为key,将Value进行设置。 map.set(this, value); else // 如果map都不存在,则在该线程中创建该map,并进行set value。 // 真是节省空间,都是用到才去创建! createMap(t, value); return value; }
- 从这个方法看到,是这个方法去真正的调用了initialValue方法进行初始化的,所以ThreadLocal的对象获取其实也是一个懒加载的方式,用到了才去进行初始化。
- 也看到了这里是对Thread中的map进行赋值的位置。
小结
- 每个线程的default级别的成员变量ThreadLocal.ThreadLocalMap threadLocals都是由ThreadLocal进行维护的。
- threadLocals 以ThreadLocal对象作为key,保证了一个线程中,同一个ThreadLocal的实例去执行get方法去获取到这个的对象都是唯一的。这也解释了为什么ThreadLocal的对象在示例中使用了static修饰,为了保证它的唯一性,当然,我更乐意在 static 前再加上 final 进行修饰。
Debug实操
- 代码分析看过了,进行debug实际查看一下,就用以上的示例代码。
- 示例代码中,线程池的核心线程数是16,最大线程数也是16.[暂且不论创建线程池的姿势不丝滑],那么在线程启动后,应当:每个线程在执行get()方法前,threadLocals这个map应该为null,执行后,每个线程保证只有一个。
初始化之前
- 第一次执行get时,没有获取到需要进行setInitialValue()。可以观察到this对象的确不存在,他去进行set初始化。
初始化之后
- 第二次这个线程再去获取时,就会去map中获取。
总结
要理清Thread、ThreadLocal、ThreadLocal.ThreadLocalMap三者的关系,结合debug,不难看到它究竟是怎么实现线程独享的一个ThreadLocal对象的。