1. 程式人生 > >從一個簡單的例子去理解Threadocal的工作原理

從一個簡單的例子去理解Threadocal的工作原理

示例程式碼

如下程式碼示例主要是演示多執行緒場景下SimpleDataFomate類(SimpleDataFormate類是非執行緒安全的)的使用!,通過該例子去理解ThreadLocal的工作原理

import java.text.SimpleDateFormat;

public class Main {

    public static void main(String[] args) {
        //使用lambda表示式的語法糖
        new Thread( ()-> runTest(1)).start();
        new Thread( ()-> runTest(1)).start();

        new Thread( ()-> runTest(2)).start();
        new Thread( ()-> runTest(2)).start();

    }

    public static void runTest(int mode) {
        for (int i = 0; i < 2; i++) {
            try {
                long times = System.currentTimeMillis();
                System.out.println(String.format("當前在[tid=%s]執行緒,格式轉化輸出 %s", Thread.currentThread().getId(),
                        mode == 1
                                ? TimeFormatUtils.formatTimeWithHHmmPattern(times) : TimeFormatUtils.formatTimeWithHHmmssPattern(times)));
                Thread.sleep(1000 * 3);
                Thread.sleep(1000 * 5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class TimeFormatUtils {

    /**
     * 全域性變數,整個執行時存在,在載入TimeFormatUtils類時初始化,sDataFormatThreadLocal 是一個存放SimpleDateFormat例項的容器
     * 每個執行緒都存放自己的SimpleDateFormat例項
     */
    private static ThreadLocal<SimpleDateFormat> sDataFormatThreadLocal = new ThreadLocal<>();


    /**
     * 全域性變數,整個執行時存在,在載入TimeFormatUtils類時初始化,sDataFormatThreadLoca2l 是一個存放SimpleDateFormat例項的容器
     * 每個執行緒都存放自己的SimpleDateFormat例項
     */
    private static ThreadLocal<SimpleDateFormat> sDataFormatThreadLocal2 = new ThreadLocal<>();


    //同理可以建立多個ThreadLocal<T>的例項,並存放T例項(各個執行緒有各自的T例項)

    public static SimpleDateFormat getFormatterWithHHmm() {
        /**
         * 這裡獲取當前執行緒的SimpleDateFormat的例項,如果沒有建立一個新的例項,並存儲在當前執行緒的ThreadLocalMap中
         */
        SimpleDateFormat format = sDataFormatThreadLocal.get();
        if (format == null) {
            format = new SimpleDateFormat("HH-mm");
            log(String.format("沒有對應的SimpleDateFormat例項(HH:mm), 建立例項之,物件地址為 = %s, 物件toString() = %s",
                    System.identityHashCode(format), format.toString()));
            sDataFormatThreadLocal.set(format);
        } else {
            log(String.format("已經有對應的SimpleDateFormat例項(HH:mm), 物件地址為 = %s",System.identityHashCode(format)));
        }
        return format;
    }

    public static void log(String info) {
        System.out.println(String.format("當前在[tid=%s]執行緒,%s", Thread.currentThread().getId(), info));
    }

    public static String formatTimeWithHHmmPattern(long times) {
        return getFormatterWithHHmm().format(times);
    }

    public static SimpleDateFormat getFormatterWithHHmmss() {
        /**
         * 這裡獲取當前執行緒的SimpleDateFormat的例項,如果沒有建立一個新的例項,並存儲在當前執行緒的ThreadLocalMap中
         */
        SimpleDateFormat format = sDataFormatThreadLocal2.get();
        if (format == null) {
            format = new SimpleDateFormat("HH:mm:ss");
            log(String.format("沒有對應的SimpleDateFormat例項, 建立例項(hh:mm:ss)之,物件地址為 = %s, 物件toString() = %s",
                    System.identityHashCode(format), format.toString()));
            sDataFormatThreadLocal2.set(format);
        }
        log(String.format("已經有對應的SimpleDateFormat例項(hh:mm:ss), 物件地址為 = %s", System.identityHashCode(format)));
        return format;
    }

    public static String formatTimeWithHHmmssPattern(long times) {
        return getFormatterWithHHmmss().format(times);
    }

}

程式碼輸出

/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/bin/java "-javaagent:/Applications/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=55068:/Applications/IntelliJ IDEA CE.app/Contents/bin" -Dfile.encoding=UTF-8 -classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/lib/tools.jar:/Users/luogw/temp/ThreadLocalDemo/out/production/ThreadLocalDemo Main
當前在[tid=11]執行緒,沒有對應的SimpleDateFormat例項(HH:mm), 建立例項之,物件地址為 = 709397301, 物件toString() = 
[email protected]
當前在[tid=14]執行緒,沒有對應的SimpleDateFormat例項, 建立例項(hh:mm:ss)之,物件地址為 = 2004765498, 物件toString() = [email protected] 當前在[tid=13]執行緒,沒有對應的SimpleDateFormat例項, 建立例項(hh:mm:ss)之,物件地址為 = 1557412645, 物件toString() = [email protected] 當前在[tid=12]執行緒,沒有對應的SimpleDateFormat例項(HH:mm), 建立例項之,物件地址為 = 1732199956, 物件toString() =
[email protected]
當前在[tid=13]執行緒,已經有對應的SimpleDateFormat例項(hh:mm:ss), 物件地址為 = 1557412645 當前在[tid=14]執行緒,已經有對應的SimpleDateFormat例項(hh:mm:ss), 物件地址為 = 2004765498 當前在[tid=13]執行緒,格式轉化輸出 19:24:34 當前在[tid=12]執行緒,格式轉化輸出 19-24 當前在[tid=14]執行緒,格式轉化輸出 19:24:34 當前在[tid=11]執行緒,格式轉化輸出 19-24 當前在[tid=13]執行緒,已經有對應的SimpleDateFormat例項(hh:mm:ss), 物件地址為 = 1557412645 當前在[tid=12]執行緒,已經有對應的SimpleDateFormat例項(HH:mm), 物件地址為 = 1732199956 當前在[tid=13]執行緒,格式轉化輸出 19:24:42 當前在[tid=12]執行緒,格式轉化輸出 19-24 當前在[tid=14]執行緒,已經有對應的SimpleDateFormat例項(hh:mm:ss), 物件地址為 = 2004765498 當前在[tid=11]執行緒,已經有對應的SimpleDateFormat例項(HH:mm), 物件地址為 = 709397301 當前在[tid=14]執行緒,格式轉化輸出 19:24:42 當前在[tid=11]執行緒,格式轉化輸出 19-24 Process finished with exit code 0

邏輯說明

如示例程式碼從資料結構與程式碼行為兩部分說明:

  1. 資料結構
    有兩個全域性的ThreadLocal物件,且範型是SimpleDateFormat型別,即可以存放SimpleDataFormat例項的ThreadLocal物件(每個執行緒都可以存放各自的SimpleDataFormat例項)
  2. 程式碼行為
    啟動四個程序,呼叫時間格式化的介面,觸發各個程序去建立自己的SimpleDataFromate物件,並快取在各個程序的ThreadLocalMap中,以供後續使用

程式碼分析

在這裡插入圖片描述
如上圖所示,我們從獲取ThreadLocal變數中的SimpleDataFormat來看看相關的邏輯。即關鍵看sDataFormatThreadLocal的get方法,即ThreadLocal的get方法

 public T get() {
 		//獲取當前執行緒
        Thread t = Thread.currentThread();
        //獲取執行緒的map,key就是當前ThreadLocal物件,value就是ThreadLoal存放的某個類的例項,這個示例程式碼是SimpleDataFormat例項
        ThreadLocalMap map = getMap(t);
        if (map != null) { 
        	//於ThreadLocal例項自己為key,去查詢對應(儲存在自己身上的)SimpleDataFormat例項
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //執行緒第一次執行到這裡時,map是沒有建立的,所以setInitalValue除了初始化執行緒對應的ThrealdLcaoMap後,還需要初始化當前ThrealLocal物件對應的SimpleDataFormat例項
        return setInitialValue();
    }

setInitialValue的程式碼如下

 private T setInitialValue() {
 		//初始化當前ThrealLocal物件對應的SimpleDataFormat例項
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

接下來我們來看看,ThrealLocal物件儲存資料(儲存基對應的SimpleDataFormat)的邏輯,關鍵是ThreadLocal的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();
        //獲取當前執行緒的ThrealLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
        	//把資料保放在map中,即key為ThreadLocal自己,value為其對應的simpleDataFormate例項
            map.set(this, value);
        else
            createMap(t, value);//執行緒第一次呼叫這裡是,map是空的,所以需要建立map在存放資料
    }

總結

  • 每個執行緒都有一個ThreadLocalMap,這個map用來存放ThrealLocal物件對應/關聯的/存放的/繫結的某個Java物件,key就是TheaalLocal物件的地址,value就是其關聯的某個Java物件
  • ThreadLocal物件其實是一個key,用來查詢其關聯的Java物件,是一個容器用來存放Java物件,只不過結合每個執行緒各自的TheadLocalMap物件後,每個執行緒都可以使用同一個ThreadLocal物件去存放不同的例項
  • ThreadLocal主要是空間換時間的思路,各個執行緒的自己的生命週期(上下文)共享一個全域性資料

注意

列印SimpleDataFormat物件地址使用System.identityHashCode方法,是因為SimpleDataFormat重裁了hashCode方法,然後裡邊的實現是呼叫了字串(格式化模板引數)的hast code,所以如下的程式碼輸出日誌中我們看到不同執行緒建立的同一格式化模板引數SimpleDataFormat的“地址”是一樣的(僅看toString的輸出時)