ThreadLocal類深刻理解

jopen 10年前發布 | 14K 次閱讀 Java開發 ThreadLocal

synchronized這類線程同步的機制可以解決多線程并發問題,在這種解決方案下,多個線程訪問到的,都是同一份變量的內容。為了防止在多線程訪問的過程中,可能會出現的并發錯誤。不得不對多個線程的訪問進行同步,這樣也就意味著,多個線程必須先后對變量的值進行訪問或者修改,這是一種以延長訪問時間來換取線程安全性的策略。

而ThreadLocal類為每一個線程都維護了自己獨有的變量拷貝。每個線程都擁有了自己獨立的一個變量,競爭條件被徹底消除了,那就沒有任何必要對這些線程進行同步,它們也能最大限度的由CPU調度,并發執行。并且由于每個線程在訪問該變量時,讀取和修改的,都是自己獨有的那一份變量拷貝,變量被徹底封閉在每個訪問的線程中,并發錯誤出現的可能也完全消除了。對比前一種方案,這是一種以空間來換取線程安全性的策略。

主要方法如下:

ThreadLocal()
          創建一個線程本地變量。
T get()
          返回此線程局部變量的當前線程副本中的值,如果這是線程第一次調用該方法,則創建并初始化此副本。
protected  T initialValue()
          返回此線程局部變量的當前線程的初始值。最多在每次訪問線程來獲得每個線程局部變量時調用此方法一次,即線程第一次使用 get() 方法訪問變量的時候。如果線程先于 get 方法調用 set(T) 方法,則不會在線程中再調用 initialValue 方法。
 
   若該實現只返回 null;如果程序員希望將線程局部變量初始化為 null 以外的某個值,則必須為 ThreadLocal 創建子類,并重寫此方法。通常,將使用匿名內部類。initialValue 的典型實現將調用一個適當的構造方法,并返回新構造的對象。
 
void remove()
          移除此線程局部變量的值。這可能有助于減少線程局部變量的存儲需求。如果再次訪問此線程局部變量,那么在默認情況下它將擁有其 initialValue。
 
void set(T value)
          將此線程局部變量的當前線程副本中的值設置為指定值。許多應用程序不需要這項功能,它們只依賴于 initialValue() 方法來設置線程局部變量的值。
 
在程序中一般都重寫initialValue方法,以給定一個特定的初始值。

典型實例:
1、Hiberante的Session 工具類HibernateUtil
   這個類是Hibernate官方文檔中HibernateUtil類,用于session管理。
   public class HibernateUtil {
    private static Log log = LogFactory.getLog(HibernateUtil.class);
    private static final SessionFactory sessionFactory;     //定義SessionFactory
 
    static {
        try {
            // 通過默認配置文件hibernate.cfg.xml創建SessionFactory
            sessionFactory = new Configuration().configure().buildSessionFactory();
        } catch (Throwable ex) {
            log.error("初始化SessionFactory失敗!", ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    //創建線程局部變量session,用來保存Hibernate的Session
    public static final ThreadLocal session = new ThreadLocal();
 
    /**
     * 獲取當前線程中的Session
     * @return Session
     * @throws HibernateException
     */
    public static Session currentSession() throws HibernateException {
        Session s = (Session) session.get();
        // 如果Session還沒有打開,則新開一個Session
        if (s == null) {
            s = sessionFactory.openSession();
            session.set(s);         //將新開的Session保存到線程局部變量中
        }
        return s;
    }
    public static void closeSession() throws HibernateException {
        //獲取線程局部變量,并強制轉換為Session類型
        Session s = (Session) session.get();
        session.set(null);
        if (s != null)
            s.close();
    }
}
   在這個類中,由于沒有重寫ThreadLocal的initialValue()方法,則首次創建線程局部變量session其初始值為null,第一次調用currentSession()的時候,線程局部變量的get()方法也為null。因此,對session做了判斷,如果為null,則新開一個 Session,并保存到線程局部變量session中,這一步非常的關鍵,這也是“public static final ThreadLocal session = new ThreadLocal()”所創建對象session能強制轉換為Hibernate Session對象的原因。

2、創建一個Bean,通過不同的線程對象設置Bean屬性,保證各個線程Bean對象的獨立性。

/**
 * 學生
 */
public class Student {
    private int age = 0;   //年齡
 
    public int getAge() {
        return this.age;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
}

/**
 * 多線程下測試程序
 */
public class ThreadLocalDemo implements Runnable {
    //創建線程局部變量studentLocal,在后面你會發現用來保存Student對象
    private final static ThreadLocal studentLocal = new ThreadLocal();
 
    public static void main(String[] agrs) {
        ThreadLocalDemo td = new ThreadLocalDemo();
        Thread t1 = new Thread(td, "a");
        Thread t2 = new Thread(td, "b");
        t1.start();
        t2.start();
    }
 
    public void run() {
        accessStudent();
    }
 
    /**
     * 示例業務方法,用來測試
     */
    public void accessStudent() {
        //獲取當前線程的名字
        String currentThreadName = Thread.currentThread().getName();
        System.out.println(currentThreadName + " is running!");
        //產生一個隨機數并打印
        Random random = new Random();
        int age = random.nextInt(100);
        System.out.println("thread " + currentThreadName + " set age to:" + age);
        //獲取一個Student對象,并將隨機數年齡插入到對象屬性中
        Student student = getStudent();
        student.setAge(age);
        System.out.println("thread " + currentThreadName + " first read age is:" + student.getAge());
        try {
            Thread.sleep(500);
        }
        catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge());
    }
 
    protected Student getStudent() {
        //獲取本地線程變量并強制轉換為Student類型
        Student student = (Student) studentLocal.get();
        //線程首次執行此方法的時候,studentLocal.get()肯定為null
        if (student == null) {
            //創建一個Student對象,并保存到本地線程變量studentLocal中
            student = new Student();
            studentLocal.set(student);
        }
        return student;
    }
}
運行結果:
a is running! 
thread a set age to:76 
b is running! 
thread b set age to:27 
thread a first read age is:76 
thread b first read age is:27 
thread a second read age is:76 
thread b second read age is:27 
可以看到a、b兩個線程age在不同時刻打印的值是完全相同的。這個程序通過妙用ThreadLocal,既實現多線程并發,游兼顧數據的安全性。

3、數據庫連接管理

     

public class ConnectionManager {
06  
07     private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
08         @Override
09         protected Connection initialValue() {
10             Connection conn = null;
11             try {
12                 conn = DriverManager.getConnection(
13                         "jdbc:mysql://localhost:3306/test""username",
14                         "password");
15             catch (SQLException e) {
16                 e.printStackTrace();
17             }
18             return conn;
19         }
20     };
21  
22     public static Connection getConnection() {
23         return connectionHolder.get();
24     }
25  
26     public static void setConnection(Connection conn) {
27         connectionHolder.set(conn);
28     }
29 }

知其所以然

那么到底ThreadLocal類是如何實現這種“為每個線程提供不同的變量拷貝”的呢?先來看一下ThreadLocal的set()方法的源碼是如何實現的:

01     /**
02      * Sets the current thread's copy of this thread-local variable
03      * to the specified value.  Most subclasses will have no need to 
04      * override this method, relying solely on the {@link #initialValue}
05      * method to set the values of thread-locals.
06      *
07      * @param value the value to be stored in the current thread's copy of
08      *        this thread-local.
09      */
10     public void set(T value) {
11         Thread t = Thread.currentThread();
12         ThreadLocalMap map = getMap(t);
13         if (map != null)
14             map.set(this, value);
15         else
16             createMap(t, value);
17     }

沒有什么魔法,在這個方法內部我們看到,首先通過getMap(Thread t)方法獲取一個和當前線程相關的ThreadLocalMap,然后將變量的值設置到這個ThreadLocalMap對象中,當然如果獲取到的 ThreadLocalMap對象為空,就通過createMap方法創建。

線程隔離的秘密,就在于ThreadLocalMap這個類。ThreadLocalMap是ThreadLocal類的一個靜態內部類,它實現了鍵值對的設置和獲取(對比Map對象來理解),每個線程中都有一個獨立的ThreadLocalMap副本,它所存儲的值,只能被當前線程讀取和修改。 ThreadLocal類通過操作每一個線程特有的ThreadLocalMap副本,從而實現了變量訪問在不同線程中的隔離。因為每個線程的變量都是自己特有的,完全不會有并發錯誤。還有一點就是,ThreadLocalMap存儲的鍵值對中的鍵是this對象指向的ThreadLocal對象,而值就是你所設置的對象了。

為了加深理解,我們接著看上面代碼中出現的getMap和createMap方法的實現:

1 ThreadLocalMap getMap(Thread t) {
2         return t.threadLocals;
3     }
1 void createMap(Thread t, T firstValue) {
2         t.threadLocals = new ThreadLocalMap(this, firstValue);
3     }

代碼已經說的非常直白,就是獲取和設置Thread內的一個叫threadLocals的變量,而這個變量的類型就是ThreadLocalMap,這樣進一步驗證了上文中的觀點:每個線程都有自己獨立的ThreadLocalMap對象。打開java.lang.Thread類的源代碼,我們能得到更直觀的證明:

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

那么接下來再看一下ThreadLocal類中的get()方法,代碼是這么說的:

01 /**
02      * Returns the value in the current thread's copy of this
03      * thread-local variable.  If the variable has no value for the
04      * current thread, it is first initialized to the value returned
05      * by an invocation of the {@link #initialValue} method.
06      *
07      * @return the current thread's value of this thread-local
08      */
09     public T get() {
10         Thread t = Thread.currentThread();
11         ThreadLocalMap map = getMap(t);
12         if (map != null) {
13             ThreadLocalMap.Entry e = map.getEntry(this);
14             if (e != null)
15                 return (T)e.value;
16         }
17         return setInitialValue();
18     }
19  
20     /**
21      * Variant of set() to establish initialValue. Used instead
22      * of set() in case user has overridden the set() method.
23      *
24      * @return the initial value
25      */
26     private T setInitialValue() {
27         T value = initialValue();
28         Thread t = Thread.currentThread();
29         ThreadLocalMap map = getMap(t);
30         if (map != null)
31             map.set(this, value);
32         else
33             createMap(t, value);
34         return value;
35     }

這兩個方法的代碼告訴我們,在獲取和當前線程綁定的值時,ThreadLocalMap對象是以this指向的ThreadLocal對象為鍵進行查找的,這當然和前面set()方法的代碼是相呼應的。

進一步地,我們可以創建不同的ThreadLocal實例來實現多個變量在不同線程間的訪問隔離,為什么可以這么做?因為不同的ThreadLocal對象作為不同鍵,當然也可以在線程的ThreadLocalMap對象中設置不同的值了。通過ThreadLocal對象,在多線程中共享一個值和多個值的區別,就像你在一個HashMap對象中存儲一個鍵值對和多個鍵值對一樣,僅此而已。

設置到這些線程中的隔離變量,會不會導致內存泄漏呢?ThreadLocalMap對象保存在Thread對象中,當某個線程終止后,存儲在其中的線程隔離的變量,也將作為Thread實例的垃圾被回收掉,所以完全不用擔心內存泄漏的問題。在多個線程中隔離的變量,光榮的生,合理的死,真是圓滿,不是么?

最后再提一句,ThreadLocal變量的這種隔離策略,也不是任何情況下都能使用的。如果多個線程并發訪問的對象實例只允許,也只能創建那么一個,那就沒有別的辦法了,老老實實的使用同步機制來訪問吧。

總結:

ThreadLocal使用場合主要解決多線程中數據數據因并發產生不一致問題。ThreadLocal為每個線程的中并發訪問的數據提供一個副本,通過訪問副本來運行業務,這樣的結果是耗費了內存,單大大減少了線程同步所帶來性能消耗,也減少了線程并發控制的復雜度。
 
ThreadLocal不能使用原子類型,只能使用Object類型。ThreadLocal的使用比synchronized要簡單得多。
 
ThreadLocal和Synchonized都用于解決多線程并發訪問。但是ThreadLocal與synchronized有本質的區別。 synchronized是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問。而ThreadLocal為每一個線程都提供了變量的副本,使得每個線程在某一時間訪問到的并不是同一個對象,這樣就隔離了多個線程對數據的數據共享。而Synchronized卻正好相反,它用于在多個線程間通信時能夠獲得數據共享。
 
Synchronized用于線程間的數據共享,而ThreadLocal則用于線程間的數據隔離。
 
當然ThreadLocal并不能替代synchronized,它們處理不同的問題域。Synchronized用于實現同步機制,比ThreadLocal更加復雜。

ThreadLocal使用的一般步驟

1、在多線程的類(如ThreadDemo類)中,創建一個ThreadLocal對象threadXxx,用來保存線程間需要隔離處理的對象xxx。
2、在ThreadDemo類中,創建一個獲取要隔離訪問的數據的方法getXxx(),在方法中判斷,若ThreadLocal對象為null時候,應該new()一個隔離訪問類型的對象,并強制轉換為要應用的類型。
3、在ThreadDemo類的run()方法中,通過getXxx()方法獲取要操作的數據,這樣可以保證每個線程對應一個數據對象,在任何時刻都操作的是這個對象。
 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!