javascript
Spring JDBC-Spring事务管理之ThreadLocal基础知识
文章目錄
- 概述
- ThreadLocal是什么
- ThreadLocal的接口方法
- ThreadLocal示例
- 與Thread同步機制的比較
- Spring中使用ThreadLocal解決線程安全問題
概述
Spring通過各種模板類降低了開發者使用各種數據持久技術的難度,這些模板類都是線程安全的。
也就是說,多個DAO可以復用同一個模板實例而不會發生沖突。我們使用模板類訪問底層數據,根據持久化技術的不同,模板類需要綁定數據連接或會話的資源。但這些資源本身是非線程安全的,也就是說它們不能在同一時刻被多個線程共享。雖然模板類通過資源池獲取數據連接或會話,但資源池本身解決的是數據連接或會話的緩存問題,并非數據連接或會話的線程安全問題。
但是這些資源本身卻是非線程安全的。根據傳統的經驗,如果某個對象是非線程安全的話,在多線程的環境下,對于對象的訪問都必須采用同步機制,但是模板類并沒有采用同步機制,因為線程的同步會降低并發性,Spring的模板類就是采用ThreadLocal 解決了在多線程環境下,不采用同步的方式解決了多線程的難題。
ThreadLocal在Spring中發揮著重要作用,在管理request作用域的Bean、事務管理、任務調度、AOP等模塊中都出現了它的身影。 想要了解Spring事務管理的底層技術,必須要攻克ThreadLocal。
ThreadLocal是什么
JDK1.2中提供了java.lang.ThreadLocal,ThreadLocal為解決多線程程序的并發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多線程程序。
ThreadLocal不是一個線程,而是線程的一個本地化對象。
當工作于多線程環境中的對象使用ThreadLocad進行維護的時候,ThreadLocad會為每一個使用這個變量的線程分配一個獨立的變量副本。
所以每一個線程可以獨立的改變自己的副本。而不會影響其他線程所對應的副本。從線程的角度來看,這個變量就像線程專有的本地變量,這也是類名中“Local”所要表達的意思。
InheritableThreadLocal繼承于ThreadLocal,它自動為子線程復制一份從副線程哪里繼承而來的本地變量:在創建子線程時,子線程會接收所有可繼承的線程本地變量的初始值, 當必須將本地線程變量自動傳送給所有創建的子線程時,應盡可能的使用InheritableThreadLocal,而非ThreadLocal。
ThreadLocal的接口方法
ThreadLocal類接口很簡單,只有4個方法,
Java5.0中 ThreadLocal已經支持泛型 。 set、get、initialValue都是泛型
-
void set(T value):設置當前線程的線程局部變量的值
-
public T get():該方法返回當前線程所對應的線程局部變量
-
public void remove():將當前線程局部變量的值刪除,目的是為了減少內存的占用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結束后,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量并不是必須的操作,但它可以加快內存回收的速度;
-
protected T initialValue():返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是為了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,并且僅執行1次。ThreadLocal中的默認實現直接返回一個null。
ThreadLocal是如何做到為每一個線程維護一份獨立的變量副本的呢?其實實現的思路很簡單:在ThreadLocal類中有一個Map,用于存儲每一個線程的變量副本,Map中元素的鍵為線程對象,而值對應線程的變量副本。
來看一個簡單的實現版本:
package com.xgj.dao.threadLocal;import java.util.Collections; import java.util.HashMap; import java.util.Map;/*** * * @ClassName: SimpleThreadLocaByOurSelf* * @Description: ThreadLocal的偽代碼,簡易實現,主要介紹原理* * @author: Mr.Yang* * @date: 2017年9月19日 下午4:58:23*/@SuppressWarnings({ "unchecked", "rawtypes" }) public class SimpleThreadLocalByOurSelf {private Map valueMap = Collections.synchronizedMap(new HashMap());/*** * * @Title: set* * @Description: 鍵為線程對象,值為本線程的變量副本* * @param newValue* * @return: void*/public void set(Object newValue) {valueMap.put(Thread.currentThread(), newValue);}/*** * * @Title: get* * @Description: 如果Map中沒有key ,則設置為初始值null ,否則從map中獲取* * * @return: void*/public void get() {Thread currentThread = Thread.currentThread();Object object = valueMap.get(currentThread);if (object == null && !valueMap.containsKey(currentThread)) {object = initValue();} else {object = valueMap.get(currentThread);}}/*** * * @Title: remove* * @Description: 移除* * * @return: void*/public void remove() {Thread currentThread = Thread.currentThread();valueMap.remove(currentThread);}/*** * * @Title: initValue* * @Description: 初始值* * @return* * @return: Object*/private Object initValue() {return null;} }ThreadLocal示例
通過一個具體的實例了解一下ThreadLocal的具體使用方法。
package com.xgj.dao.threadLocal;public class SeqNumWithThreadLocal {private ThreadLocal<Integer> seqNumLocal = new ThreadLocal<Integer>() {// 通過匿名內部類覆蓋initialValue方法,指定初始值protected Integer initialValue() {return 0;}};// 獲取下一個序列值public int getNextNum() {seqNumLocal.set(seqNumLocal.get() + 1);return seqNumLocal.get();}public static void main(String[] args) {SeqNumWithThreadLocal seqNum = new SeqNumWithThreadLocal();TestThread t1 = new TestThread(seqNum);TestThread t2 = new TestThread(seqNum);TestThread t3 = new TestThread(seqNum);t1.start();t2.start();t3.start();}@SuppressWarnings("unused")static class TestThread extends Thread {SeqNumWithThreadLocal sn = new SeqNumWithThreadLocal();public TestThread(SeqNumWithThreadLocal sn) {this.sn = sn;}@Overridepublic void run() {for (int i = 0; i < 5; i++) { // 每個線程打印 5個值System.out.println("Thread:" + Thread.currentThread().getName()+ ",sn:" + sn.getNextNum());}}}}輸出結果:
Thread:Thread-1,sn:1 Thread:Thread-0,sn:1 Thread:Thread-2,sn:1 Thread:Thread-0,sn:2 Thread:Thread-1,sn:2 Thread:Thread-0,sn:3 Thread:Thread-2,sn:2 Thread:Thread-0,sn:4 Thread:Thread-1,sn:3 Thread:Thread-0,sn:5 Thread:Thread-2,sn:3 Thread:Thread-2,sn:4 Thread:Thread-2,sn:5 Thread:Thread-1,sn:4 Thread:Thread-1,sn:5通過輸出我們可以發現每個線程所產生的序號雖然都共享同一個Sequence Number實例,但它們并沒有發生相互干擾的情況,而是各自產生獨立的序列號,這是因為我們通過ThreadLocal為每一個線程提供了單獨的副本。
與Thread同步機制的比較
ThreadLocal和線程同步機制都是為了解決多線程中相同變量的訪問沖突問題。
在同步機制中,通過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序縝密地分析什么時候對變量進行讀寫,什么時候需要鎖定某個對象,什么時候釋放對象鎖等繁雜的問題,程序設計和編寫難度相對較大。
而ThreadLocal則從另一個角度來解決多線程的并發訪問。ThreadLocal為每一個線程提供一個要訪問對象的獨立的變量副本,從而隔離了多個線程對訪問數據的沖突。因為每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的對象封裝,在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal。
由于ThreadLocal中可以持有任何類型的對象,低版本JDK所提供的get()返回的是Object對象,需要強制類型轉換。但JDK 5.0通過泛型很好的解決了這個問題,在一定程度上簡化ThreadLocal的使用。
概括起來說,對于多線程資源共享的問題,同步機制采用了“以時間換空間”的方式:訪問串行化,對象共享化。而ThreadLocal采用了“以空間換時間”的方式:訪問并行化,對象獨享化。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
Spring中使用ThreadLocal解決線程安全問題
我們知道在一般情況下,只有無狀態的Bean才可以在多線程環境下共享,在Spring中,絕大部分Bean都可以聲明為singleton作用域。就是因為Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全的“狀態性對象”采用ThreadLocal進行封裝,讓它們也成為線程安全的“狀態性對象”,因此有狀態的Bean就能夠以singleton的方式在多線程中正常工作了。
一般的Web應用劃分為展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過接口向上層開放功能調用。在一般情況下,從接收請求到返回響應所經過的所有程序調用都同屬于一個線程.
這樣用戶就可以根據需要,將一些非線程安全的變量以ThreadLocal存放,在同一次請求響應的調用線程中,所有對象所訪問的同一ThreadLocal變量都是當前線程所綁定的。
下面的實例能夠體現Spring對有狀態Bean的改造思路:
public class TopicDao {//一個非線程安全的變量private Connection conn;public void addTopic(){//引用非線程安全變量Statement stat = conn.createStatement();…} }由于conn是非線程安全的成員變量,因此addTopic()方法是非線程安全的,必須每個線程在使用時需要創建一個新TopicDao實例(非singleton)才能保障線程安全性。下面使用ThreadLocal對conn這個非線程安全的“狀態”進行改造:
import java.sql.Connection; import java.sql.Statement; public class TopicDao {//①使用ThreadLocal保存Connection變量private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();public static Connection getConnection(){//②如果connThreadLocal沒有本線程對應的Connection創建一個新的Connection,//并將其保存到線程本地變量中。if (connThreadLocal.get() == null) {Connection conn = ConnectionManager.getConnection();connThreadLocal.set(conn);return conn;}else{//③直接返回線程本地變量return connThreadLocal.get();}}public void addTopic() {//④從ThreadLocal中獲取線程對應的Statement stat = getConnection().createStatement();} }不同的線程在使用TopicDao時,先判斷connThreadLocal.get()是否為null,如果為null,則說明當前線程還沒有對應的Connection對象,這時創建一個Connection對象并添加到本地線程變量中;如果不為null,則說明當前的線程已經擁有了Connection對象,直接使用就可以了。這樣,就保證了不同的線程使用線程相關的Connection,而不會使用其他線程的Connection。因此,這個TopicDao就可以做到singleton共享了。
當然,這個例子本身很粗糙,將Connection的ThreadLocal直接放在Dao只能做到本Dao的多個方法共享Connection時不發生線程安全問題,但無法和其他Dao共用同一個Connection,要做到同一事務多Dao共享同一個Connection,必須在一個共同的外部類使用ThreadLocal保存Connection。但這個實例基本上說明了Spring對有狀態類線程安全化的解決思路。
總結
以上是生活随笔為你收集整理的Spring JDBC-Spring事务管理之ThreadLocal基础知识的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring JDBC-Spring事务
- 下一篇: Spring-AOP 混合使用各种切面类