您好,登錄后才能下訂單哦!
ThreadLocal是什么
早在JDK 1.2的版本中就提供Java.lang.ThreadLocal,ThreadLocal為解決多線程程序的并發(fā)問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優(yōu)美的多線程程序。
當(dāng)使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應(yīng)的副本。
從線程的角度看,目標(biāo)變量就象是線程的本地變量,這也是類名中“Local”所要表達的意思。
所以,在Java中編寫線程局部變量的代碼相對來說要笨拙一些,因此造成線程局部變量沒有在Java開發(fā)者中得到很好的普及。
ThreadLocal的接口方法
ThreadLocal類接口很簡單,只有4個方法,我們先來了解一下:
值得一提的是,在JDK5.0中,ThreadLocal已經(jīng)支持泛型,該類的類名已經(jīng)變?yōu)門hreadLocal<T>。API方法也相應(yīng)進行了調(diào)整,新版本的API方法分別是void set(T value)、T get()以及T initialValue()。
ThreadLocal是如何做到為每一個線程維護變量的副本的呢?其實實現(xiàn)的思路很簡單:在ThreadLocal類中有一個Map,用于存儲每一個線程的變量副本,Map中元素的鍵為線程對象,而值對應(yīng)線程的變量副本。我們自己就可以提供一個簡單的實現(xiàn)版本:
package com.test; public class TestNum { // ①通過匿名內(nèi)部類覆蓋ThreadLocal的initialValue()方法,指定初始值 private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() { public Integer initialValue() { return 0; } }; // ②獲取下一個序列值 public int getNextNum() { seqNum.set(seqNum.get() + 1); return seqNum.get(); } public static void main(String[] args) { TestNum sn = new TestNum(); // ③ 3個線程共享sn,各自產(chǎn)生序列號 TestClient t1 = new TestClient(sn); TestClient t2 = new TestClient(sn); TestClient t3 = new TestClient(sn); t1.start(); t2.start(); t3.start(); } private static class TestClient extends Thread { private TestNum sn; public TestClient(TestNum sn) { this.sn = sn; } public void run() { for (int i = 0; i < 3; i++) { // ④每個線程打出3個序列值 System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn[" + sn.getNextNum() + "]"); } } } }
通常我們通過匿名內(nèi)部類的方式定義ThreadLocal的子類,提供初始的變量值,如例子中①處所示。TestClient線程產(chǎn)生一組序列號,在③處,我們生成3個TestClient,它們共享同一個TestNum實例。運行以上代碼,在控制臺上輸出以下的結(jié)果:
thread[Thread-0] --> sn[1]
thread[Thread-1] --> sn[1]
thread[Thread-2] --> sn[1]
thread[Thread-1] --> sn[2]
thread[Thread-0] --> sn[2]
thread[Thread-1] --> sn[3]
thread[Thread-2] --> sn[2]
thread[Thread-0] --> sn[3]
thread[Thread-2] --> sn[3]
考察輸出的結(jié)果信息,我們發(fā)現(xiàn)每個線程所產(chǎn)生的序號雖然都共享同一個TestNum實例,但它們并沒有發(fā)生相互干擾的情況,而是各自產(chǎn)生獨立的序列號,這是因為我們通過ThreadLocal為每一個線程提供了單獨的副本。
Thread同步機制的比較
ThreadLocal和線程同步機制相比有什么優(yōu)勢呢?ThreadLocal和線程同步機制都是為了解決多線程中相同變量的訪問沖突問題。
在同步機制中,通過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序慎密地分析什么時候?qū)ψ兞窟M行讀寫,什么時候需要鎖定某個對象,什么時候釋放對象鎖等繁雜的問題,程序設(shè)計和編寫難度相對較大。
而ThreadLocal則從另一個角度來解決多線程的并發(fā)訪問。ThreadLocal會為每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數(shù)據(jù)的訪問沖突。因為每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal。
由于ThreadLocal中可以持有任何類型的對象,低版本JDK所提供的get()返回的是Object對象,需要強制類型轉(zhuǎn)換。但JDK 5.0通過泛型很好的解決了這個問題,在一定程度地簡化ThreadLocal的使用,代碼清單 9 2就使用了JDK 5.0新的ThreadLocal<T>版本。
概括起來說,對于多線程資源共享的問題,同步機制采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
spring使用ThreadLocal解決線程安全問題我們知道在一般情況下,只有無狀態(tài)的Bean才可以在多線程環(huán)境下共享,在Spring中,絕大部分Bean都可以聲明為singleton作用域。就是因為Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全狀態(tài)采用ThreadLocal進行處理,讓它們也成為線程安全的狀態(tài),因為有狀態(tài)的Bean就可以在多線程中共享了。
一般的Web應(yīng)用劃分為展現(xiàn)層、服務(wù)層和持久層三個層次,在不同的層中編寫對應(yīng)的邏輯,下層通過接口向上層開放功能調(diào)用。在一般情況下,從接收請求到返回響應(yīng)所經(jīng)過的所有程序調(diào)用都同屬于一個線程,如圖9‑2所示:
通通透透理解ThreadLocal
同一線程貫通三層這樣你就可以根據(jù)需要,將一些非線程安全的變量以ThreadLocal存放,在同一次請求響應(yīng)的調(diào)用線程中,所有關(guān)聯(lián)的對象引用到的都是同一個變量。
下面的實例能夠體現(xiàn)Spring對有狀態(tài)Bean的改造思路:
代碼清單3 TestDao:非線程安全
package com.test; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; public class TestDao { private Connection conn;// ①一個非線程安全的變量 public void addTopic() throws SQLException { Statement stat = conn.createStatement();// ②引用非線程安全變量 // … } }
由于①處的conn是成員變量,因為addTopic()方法是非線程安全的,必須在使用時創(chuàng)建一個新TopicDao實例(非singleton)。下面使用ThreadLocal對conn這個非線程安全的“狀態(tài)”進行改造:
代碼清單4 TestDao:線程安全
package com.test; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; public class TestDaoNew { // ①使用ThreadLocal保存Connection變量 private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>(); public static Connection getConnection() { // ②如果connThreadLocal沒有本線程對應(yīng)的Connection創(chuàng)建一個新的Connection, // 并將其保存到線程本地變量中。 if (connThreadLocal.get() == null) { Connection conn = getConnection(); connThreadLocal.set(conn); return conn; } else { return connThreadLocal.get();// ③直接返回線程本地變量 } } public void addTopic() throws SQLException { // ④從ThreadLocal中獲取線程對應(yīng)的Connection Statement stat = getConnection().createStatement(); } }
不同的線程在使用TopicDao時,先判斷connThreadLocal.get()是否是null,如果是null,則說明當(dāng)前線程還沒有對應(yīng)的Connection對象,這時創(chuàng)建一個Connection對象并添加到本地線程變量中;如果不為null,則說明當(dāng)前的線程已經(jīng)擁有了Connection對象,直接使用就可以了。這樣,就保證了不同的線程使用線程相關(guān)的Connection,而不會使用其它線程的Connection。因此,這個TopicDao就可以做到singleton共享了。
當(dāng)然,這個例子本身很粗糙,將Connection的ThreadLocal直接放在DAO只能做到本DAO的多個方法共享Connection時不發(fā)生線程安全問題,但無法和其它DAO共用同一個Connection,要做到同一事務(wù)多DAO共享同一Connection,必須在一個共同的外部類使用ThreadLocal保存Connection。
ConnectionManager.java
package com.test; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class ConnectionManager { private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { @Override protected Connection initialValue() { Connection conn = null; try { conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/test", "username", "password"); } catch (SQLException e) { e.printStackTrace(); } return conn; } }; public static Connection getConnection() { return connectionHolder.get(); } public static void setConnection(Connection conn) { connectionHolder.set(conn); } }
java.lang.ThreadLocal<T>的具體實現(xiàn)
那么到底ThreadLocal類是如何實現(xiàn)這種“為每個線程提供不同的變量拷貝”的呢?先來看一下ThreadLocal的set()方法的源碼是如何實現(xiàn)的:
/** * 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 map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
在這個方法內(nèi)部我們看到,首先通過getMap(Thread t)方法獲取一個和當(dāng)前線程相關(guān)的ThreadLocalMap,然后將變量的值設(shè)置到這個ThreadLocalMap對象中,當(dāng)然如果獲取到的ThreadLocalMap對象為空,就通過createMap方法創(chuàng)建。
線程隔離的秘密,就在于ThreadLocalMap這個類。ThreadLocalMap是ThreadLocal類的一個靜態(tài)內(nèi)部類,它實現(xiàn)了鍵值對的設(shè)置和獲取(對比Map對象來理解),每個線程中都有一個獨立的ThreadLocalMap副本,它所存儲的值,只能被當(dāng)前線程讀取和修改。ThreadLocal類通過操作每一個線程特有的ThreadLocalMap副本,從而實現(xiàn)了變量訪問在不同線程中的隔離。因為每個線程的變量都是自己特有的,完全不會有并發(fā)錯誤。還有一點就是,ThreadLocalMap存儲的鍵值對中的鍵是this對象指向的ThreadLocal對象,而值就是你所設(shè)置的對象了。
為了加深理解,我們接著看上面代碼中出現(xiàn)的getMap和createMap方法的實現(xiàn):
/** * 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; } /** * 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 * @param map the map to store. */ void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
接下來再看一下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 map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); }
再來看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(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }
獲取和當(dāng)前線程綁定的值時,ThreadLocalMap對象是以this指向的ThreadLocal對象為鍵進行查找的,這當(dāng)然和前面set()方法的代碼是相呼應(yīng)的。
進一步地,我們可以創(chuàng)建不同的ThreadLocal實例來實現(xiàn)多個變量在不同線程間的訪問隔離,為什么可以這么做?因為不同的ThreadLocal對象作為不同鍵,當(dāng)然也可以在線程的ThreadLocalMap對象中設(shè)置不同的值了。通過ThreadLocal對象,在多線程中共享一個值和多個值的區(qū)別,就像你在一個HashMap對象中存儲一個鍵值對和多個鍵值對一樣,僅此而已。
小結(jié)
ThreadLocal是解決線程安全問題一個很好的思路,它通過為每個線程提供一個獨立的變量副本解決了變量并發(fā)訪問的沖突問題。在很多情況下,ThreadLocal比直接使用synchronized同步機制解決線程安全問題更簡單,更方便,且結(jié)果程序擁有更高的并發(fā)性。
ConnectionManager.java
package com.test; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class ConnectionManager { private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { @Override protected Connection initialValue() { Connection conn = null; try { conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/test", "username", "password"); } catch (SQLException e) { e.printStackTrace(); } return conn; } }; public static Connection getConnection() { return connectionHolder.get(); } public static void setConnection(Connection conn) { connectionHolder.set(conn); } }
后記
看到網(wǎng)友評論的很激烈,甚至關(guān)于ThreadLocalMap不是ThreadLocal里面的,而是Thread里面的這種評論都出現(xiàn)了,于是有了這個后記,下面先把jdk源碼貼上,源碼最有說服力了。
/** * ThreadLocalMap is a customized hash map suitable only for * maintaining thread local values. No operations are exported * outside of the ThreadLocal class. The class is package private to * allow declaration of fields in class Thread. To help deal with * very large and long-lived usages, the hash table entries use * WeakReferences for keys. However, since reference queues are not * used, stale entries are guaranteed to be removed only when * the table starts running out of space. */ static class ThreadLocalMap {...}
源碼就是以上,這源碼自然是在ThreadLocal里面的,有截圖為證。
本文是自己在學(xué)習(xí)ThreadLocal的時候,一時興起,深入看了源碼,思考了此類的作用、使用范圍,進而聯(lián)想到對傳統(tǒng)的synchronize共享變量線程安全的問題進行比較,而總結(jié)的博文,總結(jié)一句話就是一個是鎖機制進行時間換空間,一個是存儲拷貝進行空間換時間。
以上所述是小編給大家介紹的Java 中的ThreadLocal,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對億速云網(wǎng)站的支持!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。