面試必備:ThreadLocal原理解析[精品長文]

基於OpenJDK 12html

本文主要想了解兩個地方:java

  1. ThreadLocal實例看起來是在多個線程共享,但其實是彼此獨立的,這個是怎麼實現的?
  2. ThreadLocal使用不當真的會OOM嗎?若是會,那麼緣由是啥?

先看一下ThreadLocal的官方API解釋爲:git

該類提供了線程局部 (thread-local) 變量。這些變量不一樣於它們的普通對應物,由於訪問某個變量(經過其 get 或 set 方法)的每一個線程都有本身的局部變量,它獨立於變量的初始化副本[原文: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 實例一般是類中的 private static 字段,它們但願將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。github

大概的意思有兩點:api

  • ThreadLocal提供了一種訪問某個變量的特殊方式:訪問到的變量屬於當前線程,即保證每一個線程的變量不同,而同一個線程在任何地方拿到的變量都是一致的,這就是所謂的線程隔離。
  • 若是要使用ThreadLocal,一般定義爲private static類型,在我看來最好是定義爲private static final類型。

看一段代碼:bash

// 代碼來自:
// http://tutorials.jenkov.com/java-concurrency/threadlocal.html
public class ThreadLocalExample {
    public static class MyRunnable implements Runnable {

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

        @Override
        public void run() {
            //注意這裏 set的值是run函數的內部變量,若是是MyRunnable的全局變量
            //則沒法起到線程隔離的做用
            threadLocal.set((int) (Math.random() * 100D));
            try {
                //sleep兩秒的做用是讓thread2 set操做在thread1的輸出以前執行
                //若是線程之間是共用threadLocal,則thread2 set操做會覆蓋掉thread1的set操做
                //從而二者的輸出都是thread2 set的值
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                System.out.println(e);
            }
            System.out.println(threadLocal.get());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable sharedRunnableInstance = new MyRunnable();

        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);

        thread1.start();
        thread2.start();

        thread1.join(); //wait for thread 1 to terminate
        thread2.join(); //wait for thread 2 to terminate
    }
}
複製代碼

輸出結果:微信

thread1 start
thread2 start
38
thread1 join
78
thread2 join
複製代碼

MyRunnable run中sleep兩秒的做用是讓thread2 set操做在thread1的輸出以前執行,若是線程之間是共用threadLocal,則thread2 set操做會覆蓋掉thread1的set操做,二者的輸出都是thread2 set的值,從而輸出的應該是同一個值。oracle

但從代碼執行結果來看,thread一、thread2的threadLocal是不一樣的,也就是實現了線程隔離。dom

ThreadLocal實例在線程間是如何獨立的?

看一眼ThreadLocal set方法:ide

public void set(T value) {
    //currentThread是個native方法,會返回對當前執行線程對象的引用。
    Thread t = Thread.currentThread();
    //getMap 返回線程自身的threadLocals
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //把value set到線程自身的ThreadLocalMap中了
        map.set(this, value);
    } else {
        //線程自身的ThreadLocalMap未初始化,則先初始化,再set
        createMap(t, value);
    }
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
//Thread類中
//ThreadLocalMapset的set方法未執行深拷貝,須要注意傳遞值的類型
ThreadLocal.ThreadLocalMap threadLocals = null;
複製代碼

從代碼中能夠看到,在set的時候,會根據Thread對象的引用來將值添加到各自線程中。但set的值value仍是同一個對象,既然傳遞的是同一個對象,那就涉及到另外一個問題:參數值傳遞、引用傳遞的問題了。

基本類型

public class ThreadLocalExample {
    public static class MyRunnable implements Runnable {

        private ThreadLocal<Object> threadLocal = new ThreadLocal<>();

        // MyRunnable 全局變量
        int random;

        @Override
        public void run() {
            random = (int) (Math.random() * 100D);
            threadLocal.set(random);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                System.out.println(e);
            }

            System.out.println(threadLocal.get());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable sharedRunnableInstance = new MyRunnable();

        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);

        thread1.start();
        System.out.println("thread1 start");
        thread2.start();
        System.out.println("thread2 start");

        thread1.join(); //wait for thread 1 to terminate
        System.out.println("thread1 join");
        thread2.join(); //wait for thread 2 to terminate
        System.out.println("thread2 join");
    }
}
複製代碼

輸出結果:

thread1 start
thread2 start
//兩個值不一樣
16
thread1 join
75
thread2 join
複製代碼

從輸出能夠看出二者隔離了。

引用類型

全局引用

public class ThreadLocalExample {
    public static class MyRunnable implements Runnable {

        private ThreadLocal<Object> threadLocal = new ThreadLocal<>();

        // MyRunnable 全局變量
        Obj obj = new Obj();

        @Override
        public void run() {
            obj.value = (int) (Math.random() * 100D);
            threadLocal.set(obj);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                System.out.println(e);
            }

            System.out.println(((Obj) threadLocal.get()).value);
        }

        class Obj {
            int value;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable sharedRunnableInstance = new MyRunnable();

        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);

        thread1.start();
        System.out.println("thread1 start");
        thread2.start();
        System.out.println("thread2 start");

        thread1.join(); //wait for thread 1 to terminate
        System.out.println("thread1 join");
        thread2.join(); //wait for thread 2 to terminate
        System.out.println("thread2 join");
    }
}
複製代碼

輸出結果:

thread1 start
thread2 start
//兩個值相同
36
36
thread1 join
thread2 join
複製代碼

從輸出結果來看,當set操做的值是MyRunnable的全局變量,而且是引用類型的時候,沒法起到隔離的做用。

局部引用

public class ThreadLocalExample {
    public static class MyRunnable implements Runnable {

        private ThreadLocal<Object> threadLocal = new ThreadLocal<>();

        //Obj obj = new Obj();
        @Override
        public void run() {
            Obj obj = new Obj();
            obj.value = (int) (Math.random() * 100D);
            threadLocal.set(obj);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                System.out.println(e);
            }

            System.out.println(((Obj) threadLocal.get()).value);
        }

        class Obj {
            int value;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable sharedRunnableInstance = new MyRunnable();

        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);

        thread1.start();
        System.out.println("thread1 start");
        thread2.start();
        System.out.println("thread2 start");

        thread1.join(); //wait for thread 1 to terminate
        System.out.println("thread1 join");
        thread2.join(); //wait for thread 2 to terminate
        System.out.println("thread2 join");
    }
}
複製代碼

輸出結果:

thread1 start
thread2 start
//兩個值不一樣
12
19
thread1 join
thread2 join
複製代碼

從輸出結果看,局部引用,能夠相互隔離。

到這裏能夠看出ThreadLocal,只是把set值或引用綁定到了當前線程,但卻沒有進行相應的深拷貝,因此ThreadLocal要想作的線程隔離,必須是基本類型或者run的局部變量。

ThreadLocal OOM ?

看一下ThreadLocalMap內部Entry:

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

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

從代碼中看到,Entry繼承了WeakReference,並將ThreadLocal設置爲了WeakReference,value設置爲強引用。也就是:當沒有強引用指向ThreadLocal變量時,它可被回收。

可是,還有一個問題:ThreadLocalMap維護ThreadLocal變量與具體實例的映射,當ThreadLocal變量被回收後,該映射的key變爲 null,而該Entry仍是在ThreadLocalMap中,從而這些沒法清理的Entry,會形成內存泄漏。

ThreadLocal自帶的remove、set方法,都沒法處理ThreadLocal自身爲null的狀況,由於代碼中都直接取ThreadLocal的threadLocalHashCode屬性了,因此若是ThreadLocal自身已是null,這時調用remove、set會報空指針異常(java.lang.NullPointerException)的。

因此,在使用ThreadLocal的時候,在使用完畢記得remove(remove方法會將Entry的value及Entry自身設置爲null並進行清理)。

JDK 12 ThreadLocal代碼地址: github.com/jiankunking…

我的微信公衆號:

我的github:

github.com/jiankunking

我的博客:

jiankunking.com

相關文章
相關標籤/搜索