ThreadLocal的理解與在Spring中的應用

原文:http://neoremind.net/2010/11/threadlocal_learn/

一 引子

首先我們先來看一下Spring框架中是如何使用數據庫模板的。

數據庫表:

CREATE TABLE users
(
id int AUTO_INCREMENT NOT NULL PRIMARY KEY,
name varchar(32) NOT NULL,
password varchar(32) NOT NULL
);

DAO接口:

public interface UsersDao {
public boolean insert(Users users) throws Exception;
public Users select(int id) throws Exception;
public boolean update(Users users) throws Exception;
public boolean delete(int id) throws Exception;
public List selectAll() throws Exception;
public List selectAllByPage(int curPage, int lineSize) throws Exception;
public int getCount() throws Exception;
}

DAOImpl實現類:

import org.hibernate.Query;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
public class UsersDaoImpl extends HibernateDaoSupport implements UsersDao {
public boolean delete(int id) throws Exception {
String hql = "delete from Users where id=:id";// 注意刪除的寫法!
Query q = this.getSession().createQuery(hql);
q.setParameter("id", id);
if (q.executeUpdate() > 0) {
return true;
}
return false;
}
public boolean insert(Users users) throws Exception {
this.getSession().save(users);
return true;
}
public Users select(int id) throws Exception {
String hql = "from Users u where u.id=:id";
Query q = this.getSession().createQuery(hql);
q.setParameter("id", id);
List l = q.list();
if (l.size() > 0) {
return (Users) l.get(0);
}
return null;
}
public boolean update(Users users) throws Exception {
this.getHibernateTemplate().update(users);//注意更新操作
return true;
}
public List selectAll() throws Exception {
List all = null;
String hql = "from Users";
Query q = this.getSession().createQuery(hql);
List l = q.list();
if (l.size() > 0) {
all = l;
}
return all;
}
public List selectAllByPage(int curPage, int lineSize) throws Exception {
List all = null;
String hql = "from Users";
Query q = this.getSession().createQuery(hql);
q.setFirstResult((curPage - 1) * lineSize);
q.setMaxResults(lineSize);
List l = q.list();
if (l.size() > 0) {
all = l;
}
return all;
}
public int getCount() throws Exception {
String hql = "select count(*) from Users";
Query q = this.getSession().createQuery(hql);
if (q.list().size() > 0) {
return (Integer) q.list().get(0);
}
return 0;
}
}

如何調用:

public class Text {
public static void main(String[] args) throws Exception {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UsersDaoImpl userDaoImpl = (UsersDaoImpl) context.getBean("userDaoImpl");
List l = userDaoImpl.selectAllByPage(5, 10);
if (l != null) {
for (Object u : l) {
System.out.println(((Users) u).getId() + ((Users) u).getName());
}
} else {
System.out.println("沒有記錄");
}
System.out.println(userDaoImpl.getCount());
}
}

Spring配置文件applicationContext.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="dataSource"
   class="org.apache.commons.dbcp.BasicDataSource">
   <property name="driverClassName">
    <value>com.mysql.jdbc.Driver</value>
   </property>
   <property name="url">
    <value>jdbc:mysql://127.0.0.1:3306/zzcfront</value>
   </property>
   <property name="username">
    <value>root</value>
   </property>
   <property name="password">
    <value>root</value>
   </property>
</bean>
<bean id="sessionFactory"
   class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
   <property name="dataSource">
    <ref bean="dataSource" />
   </property>
   <property name="hibernateProperties">
    <props>
     <prop key="hibernate.dialect">
      org.hibernate.dialect.MySQLDialect
     </prop>
     <prop key="hibernate.show_sql">true</prop>
    </props>
   </property>
   <property name="mappingResources">
    <list>
     <value>com/ssh/orm/Users.hbm.xml</value>
    </list>
   </property>
</bean>
<bean id="hibernateTemplate"
   class="org.springframework.orm.hibernate3.HibernateTemplate">
   <property name="sessionFactory">
    <ref bean="sessionFactory" />
   </property>
</bean>
<bean id="userDao" class="com.ssh.dao.UsersDao" abstract="true"></bean>
<bean id="userDaoImpl" class="com.ssh.dao.impl.UsersDaoImpl"
   parent="userDao">
   <property name="hibernateTemplate">
    <ref bean="hibernateTemplate" />
   </property>
</bean>
</beans>

二 問題的引出

Spring通過各種DAO模板類降低了研發者使用各種數據持久技術的難度。這些模板類都是線程安全的,也就是說,多個DAO能夠複用同一個模板實例而不會發生衝突。
我們使用模板類訪問底層數據,根據持久化技術的不同,模板類需要綁定數據連接或會話的資源。但這些資源本身是非線程安全的,也就是說他們不能在同一時刻被多個線程共享。
雖然模板類通過資源池獲取數據連接或會話,但資源池本身解決的是數據連接或會話的緩存問題,並非數據連接或會話的線程安全問題。
按照傳統經驗,假如某個對象是非線程安全的,在多線程環境下,對對象的訪問必須採用synchronized進行線程同步。但Spring的DAO模板類並未採用線程同步機制,因爲線程同步限制了併發訪問,會帶來很大的性能損失
此外,通過代碼同步解決性能安全問題挑戰性很大,可能會增強好幾倍的實現難度。那模板類究竟仰丈何種魔法神功,能夠在無需同步的情況下就化解線程安全的難題呢?答案就是ThreadLocal!
ThreadLocal在Spring中發揮着重要的作用,在管理request作用域的Bean、事務管理、任務調度、AOP等模塊都出現了他們的身影,起着舉足輕重的作用。要想了解Spring事務管理的底層技術,ThreadLocal是必須攻克的山頭堡壘。

三 什麼是ThreadLocal變量?

當使用ThreadLocal維護變量時,ThreadLocal爲每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。

從線程的角度看,目標變量就象是線程的本地變量,這也是類名中“Local”所要表達的意思。

線程局部變量並不是Java的新發明,很多語言(如IBM IBM XL FORTRAN)在語法層面就提供線程局部變量。在Java中沒有提供在語言級支持,而是變相地通過ThreadLocal的類提供支持。

所以,在Java中編寫線程局部變量的代碼相對來說要笨拙一些,因此造成線程局部變量沒有在Java開發者中得到很好的普及。

四 ThreadLocal與同步Synchronized的比較與區別

ThreadLocal和線程同步機制都是爲了解決多線程中相同變量的訪問衝突問題

在同步機制中,通過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序慎密地分析什麼時候對變量進行讀寫,什麼時候需要鎖定某個對象,什麼時候釋放對象鎖等繁雜的問題,程序設計和編寫難度相對較大。所有這些都是因爲多個線程共享了資源造成的。

而ThreadLocal則從另一個角度來解決多線程的併發訪問。ThreadLocal會爲每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問衝突。因爲每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal。

由於ThreadLocal中可以持有任何類型的對象,低版本JDK所提供的get()返回的是Object對象,需要強制類型轉換。但JDK 5.0通過泛型很好的解決了這個問題,在一定程度地簡化ThreadLocal的使用,概括起來說,對於多線程資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而後者爲每一個線程都提供了一份變量,因此可以同時訪問而互不影響

ThreadLocal是解決線程安全問題一個很好的思路,它通過爲每個線程提供一個獨立的變量副本解決了變量併發訪問的衝突問題。在很多情況下,ThreadLocal比直接使用synchronized同步機制解決線程安全問題更簡單,更方便,且結果程序擁有更高的併發性。


五 Spring中數據庫模板中是如何使用ThreadLocal的?

Spring使用ThreadLocal解決線程安全問題。一般情況下,只有無狀態的Bean纔可以在多線程環境下共享,在Spring中,絕大部分Bean都可以聲明爲singleton作用域。就是因爲Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全狀態採用ThreadLocal進行處理,讓它們也成爲線程安全的狀態,因爲有狀態的Bean就可以在多線程中共享了。
一般的Web應用劃分爲展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過接口向上層開放功能調用。在一般情況下,從接收請求到返回響應所經過的所有程序調用都同屬於一個線程。
下面的實例能夠體現Spring對有狀態Bean的改造思路:
代碼1 TopicDao:非線程安全
publicclass TopicDao {privateConnection conn;①一個非線程安全的變量
publicvoid addTopic(){Statement stat = conn.createStatement();②引用非線程安全變量
…
}}
由於①處的conn是成員變量,因爲addTopic()方法是非線程安全的,必須在使用時創建一個新TopicDao實例(非singleton)。下面使用ThreadLocal對conn這個非線程安全的“狀態”進行改造:
代碼 TopicDao:線程安全
import java.sql.Connection;
import java.sql.Statement;
                 
public class TopicDao {
①使用ThreadLocal保存Connection變量
private static ThreadLocal connThreadLocal = new ThreadLocal();
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中獲取線程對應的Connection
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。

六 ThreadLocal深入剖析

ThreadLocal的接口方法

ThreadLocal類接口很簡單,只有4個方法,我們先來了解一下:
* void set(Object value)
配置當前線程的線程局部變量的值。
* public Object get()
該方法返回當前線程所對應的線程局部變量。
* public void remove()
將當前線程局部變量的值刪除,目的是爲了減少內存的佔用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結束後,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量並不是必須的操作,但他能夠加快內存回收的速度。
* protected Object initialValue()
返回該線程局部變量的初始值,該方法是個protected的方法,顯然是爲了讓子類覆蓋而設計的。這個方法是個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,並且僅執行1次。ThreadLocal中的缺省實現直接返回一個null。
簡化版JDK中的ThreadLocal類:
public class ThreadLocal
{
private Map values = Collections.synchronizedMap(new HashMap());
public Object get()
{
Thread curThread = Thread.currentThread();
Object o = values.get(curThread);
if (o == null &amp;&amp; !values.containsKey(curThread))
{
o = initialValue();
values.put(curThread, o);
}
return o;
}
public void set(Object newValue)
{
values.put(Thread.currentThread(), newValue);
}
public Object initialValue()
{
return null;
}
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章