ThreadLocal,直譯爲「線程本地」或「本地線程」,若是你真的這麼認爲,那就錯了!其實,它就是一個容器,用於存放線程的局部變量,我認爲應該叫作 ThreadLocalVariable(線程局部變量)纔對,真不理解爲何當初 Sun 公司的工程師這樣命名。java
早在 JDK 1.2 的時代,java.lang.ThreadLocal 就誕生了,它是爲了解決多線程併發問題而設計的,只不過設計得有些難用,因此至今沒有獲得普遍使用。其實它仍是挺有用的,不相信的話,咱們一塊兒來看看這個例子吧。mysql
一個序列號生成器的程序,可能同時會有多個線程併發訪問它,要保證每一個線程獲得的序列號都是自增的,而不能相互干擾。程序員
先定義一個接口:sql
public interface Sequence { int getNumber(); }
每次調用 getNumber() 方法可獲取一個序列號,下次再調用時,序列號會自增。數據庫
再作一個線程類:設計模式
public class ClientThread extends Thread { private Sequence sequence; public ClientThread(Sequence sequence) { this.sequence = sequence; } @Override public void run() { for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + " => " + sequence.getNumber()); } } }
在線程中連續輸出三次線程名與其對應的序列號。安全
咱們先不用 ThreadLocal,來作一個實現類吧。多線程
public class SequenceA implements Sequence { private static int number = 0; public int getNumber() { number = number + 1; return number; } public static void main(String[] args) { Sequence sequence = new SequenceA(); ClientThread thread1 = new ClientThread(sequence); ClientThread thread2 = new ClientThread(sequence); ClientThread thread3 = new ClientThread(sequence); thread1.start(); thread2.start(); thread3.start(); } }
序列號初始值是0,在 main() 方法中模擬了三個線程,運行後結果以下:併發
Thread-0 => 1
Thread-0 => 2
Thread-0 => 3
Thread-2 => 4
Thread-2 => 5
Thread-2 => 6
Thread-1 => 7
Thread-1 => 8
Thread-1 => 9ide
因爲線程啓動順序是隨機的,因此並非0、一、2這樣的順序,這個好理解。爲何當 Thread-0 輸出了一、二、3以後,而 Thread-2 卻輸出了四、五、6呢?線程之間居然共享了 static 變量!這就是所謂的「非線程安全」問題了。
那麼如何來保證「線程安全」呢?對應於這個案例,就是說不一樣的線程可擁有本身的 static 變量,如何實現呢?下面看看另一個實現吧。
public class SequenceB implements Sequence { private static ThreadLocal<Integer> numberContainer = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return 0; } }; public int getNumber() { numberContainer.set(numberContainer.get() + 1); return numberContainer.get(); } public static void main(String[] args) { Sequence sequence = new SequenceB(); ClientThread thread1 = new ClientThread(sequence); ClientThread thread2 = new ClientThread(sequence); ClientThread thread3 = new ClientThread(sequence); thread1.start(); thread2.start(); thread3.start(); } }
經過 ThreadLocal 封裝了一個 Integer 類型的 numberContainer 靜態成員變量,而且初始值是0。再看 getNumber() 方法,首先從 numberContainer 中 get 出當前的值,加1,隨後 set 到 numberContainer 中,最後將 numberContainer 中 get 出當前的值並返回。
是否是很噁心?可是很強大!確實稍微饒了一下,咱們不妨把 ThreadLocal 當作是一個容器,這樣理解就簡單了。因此,這裏故意用 Container 這個單詞做爲後綴來命名 ThreadLocal 變量。
運行結果如何呢?看看吧。
Thread-0 => 1
Thread-0 => 2
Thread-0 => 3
Thread-2 => 1
Thread-2 => 2
Thread-2 => 3
Thread-1 => 1
Thread-1 => 2
Thread-1 => 3
每一個線程相互獨立了,一樣是 static 變量,對於不一樣的線程而言,它沒有被共享,而是每一個線程各一份,這樣也就保證了線程安全。 也就是說,TheadLocal 爲每個線程提供了一個獨立的副本!
搞清楚 ThreadLocal 的原理以後,有必要總結一下 ThreadLocal 的 API,其實很簡單。
爲何 initialValue() 方法是 protected 的呢?就是爲了提醒程序員們,這個方法是要大家來實現的,請給這個線程局部變量一個初始值吧。
瞭解了原理與這些 API,其實想一想 ThreadLocal 裏面不就是封裝了一個 Map 嗎?本身均可以寫一個 ThreadLocal 了,嘗試一下吧。
public class MyThreadLocal<T> { private Map<Thread, T> container = Collections.synchronizedMap(new HashMap<Thread, T>()); public void set(T value) { container.put(Thread.currentThread(), value); } public T get() { Thread thread = Thread.currentThread(); T value = container.get(thread); if (value == null && !container.containsKey(thread)) { value = initialValue(); container.put(thread, value); } return value; } public void remove() { container.remove(Thread.currentThread()); } protected T initialValue() { return null; } }
以上徹底山寨了一個 ThreadLocal,其中中定義了一個同步 Map(爲何要這樣?請讀者自行思考),代碼應該很是容易讀懂。
下面用這 MyThreadLocal 再來實現一把看看。
public class SequenceC implements Sequence { private static MyThreadLocal<Integer> numberContainer = new MyThreadLocal<Integer>() { @Override protected Integer initialValue() { return 0; } }; public int getNumber() { numberContainer.set(numberContainer.get() + 1); return numberContainer.get(); } public static void main(String[] args) { Sequence sequence = new SequenceC(); ClientThread thread1 = new ClientThread(sequence); ClientThread thread2 = new ClientThread(sequence); ClientThread thread3 = new ClientThread(sequence); thread1.start(); thread2.start(); thread3.start(); } }
以上代碼其實就是將 ThreadLocal 替換成了 MyThreadLocal,僅此而已,運行效果和以前的同樣,也是正確的。
其實 ThreadLocal 能夠單獨成爲一種設計模式,就看你怎麼看了。
ThreadLocal 具體有哪些使用案例呢?
我想首先要說的就是:經過 ThreadLocal 存放 JDBC Connection,以達到事務控制的能力。
注意:當您在一個類中使用了 static 成員變量的時候,必定要多問問本身,這個 static 成員變量須要考慮「線程安全」嗎?(也就是說,多個線程須要獨享本身的 static 成員變量嗎?)若是須要考慮,那就請用 ThreadLocal 吧!
仍是保持我一向的 Style,用一個 Demo 來講話吧。用戶提出一個需求:當修改產品價格的時候,須要記錄操做日誌,何時作了什麼事情。
想必這個案例,只要是作過應用系統的小夥伴們,都應該遇到過吧?無外乎數據庫裏就兩張表:product 與 log,用兩條 SQL 語句應該能夠解決問題:
update product set price = ? where id = ? insert into log (created, description) values (?, ?)
But!要確保這兩條 SQL 語句必須在同一個事務裏進行提交,不然有可能 update 提交了,但 insert 卻沒有提交。若是這樣的事情真的發生了,咱們確定會被用戶指着鼻子狂罵:「爲何產品價格改了,卻看不到何時改的呢?」。
聰明的我在接到這個需求之後,是這樣作的:
首先,我寫一個 DBUtil 的工具類,封裝了數據庫的經常使用操做:
public class DBUtil { // 數據庫配置 private static final String driver = "com.mysql.jdbc.Driver"; private static final String url = "jdbc:mysql://localhost:3306/demo"; private static final String username = "root"; private static final String password = "root"; // 定義一個數據庫鏈接 private static Connection conn = null; // 獲取鏈接 public static Connection getConnection() { try { Class.forName(driver); conn = DriverManager.getConnection(url, username, password); } catch (Exception e) { e.printStackTrace(); } return conn; } // 關閉鏈接 public static void closeConnection() { try { if (conn != null) { conn.close(); } } catch (Exception e) { e.printStackTrace(); } } }
裏面搞了一個 static 的 Connection,這下子數據庫鏈接就好操做了,牛逼吧!
而後,我定義了一個接口,用於給邏輯層來調用:
public interface ProductService { void updateProductPrice(long productId, int price); }
根據用戶提出的需求,我想這個接口徹底夠用了。根據 productId 去更新對應 Product 的 price,而後再插入一條數據到 log 表中。
其實業務邏輯也不太複雜,因而我快速地完成了 ProductService 接口的實現類:
public class ProductServiceImpl implements ProductService { private static final String UPDATE_PRODUCT_SQL = "update product set price = ? where id = ?"; private static final String INSERT_LOG_SQL = "insert into log (created, description) values (?, ?)"; public void updateProductPrice(long productId, int price) { try { // 獲取鏈接 Connection conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 關閉自動提交事務(開啓事務) // 執行操做 updateProduct(conn, UPDATE_PRODUCT_SQL, productId, price); // 更新產品 insertLog(conn, INSERT_LOG_SQL, "Create product."); // 插入日誌 // 提交事務 conn.commit(); } catch (Exception e) { e.printStackTrace(); } finally { // 關閉鏈接 DBUtil.closeConnection(); } } private void updateProduct(Connection conn, String updateProductSQL, long productId, int productPrice) throws Exception { PreparedStatement pstmt = conn.prepareStatement(updateProductSQL); pstmt.setInt(1, productPrice); pstmt.setLong(2, productId); int rows = pstmt.executeUpdate(); if (rows != 0) { System.out.println("Update product success!"); } } private void insertLog(Connection conn, String insertLogSQL, String logDescription) throws Exception { PreparedStatement pstmt = conn.prepareStatement(insertLogSQL); pstmt.setString(1, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date())); pstmt.setString(2, logDescription); int rows = pstmt.executeUpdate(); if (rows != 0) { System.out.println("Insert log success!"); } } }
代碼的可讀性還算不錯吧?這裏我用到了 JDBC 的高級特性 Transaction 了。暗自慶幸了一番以後,我想是否是有必要寫一個客戶端,來測試一下執行結果是否是我想要的呢? 因而我偷懶,直接在 ProductServiceImpl 中增長了一個 main() 方法:
public static void main(String[] args) { ProductService productService = new ProductServiceImpl(); productService.updateProductPrice(1, 3000); }
我想讓 productId 爲 1 的產品的價格修改成 3000。因而我把程序跑了一遍,控制檯輸出:
Update product success!
Insert log success!
應該是對了。做爲一名專業的程序員,爲了萬無一失,我必定要到數據庫裏在看看。沒錯!product 表對應的記錄更新了,log 表也插入了一條記錄。這樣就能夠將 ProductService 接口交付給別人來調用了。
幾個小時過去了,QA 妹妹開始罵我:「我靠!我才模擬了 10 個請求,你這個接口怎麼就掛了?說是數據庫鏈接關閉了!」。
聽到這樣的叫聲,讓我渾身打顫,立馬中斷了個人小視頻,趕忙打開 IDE,找到了這個 ProductServiceImpl 這個實現類。好像沒有 Bug 吧?但我如今不敢給她任何迴應,我確實有點怕她的。
我忽然想起,她是用工具模擬的,也就是模擬多個線程了!那我本身也能夠模擬啊,因而我寫了一個線程類:
public class ClientThread extends Thread { private ProductService productService; public ClientThread(ProductService productService) { this.productService = productService; } @Override public void run() { System.out.println(Thread.currentThread().getName()); productService.updateProductPrice(1, 3000); } }
我用這線程去調用 ProduceService 的方法,看看是否是有問題。此時,我還要再修改一下 main() 方法:
// public static void main(String[] args) { // ProductService productService = new ProductServiceImpl(); // productService.updateProductPrice(1, 3000); // } public static void main(String[] args) { for (int i = 0; i < 10; i++) { ProductService productService = new ProductServiceImpl(); ClientThread thread = new ClientThread(productService); thread.start(); } }
我也模擬 10 個線程吧,我就不信那個邪了!
運行結果然的讓我很暈、很暈:
Thread-1
Thread-3
Thread-5
Thread-7
Thread-9
Thread-0
Thread-2
Thread-4
Thread-6
Thread-8
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after connection closed.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:411)
at com.mysql.jdbc.Util.getInstance(Util.java:386)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1015)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:989)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:975)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:920)
at com.mysql.jdbc.ConnectionImpl.throwConnectionClosedException(ConnectionImpl.java:1304)
at com.mysql.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:1296)
at com.mysql.jdbc.ConnectionImpl.commit(ConnectionImpl.java:1699)
at com.smart.sample.test.transaction.solution1.ProductServiceImpl.updateProductPrice(ProductServiceImpl.java:25)
at com.smart.sample.test.transaction.ClientThread.run(ClientThread.java:18)
我靠!居然在多線程的環境下報錯了,果真是數據庫鏈接關閉了。怎麼回事呢?我陷入了沉思中。因而我 Copy 了一把那句報錯信息,在百度、Google,還有 OSC 裏都找了,解答實在是千奇百怪。
我忽然想起,既然是跟 Connection 有關係,那我就將主要精力放在檢查 Connection 相關的代碼上吧。是否是 Connection 不該該是 static 的呢?我當初設計成 static 的主要是爲了讓 DBUtil 的 static 方法訪問起來更加方便,用 static 變量來存放 Connection 也提升了性能啊。怎麼搞呢?
因而我看到了 OSC 上很是火爆的一篇文章《ThreadLocal 那點事兒》,終於才讓我明白了!原來要使每一個線程都擁有本身的鏈接,而不是共享同一個鏈接,不然線程1有可能會關閉線程2的鏈接,因此線程2就報錯了。必定是這樣!
我趕忙將 DBUtil 給重構了:
public class DBUtil { // 數據庫配置 private static final String driver = "com.mysql.jdbc.Driver"; private static final String url = "jdbc:mysql://localhost:3306/demo"; private static final String username = "root"; private static final String password = "root"; // 定義一個用於放置數據庫鏈接的局部線程變量(使每一個線程都擁有本身的鏈接) private static ThreadLocal<Connection> connContainer = new ThreadLocal<Connection>(); // 獲取鏈接 public static Connection getConnection() { Connection conn = connContainer.get(); try { if (conn == null) { Class.forName(driver); conn = DriverManager.getConnection(url, username, password); } } catch (Exception e) { e.printStackTrace(); } finally { connContainer.set(conn); } return conn; } // 關閉鏈接 public static void closeConnection() { Connection conn = connContainer.get(); try { if (conn != null) { conn.close(); } } catch (Exception e) { e.printStackTrace(); } finally { connContainer.remove(); } } }
我把 Connection 放到了 ThreadLocal 中,這樣每一個線程之間就隔離了,不會相互干擾了。
此外,在 getConnection() 方法中,首先從 ThreadLocal 中(也就是 connContainer 中) 獲取 Connection,若是沒有,就經過 JDBC 來建立鏈接,最後再把建立好的鏈接放入這個 ThreadLocal 中。能夠把 ThreadLocal 看作是一個容器,一點不假。
一樣,我也對 closeConnection() 方法作了重構,先從容器中獲取 Connection,拿到了就 close 掉,最後從容器中將其 remove 掉,以保持容器的清潔。
這下應該行了吧?我再次運行 main() 方法:
Thread-0
Thread-2
Thread-4
Thread-6
Thread-8
Thread-1
Thread-3
Thread-5
Thread-7
Thread-9
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
我去!總算是解決了,QA 妹妹,你應該會對我微笑一下吧?
感謝您的關注,分享是一種快樂,也但願獲得您的支持與批評!
注意:該示例僅用於說明 TheadLocal 的基本用法。在實際工做中,推薦使用鏈接池來管理數據庫鏈接。示例中的代碼僅做參考,使用前請酌情考慮。