ThreadLocal原理
线程程序介绍
早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
Threadlocal变量
ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
从线程的角度看,目标变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。
线程局部变量并不是Java的新发明,很多语言(如IBMIBM XL FORTRAN)在语法层面就提供线程局部变量。在Java中没有提供在语言级支持,而是变相地通过ThreadLocal的类提供支持。
所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。
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。
值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中定义了一个ThreadLocalMap,每一个Tread中都有一个该类型的变量——threadLocals——用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。
基本概念
为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。使用场景
To keep state with a thread (user-id,transaction-id, logging-id) To cache objects which you need frequentlyThreadLocal类
它主要由四个方法组成initialValue(),get(),set(T),remove(),其中值得注意的是initialValue(),该方法是一个protected的方法,显然是为了子类重写而特意实现的。该方法返回当前线程在该线程局部变量的初始值,这个方法是一个延迟调用方法,在一个线程第1次调用get()或者set(Object)时才执行,并且仅执行1次。ThreadLocal中的确实实现直接返回一个null:
举例
ThreadLocal的原理
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。比如下面的示例实现:
public class ThreadLocal
{
private Map values = Collections.synchronizedMap(newHashMap());
public Object get()
{
Thread curThread = Thread.currentThread();
Object o = values.get(curThread);
if (o == null &&!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;
}
}
使用方法
ThreadLocal 的使用
使用方法一:
Hibernate的文档时看到了关于使ThreadLocal管理多线程访问的部分。具体代码如下
1. public static final ThreadLocal session =new ThreadLocal();
2. public static Session currentSession() {
3. Session s = (Session)session.get();
4. //open a new session,if this session hasnone
5. if(s == null){
6. s = sessionFactory.openSession();
7. session.set(s);
8. }
return s;
9. }
我们逐行分析
1。 初始化一个ThreadLocal对象,ThreadLocal有三个成员方法 get()、set()、initialvalue()。
如果不初始化initialvalue,则initialvalue返回null。
3。session的get根据当前线程返回其对应的线程内部变量,也就是我们需要的net.sf.hibernate.Session(相当于对应每个数据库连接).多线程情况下共享数据库链接是不安全的。ThreadLocal保证了每个线程都有自己的s(数据库连接)。
5。如果是该线程初次访问,自然,s(数据库连接)会是null,接着创建一个Session,具体就是行6。
6。创建一个数据库连接实例s
7。保存该数据库连接s到ThreadLocal中。
8。如果当前线程已经访问过数据库了,则从session中get()就可以获取该线程上次获取过的连接实例。
使用方法二
当要给线程初始化一个特殊值时,需要自己实现ThreadLocal的子类并重写该方法,通常使用一个内部匿名类对ThreadLocal进行子类化,EasyDBO中创建jdbc连接上下文就是这样做的:
public class JDBCContext{
private static Logger logger =Logger.getLogger(JDBCContext.class);
private DataSource ds;
protected Connection connection;
private boolean isValid = true;
private static ThreadLocal jdbcContext;
private JDBCContext(DataSource ds){
this.ds = ds;
createConnection();
}
public static JDBCContextgetJdbcContext(javax.sql.DataSource ds)
{
if(jdbcContext==null)jdbcContext=newJDBCContextThreadLocal(ds);
JDBCContext context = (JDBCContext)jdbcContext.get();
if (context == null) {
context = new JDBCContext(ds);
}
return context;
}
private static class JDBCContextThreadLocalextends ThreadLocal {
public javax.sql.DataSource ds;
publicJDBCContextThreadLocal(javax.sql.DataSource ds)
{
this.ds=ds;
}
protected synchronized Object initialValue(){
return new JDBCContext(ds);
}
}
}
简单的实现版本
代码清单1 SimpleThreadLocal
public class SimpleThreadLocal {
private Map valueMap =Collections.synchronizedMap(new HashMap());
public void set(Object newValue) {
valueMap.put(Thread.currentThread(),newValue);①键为线程对象,值为本线程的变量副本
}
public Object get() {
Thread currentThread =Thread.currentThread();
Object o = valueMap.get(currentThread);②返回本线程对应的变量
if (o == null &&!valueMap.containsKey(currentThread)) {③如果在Map中不存在,放到Map
中保存起来。
o = initialValue();
valueMap.put(currentThread, o);
}
return o;
}
public void remove() {
valueMap.remove(Thread.currentThread());
}
public Object initialValue() {
return null;
}
}
虽然代码清单9‑3这个ThreadLocal实现版本显得比较幼稚,但它和JDK所提供的ThreadLocal类在实现思路上是相近的。
举例
下面,我们通过一个具体的实例了解一下ThreadLocal的具体使用方法。
代码清单2 SequenceNumber
package com.baobaotao.basic;
public class SequenceNumber {
①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
private static ThreadLocal seqNum = newThreadLocal(){
public Integer initialValue(){
return 0;
}
};
②获取下一个序列值
public int getNextNum(){
seqNum.set((Integer)seqNum.get()+1);
return (Integer)seqNum.get();
}
public static void main(String[] args)
{
SequenceNumber sn = new SequenceNumber();
③ 3个线程共享sn,各自产生序列号
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 extendsThread
{
private SequenceNumber sn;
public TestClient(SequenceNumber 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()+"]");
}
}
}
}
分析
通常我们通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量值,如例子中①处所示。TestClient线程产生一组序列号,在③处,我们生成3个TestClient,它们共享同一个SequenceNumber实例。运行以上代码,在控制台上输出以下的结果:
thread[Thread-2] sn[1]
thread[Thread-0] sn[1]
thread[Thread-1] sn[1]
thread[Thread-2] sn[2]
thread[Thread-0] sn[2]
thread[Thread-1] sn[2]
thread[Thread-2] sn[3]
thread[Thread-0] sn[3]
thread[Thread-1] sn[3]
考察输出的结果信息,我们发现每个线程所产生的序号虽然都共享同一个SequenceNumber实例,但它们并没有发生相互干扰的情况,而是各自产生独立的序列号,这是因为我们通过ThreadLocal为每一个线程提供了单独的副本。
说明
在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。但在有些情况下,synchronized不能保证多线程对共享变量的正确读写。例如类有一个类变量,该类变量会被多个类方法读写,当多线程操作该类的实例对象时,如果线程对类变量有读取、写入操作就会发生类变量读写错误,即便是在类方法前加上synchronized也无效,因为同一个线程在两次调用方法之间时锁是被释放的,这时其它线程可以访问对象的类方法,读取或修改类变量。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。
下面举例说明:
public class QuerySvc {
private String sql;
private static ThreadLocal sqlHolder = newThreadLocal();
public QuerySvc() {
}
public void execute() {
System.out.println("Thread " +Thread.currentThread().getId() +" Sql is " + sql);
System.out.println("Thread " +Thread.currentThread().getId() +" Thread Local variable Sql is " +sqlHolder.get());
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
sqlHolder.set(sql);
}
}
多线程访问
为了说明多线程访问对于类变量和ThreadLocal变量的影响,QuerySvc中分别设置了类变量sql和ThreadLocal变量,使用时先创建QuerySvc的一个实例对象,然后产生多个线程,分别设置不同的sql实例对象,然后再调用execute方法,读取sql的值,看是否是set方法中写入的值。这种场景类似web应用中多个请求线程携带不同查询条件对一个servlet实例的访问,然后servlet调用业务对象,并传入不同查询条件,最后要保证每个请求得到的结果是对应的查询条件的结果。
使用QuerySvc的工作线程如下:
public class Work extends Thread {
private QuerySvc querySvc;
private String sql;
public Work(QuerySvc querySvc,String sql) {
this.querySvc = querySvc;
this.sql = sql;
}
public void run() {
querySvc.setSql(sql);
querySvc.execute();
}
}
运行线程代码如下:
QuerySvc qs = new QuerySvc();
for (int k=0; k<10; k++)
String sql = "Select * from table whereid =" + k;
new Work(qs,sql).start();
}
先创建一个QuerySvc实例对象,然后创建若干线程来调用QuerySvc的set和execute方法,每个线程传入的sql都不一样,从运行结果可以看出sql变量中值不能保证在execute中值和set设置的值一样,在 web应用中就表现为一个用户查询的结果不是自己的查询条件返回的结果,而是另一个用户查询条件的结果;而ThreadLocal中的值总是和set中设置的值一样,这样通过使用ThreadLocal获得了线程安全性。
如果一个对象要被多个线程访问,而该对象存在类变量被不同类方法读写,为获得线程安全,可以用ThreadLocal来替代类变量。
Thread同步机制的比较
说明
ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK 5.0新的ThreadLocal
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
Spring使用ThreadLocal解决线程安全问题
我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9‑2所示:
9-2
图1同一线程贯通三层
这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。
下面的实例能够体现Spring对有状态Bean的改造思路:
代码清单3 TopicDao:非线程安全
public class TopicDao {
private Connection conn;①一个非线程安全的变量
public void addTopic(){
Statement stat = conn.createStatement();②引用非线程安全变量
…
}
}
由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:
代码清单4 TopicDao:线程安全
import java.sql.Connection;
import java.sql.Statement;
public class TopicDao {
①使用ThreadLocal保存Connection变量
private static 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比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
ThreadLocal线程局部变量
什么是线程局部变量
什么是线程局部变量(thread-localvariable)?轻松使用线程:不共享有时是最好的
ThreadLocal 类是悄悄地出现在 Java 平台版本 1.2 中的。虽然支持线程局部变量早就是许多线程工具(例如 Posix pthreads 工具)的一部分,但Java Threads API 的最初设计却没有这项有用的功能。而且,最初的实现也相当低效。由于这些原因, ThreadLocal 极少受到关注,但对简化线程安全并发程序的开发来说,它却是很方便的。在 轻松使用线程的第 3 部分,Java 软件顾问 BrianGoetz 研究了 ThreadLocal 并提供了一些使用技巧。
编写线程安全类是困难的。它不但要求仔细分析在什么条件可以对变量进行读写,而且要求仔细分析其它类能如何使用某个类。 有时,要在不影响类的功能、易用性或性能的情况下使类成为线程安全的是很困难的。有些类保留从一个方法调用到下一个方法调用的状态信息,要在实践中使这样 的类成为线程安全的是困难的。
管理非线程安全类的使用比试图使类成为线程安全的要更容易些。非线程安全类通常可以安全地在多线程程序中使用,只要您能确保一个线程所用的类的实例不被其它线程使用。例如,JDBC Connection 类是非线程安全的 — 两个线程不能在小粒度级上安全地共享一个 Connection — 但如果每个线程都有它自己的Connection ,那么多个线程就可以同时安全地进行数据库操作。
不使用 ThreadLocal 为每个线程维护一个单独的JDBC 连接(或任何其它对象)当然是可能的;Thread API 给了我们把对象和线程联系起来所需的所有工具。而 ThreadLocal 则使我们能更容易地把线程和它的每线程(per-thread)数据成功地联系起来。
什么是线程局部变量(thread-local variable)?
线程局部变量高效地为每个使用它的线程提供单独的线程局部变量值的副本。每个线程只能看到与自己相联系的值,而不知道别的线程可 能正在使用或修改它们自己的副本。一些编译器(例如 Microsoft Visual C++ 编译器或 IBM XL FORTRAN 编译器)用存储类别修饰符(像static 或 volatile )把对线程局部变量的支持集成到了其语言中。Java 编译器对线程局部变量不提供特别的语言支持;相反地,它用 ThreadLocal 类实现这些支持, 核心 Thread 类中有这个类的特别支持。
因为线程局部变量是通过一个类来实现的,而不是作为 Java 语言本身的一部分,所以Java 语言线程局部变量的使用语法比内建线程局部变量语言的使用语法要笨拙一些。要创建一个线程局部变量,请实例化类 ThreadLocal 的一个对象。ThreadLocal 类的行为与 java.lang.ref 中的各种 Reference 类的行为很相似;ThreadLocal 类充当存储或检索一个值时的间接句柄。清单 1 显示了ThreadLocal 接口。
清单 1.ThreadLocal 接口
public class ThreadLocal { public Object get(); public void set(Object newValue); public Object initialValue(); }
|
get() 访问器检索变量的当前线程的值; set() 访问器修改当前线程的值。 initialValue() 方法是可选的,如果线程未使用过某个变量,那么您可以用这个方法来设置这个变量的初始值;它允许延迟初始化。用一个示例实现来说明 ThreadLocal 的工作方式是最好的方法。清单2 显示了 ThreadLocal 的一个实现方式。它不是一个特别好的实现(虽然它与最初实现非常相似),所以很可能性能不佳,但它清楚地说明了 ThreadLocal 的工作方式。
清单 2.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 && !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; } }
|
这个实现的性能不会很好,因为每个 get() 和 set() 操作都需要 values 映射表上的同步,而且如果多个线程同时访问同一个 ThreadLocal ,那么将发生争用。此外,这个实现也是不切实际的,因为用 Thread 对象做 values 映射表中的关键字将导致无法在线程退出后对 Thread 进行垃圾回收,而且也无法对死线程的 ThreadLocal 的特定于线程的值进行垃圾回收。
用 ThreadLocal 实现每线程 Singleton
线程局部变量常被用来描绘有状态“单子”(Singleton) 或线程安全的共享对象,或者是通过把不安全的整个变量封装进 ThreadLocal ,或者是通过把对象的特定于线程的状态封装进 ThreadLocal 。例如,在与数据库有紧密联系的应用程序中,程序的很多方法可能都需要访问数据库。在系统的每个方法中都包含一个 Connection 作为参数是不方便的 — 用“单子”来访问连接可能是一个虽然更粗糙,但却方便得多的技术。然而,多个线程不能安全地共享一个 JDBC Connection 。如清单 3 所示,通过使用“单子”中的 ThreadLocal ,我们就能让我们的程序中的任何类容易地获取每线程 Connection 的一个引用。这样,我们可以认为 ThreadLocal 允许我们创建 每线程单子。
清单 3. 把一个 JDBC连接存储到一个每线程Singleton 中
public class ConnectionDispenser { private static class ThreadLocalConnection extends ThreadLocal { public Object initialValue() { return DriverManager.getConnection(ConfigurationSingleton.getDbUrl()); } } private ThreadLocalConnection conn = new ThreadLocalConnection(); public static Connection getConnection() { return (Connection) conn.get(); } }
|
任何创建的花费比使用的花费相对昂贵些的有状态或非线程安全的对象,例如 JDBC Connection 或正则表达式匹配器,都是可以使用每线程单子(singleton)技术的好地方。当然,在类似这样的地方,您可以使用其它技术,例如用池,来安全地管理 共享访问。然而,从可伸缩性角度看,即使是用池也存在一些潜在缺陷。因为池实现必须使用同步,以维护池数据结构的完整性,如果所有线程使用同一个池,那么 在有很多线程频繁地对池进行访问的系统中,程序性能将因争用而降低。
用 ThreadLocal 简化调试日志纪录
其它适合使用 ThreadLocal 但用池却不能成为很好的替代技术的应用程序包括存储或累积每线程上下文信息以备稍后检索之用这样的应用程序。例如,假设您想创建一个用于管理多线程应用程序调试信息的工具。您可以用如清单 4 所示的DebugLogger 类作为线程局部容器来累积调试信息。在一个工作单元的开头,您清空容器,而当一个错误出现时,您查询该容器以检索这个工作单元迄今为止生成的所有调试信息。
清单 4. 用ThreadLocal 管理每线程调试日志
public class DebugLogger { private static class ThreadLocalList extends ThreadLocal { public Object initialValue() { return new ArrayList(); } public List getList() { return (List) super.get(); } } private ThreadLocalList list = new ThreadLocalList(); private static String[] stringArray = new String[0]; public void clear() { list.getList().clear(); } public void put(String text) { list.getList().add(text); } public String[] get() { return list.getList().toArray(stringArray); } }
|
在您的代码中,您可以调用 DebugLogger.put() 来保存您的程序正在做什么的信息,而且,稍后如果有必要(例如发生了一个错误),您能够容易地检索与某个特定线程相关的调试信息。 与简单地把所有信息转储到一个日志文件,然后努力找出哪个日志记录来自哪个线程(还要担心线程争用日志纪录对象)相比,这种技术简便得多,也有效得多。
ThreadLocal 在基于 servlet 的应用程序或工作单元是一个整体请求的任何多线程应用程序服务器中也是很有用的,因为在处理请求的整个过程中将要用到单个线程。您可以通过前面讲述的每线程单子技术用 ThreadLocal 变量来存储各种每请求(per-request)上下文信息。
ThreadLocal的线程安全性稍差的堂兄弟,InheritableThreadLocal
ThreadLocal 类有一个亲戚,InheritableThreadLocal,它以相似的方式工作,但适用于种类完全不同的应用程序。创建一个线程时如果保存了所有 InheritableThreadLocal 对象的值,那么这些值也将自动传递给子线程。如果一个子线程调用 InheritableThreadLocal 的 get() ,那么它将与它的父线程看到同一个对象。为保护线程安全性,您应该只对不可变对象(一旦创建,其状态就永远不会被改变的对象)使用 InheritableThreadLocal ,因为对象被多个线程共享。 InheritableThreadLocal 很合适用于把数据从父线程传到子线程,例如用户标识(user id)或事务标识(transaction id),但不能是有状态对象,例如JDBC Connection 。
ThreadLocal的性能
虽然线程局部变量早已赫赫有名并被包括 Posix pthreads 规范在内的很多线程框架支持,但最初的 Java 线程设计中却省略了它,只是在Java 平台的版本 1.2 中才添加上去。在很多方面,ThreadLocal 仍在发展之中;在版本 1.3 中它被重写,版本 1.4中又重写了一次,两次都专门是为了性能问题。
在 JDK 1.2 中,ThreadLocal 的实现方式与清单 2 中的方式非常相似,除了用同步WeakHashMap 代替 HashMap 来存储 values 之外。(以一些额外的性能开销为代价,使用 WeakHashMap 解决了无法对Thread 对象进行垃圾回收的问题。)不用说, ThreadLocal 的性能是相当差的。
Java 平台版本 1.3 提供的 ThreadLocal 版本已经尽量更好了;它不使用任何同步,从而不存在可伸缩性问题,而且它也不使用弱引用。相反地,人们通过给 Thread 添加一个实例变量(该变量用于保存当前线程的从线程局部变量到它的值的映射的 HashMap )来修改 Thread 类以支持 ThreadLocal 。因为检索或设置一个线程局部变量的过程不涉及对可能被另一个线程读写的数据的读写操作,所以您可以不用任何同步就实现 ThreadLocal.get() 和 set() 。而且,因为每线程值的引用被存储在自已的 Thread 对象中,所以当对Thread 进行垃圾回收时,也能对该 Thread 的每线程值进行垃圾回收。
不幸的是,即使有了这些改进,Java 1.3 中的ThreadLocal 的性能仍然出奇地慢。据我的粗略测量,在双处理器 Linux 系统上的 Sun 1.3JDK 中进行 ThreadLocal.get() 操作,所耗费的时间大约是无争用同步的两倍。性能这么差的原因是 Thread.currentThread() 方法的花费非常大,占了 ThreadLocal.get() 运行时间的三分之二还多。虽然有这些缺点,JDK 1.3 ThreadLocal.get() 仍然比争用同步快得多,所以如果在任何存在严重争用的地方(可能是有非常多的线程,或者同步块被频繁地执行,或者同步块很大), ThreadLocal 可能仍然要高效得多。
在 Java 平台的最新版本,即版本1.4b2 中, ThreadLocal 和Thread.currentThread() 的性能都有了很大提高。有了这些提高, ThreadLocal 应该比其它技术,如用池,更快。由于它比其它技术更简单,也更不易出错,人们最终将发现它是避免线程间出现不希望的交互的有效途径。
ThreadLocal的好处
ThreadLocal 能带来很多好处。它常常是把有状态类描绘成线程安全的,或者封装非线程安全类以使它们能够在多线程环境中安全地使用的最容易的方式。使用 ThreadLocal 使我们可以绕过为实现线程安全而对何时需要同步进行判断的复杂过程,而且因为它不需要任何同步,所以也改善了可伸缩性。除简单之外,用 ThreadLocal 存储每线程单子或每线程上下文信息在归档方面还有一个颇有价值好处 — 通过使用 ThreadLocal ,存储在ThreadLocal 中的对象都是 不被线程共享的是清晰的,从而简化了判断一个类是否线程安全的工作。
我希望您从这个系列中得到了乐趣,也学到了知识,我也鼓励您到我的 讨论论坛中来深入研究多线程问题。
Web服务器开发环境下的线程安全问题
Servlet是在多线程环境下的。即可能有多个请求发给一个servelt实例,每个请求是一个线程。
struts下的action也 类似,同样在多线程环境下。可以参考struts user guide:http://struts.apache.org/struts-action/userGuide /building_controller.html 中的Action Class Design Guidelines一节: Write code for a multi-threaded environment - Ourcontroller servlet creates only one instance of your Action class, and usesthis one instance to service all requests. Thus, you need to write thread-safeAction classes. Follow the same guidelines you would use to write thread-safeServlets.
译:为多线程环境编写代码。我们的controller servlet指挥创建你的Action 类的一个实例,用此实例来服务所有的请求。因此,你必须编写线程安全的Action类。遵循与写线程安全的servlet同样的方针。
1.什么是线程安全的代码
在多线程环境下能正确执行的代码就是线程安全的。
安全的意思是能正确执行,否则后果是程序执行错误,可能出现各种异常情况。
2.如何编写线程安全的代码
很多书籍里都详细讲解了如何这方面的问题,他们主要讲解的是如何同步线程对共享资源的使用的问题。主要是对synchronized关键字的各种用法,以及锁的概念。
Java1.5中也提供了如读写锁这类的工具类。这些都需要较高的技巧,而且相对难于调试。
但是,线程同步是不得以的方法,是比较复杂的,而且会带来性能的损失。等效的代码中,不需要同步在编写容易度和性能上会更好些。
我这里强调的是什么代码是始终为线程安全的、是不需要同步的。如下:
1)常量始终是线程安全的,因为只存在读操作。
2)对构造器的访问(new 操作)是线程安全的,因为每次都新建一个实例,不会访问共享的资源。
3)最重要的是:局部变量是线程安全的。因为每执行一个方法,都会在独立的空间创建局部变量,它不是共享的资源。局部变量包括方法的参数变量。
struts user guide里有:
Only Use Local Variables - The most important principle that aids inthread-safe coding is to use only local variables, not instance variables , inyour Action class.
译:只使用用局部变量。--编写线程安全的代码最重要的原则就是,在Action类中只使用局部变量,不使用实例变量。
总结:
在Java的Web服务器环境下开发,要注意线程安全的问题。最简单的实现方式就是在Servlet和Struts Action里不要使用类变量、实例变量,但可以使用类常量和实例常量。
如果有这些变量,可以将它们转换为方法的参数传入,以消除它们。
注意一个容易混淆的地方:被Servlet或Action调用的类中(如值对象、领域模型类)中是否可以安全的使用实例变量?如果你在每次方法调用时
新建一个对象,再调用它们的方法,则不存在同步问题---因为它们不是多个线程共享的资源,只有共享的资源才需要同步---而Servlet和Action的实例对于多个线程是共享的。
换句话说,Servlet和Action的实例会被多个线程同时调用,而过了这一层,如果在你自己的代码中没有另外启动线程,且每次调用后续业务对象时都是先新建一个实例再调用,则都是线程安全的。
深入浅出ThreadLocal
一、ThreadLocal概述
学习JDK中的类,首先看下JDK API对此类的描述,描述如下:
JDK API 写道
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
API表达了下面几种观点:
1、ThreadLocal不是线程,是线程的一个变量,你可以先简单理解为线程类的属性变量。
2、ThreadLocal 在类中通常定义为静态类变量。
3、每个线程有自己的一个ThreadLocal,它是变量的一个‘拷贝’,修改它不影响其他线程。
既然定义为类变量,为何为每个线程维护一个副本(姑且成为‘拷贝’容易理解),让每个线程独立访问?多线程编程的经验告诉我们,对于线程共享资源(你可以理解为属性),资源是否被所有线程共享,也就是说这个资源被一个线程修改是否影响另一个线程的运行,如果影响我们需要使用synchronized同步,让线程顺序访问。
ThreadLocal适用于资源共享但不需要维护状态的情况,也就是一个线程对资源的修改,不影响另一个线程的运行;这种设计是‘空间换时间’,synchronized顺序执行是‘时间换取空间’。
二、ThreadLocal方法介绍
T |
get() |
protected T |
initialValue() |
void |
remove() |
void |
set(T value) |
三、深入源码
ThreadLocal有一个ThreadLocalMap静态内部类,你可以简单理解为一个MAP,这个‘Map’为每个线程复制一个变量的‘拷贝’存储其中。
当线程调用ThreadLocal.get()方法获取变量时,首先获取当前线程引用,以此为key去获取响应的ThreadLocalMap,如果此‘Map’不存在则初始化一个,否则返回其中的变量,代码如下:
1. public T get() {
2. Thread t = Thread.currentThread();
3. ThreadLocalMap map = getMap(t);
4. if (map != null) {
5. ThreadLocalMap.Entry e = map.getEntry(this);
6. if (e != null)
7. return (T)e.value;
8. }
9. return setInitialValue();
10. }
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(); }
调用get方法如果此Map不存在首先初始化,创建此map,将线程为key,初始化的vlaue存入其中,注意此处的initialValue,我们可以覆盖此方法,在首次调用时初始化一个适当的值。setInitialValue代码如下:
1. private T setInitialValue() {
2. T value = initialValue();
3. Thread t = Thread.currentThread();
4. ThreadLocalMap map = getMap(t);
5. if (map != null)
6. map.set(this, value);
7. else
8. createMap(t, value);
9. return value;
10. }
private TsetInitialValue() { T value = initialValue(); Thread t =Thread.currentThread();
set方法相对比较简单如果理解以上俩个方法,获取当前线程的引用,从map中获取该线程对应的map,如果map存在更新缓存值,否则创建并存储,代码如下:
1. public void set(T value) {
2. Thread t = Thread.currentThread();
3. ThreadLocalMap map = getMap(t);
4. if (map != null)
5. map.set(this, value);
6. else
7. createMap(t, value);
8. }
public void set(T value) {Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map !=null) map.set(this, value); else createMap(t, value); }
四、ThreadLocal应用示例
1. import java.text.DateFormat;
2. import java.text.ParseException;
3. import java.text.SimpleDateFormat;
4. import java.util.Date;
5.
6. public class DateUtil {
7.
8. private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
9.
10. @SuppressWarnings("rawtypes")
11. private static ThreadLocal threadLocal = new ThreadLocal() {
12. protected synchronized Object initialValue() {
13. return new SimpleDateFormat(DATE_FORMAT);
14. }
15. };
16.
17. public static DateFormat getDateFormat() {
18. return (DateFormat) threadLocal.get();
19. }
20.
21. public static Date parse(String textDate) throws ParseException {
22. return getDateFormat().parse(textDate);
23. }
24. }
import java.text.DateFormat; import java.text.ParseException;import java.text.SimpleDateFormat; import java.util.Date; public class DateUtil{ private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";@SuppressWarnings("rawtypes") private static ThreadLocal threadLocal= new ThreadLocal() { protected synchronized Object initialValue() { return newSimpleDateFormat(DATE_FORMAT); } }; public static DateFormat getDateFormat() {return (DateFormat) threadLocal.get(); } public static Date parse(StringtextDate) throws ParseException { return getDateFormat().parse(textDate); } }
创建一个ThreadLocal类变量,这里创建时用了一个匿名类,覆盖了initialValue方法,主要作用是创建时初始化实例。也可以采用下面方式创建;
1. //第一次调用get将返回null
2. private static ThreadLocal threadLocal = new ThreadLocal();
3. //获取线程的变量副本,如果不覆盖initialValue,第一次get返回null,故需要初始化一个SimpleDateFormat,并set到threadLocal中
4. public static DateFormat getDateFormat()
5. {
6. DateFormat df = (DateFormat) threadLocal.get();
7. if(df==null){
8. df = new SimpleDateFormat(DATE_FORMAT)
9. threadLocal.set(df);
10. }
11. return df;
12. }
//第一次调用get将返回null private static ThreadLocal threadLocal = new ThreadLocal(); //获取线程的变量副本,如果不覆盖initialValue,第一次get返回null,故需要初始化一个SimpleDateFormat,并set到threadLocal中 public staticDateFormat getDateFormat() { DateFormat df = (DateFormat) threadLocal.get();if(df==null){ df = new SimpleDateFormat(DATE_FORMAT) threadLocal.set(df); }return df; }
我们看下我们覆盖的initialValue方法:
1. protected T initialValue() {
2. return null;//直接返回null
3. }
最近看了Spring和Hibernate的源码,发现大量使用了ThreadLocal,于是上网学习了一些关于ThreadLocal的文章,将自己的学习小结贴上来,大家一起进步!
1.ThreadLocal用来解决多线程程序的并发问题
2.ThreadLocal并不是一个Thread,而是Thread的局部变量,当使用ThreadLocal维护
变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每个线程都
可以独立地改变自己的副本,而不会影响其它线程所对应的副本.
3.从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要
表达的意思。
4.线程局部变量并不是Java的新发明,Java没有提供在语言级支持(语法上),而是变
相地通过ThreadLocal的类提供支持.
5.ThreadLocal类中的方法:(JDK5版本之后支持泛型)
void set(T value)
将此线程局部变量的当前线程副本中的值设置为指定值
void remove()
移除此线程局部变量当前线程的值
protected T initialValue()
返回此线程局部变量的当前线程的“初始值”
T get()
返回此线程局部变量的当前线程副本中的值
6.ThreadLocal的原理:
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很
简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素
的键为线程对象,而值对应线程的变量副本
7.自己模拟ThreadLocal:
public class SimpleThreadLocal{
private MapvalueMap=Collections.synchronizedMap(new HashMap());
public void set(Object newValue){
valueMap.put(Thread.currentThread(),newValue);//键为线程对象,值
为本线程的变量副本
}
public Object get(){
Thread currentThread=Thread.currentThread();
Object o=valueMap.get(currentThread);//返回本线程对应的变量
if(o==null&&!valueMap.containsKey(currentThread)){
//如果在Map中不存在,放到Map中保存起来
o=initialValue();
valueMap.put(currentThread,o);
}
return o;
}
public void remove(){
valueMap.remove(Thread.currentThread());
}
public void initialValue(){
return null;
}
}
8.使用ThreadLocal的具体例子:
public class SequenceNumber{
//通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
private static ThreadLocal
(){
protected Integer initialValue(){
return 0;
}
}
public Integer getNextNum(){
seNum.set(seNum.get()+1);
return seNum.get();
}
public static void main(String[] args){
SequenceNumber sn=new SequenceNumber();
//3个线程共享sn,各自产生序列号
TestClient t1 = new TestClient(sn);
TestClient t2 = newTestClient(sn);
TestClient t3 = newTestClient(sn);
t1.start();
t2.start();
t3.start();
}
private static class TestClient extendsThread{
private SequenceNumber sn;
public TestClient(SequenceNumber sn){
this.sn=sn;
}
public void run(){
//每个线程打印3个序列号
for(int i=0;i<3;i++){
System.out.println("thread["+Thread.currentThread
().getName()+",sn["+sn.getNextNum()+"]");
}
}
}.
}
解析:通常我们通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量
值.
考察输出的结果信息(将上段代码在IDE运行),我们发现每个线程所产生的序号虽然都共享同一个
SequenceNumber实例,但它们并没有发生相互干扰的情况,而是各自产生独立的序
列号,这是因为我们通过ThreadLocal为每一个线程提供了单独的副本。
9.ThreadLocal和同步机制的比较:
相同点:ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲
突问题
区别:在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。
这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量
进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序
设计和编写难度相对较大。
ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每
一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为
每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。
ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变
量封装进ThreadLocal.
概括来说:对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方
式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同
的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互
不影响。
总结:ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变
量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题
更简单,更方便,且结果程序拥有更高的并发性。
关于ThreadLocal的用法在进行了诸多的研究分析之后,终于有所斩获,掌握了其本质的若干用法,先总结如下:
在对运行同一段代码的多线程程序中,用以实现同一个线程之内同一个变量的同步,避免其多个线程之间彼此的干扰。效率相对于synchronized而言,有相当的提升。
本质上讲,其就是实现单一线程的私有变量
关于其实现,摘引klyuan(javaeye)上的例子,如下,很好的说明了ThreadLocal的实现方式,当然你也可以参考JDK源代码中的实现。本质上,其就是一个同步的HashMap而已,可以为线程,value为所定义的局部变量。
public class ThreadLocal
{
private Map values = Collections.synchronizedMap(newHashMap());
public Object get()
{
Thread curThread = Thread.currentThread();
Object o = values.get(curThread);
if (o == null &&!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;
}
}
应用分析,一个同事将一段代码给我,说其中关于ThreadLocal的set(null)这行代码,如果注释掉,会出现什么情况,详细代码如下:
我们可以看到有2行关于local和local_tr的set(null)的方法如果被注释掉的情况,我们可以推测会出现什么样的情况。
/**
* Import Package missing for space
**/
public class HibernateUtil {
private static SessionFactorysf;
private static ThreadLocallocal = new ThreadLocal();
private static ThreadLocallocal_tr = new ThreadLocal();
static{
Configuration conf = new Configuration();
conf =conf.configure("/hibernate.cfg.xml");
sf =conf.buildSessionFactory();
}
public static Session getSession(){
System.out.println("threadLocalid=="+local);
Sessionsession = null;
if(local.get()==null){
session = sf.openSession();
System.out.println("---------session is created------------");
local.set(session);
}else{
session =(Session)local.get();
}
return session;
}
public static void begin(){
Session session =(Session)local.get();
Transaction ts = null;
if(local_tr.get()==null){
ts = session.beginTransaction();
local_tr.set(ts);
}
}
public static void commit(){
Transactiontr = (Transaction)local_tr.get();
tr.commit();
local_tr.set(null); // this line is removed,then what will happen?
System.out.println("---------transaction is commited------------");
}
public static void rollback(){
Transaction tr = (Transaction)local_tr.get();
tr.rollback();
System.out.println("---------transaction is rollbacked------------");
}
public static void close(){
Session session = (Session)local.get();
if(session!=null){
session.close();
System.out.println("---------session is destroyed------------");
}
local.set(null); // this line is removed, then what will happen?
}
}
如果基于local(Session的局部变量)不进行set(null)的话,会出现所有的thread都基于获取同一个session的情况。
如果local_tr(Transaction的局部变量)不进行set(null)的话,对于基于同一个session的线程而言,其只可以进行一次的事务提交,后续都将无法进行,因为每次进行之前会进行局部变量的有效判断。
总结: ThreadLocal是一个有效的多线程局部变量工具,当然也是一把双刃剑,用好的话可以制敌,反之则可能祸己。要用好ThreadLocal,我们只需记住其本质:线程局部变量;其实现的本质是一个基于同步的HashMap就可以了。每一个线程都是用于该变量的一个独立副本。
在使用Java的时候,我们都会遇到使用集合(Collection)的时候,但是Java API提供了多种集合的实现,我在使用和面试的时候频
频遇到这样的“抉择” 。 :)(主要还是面试的时候)
久而久之,也就有了一点点的心得体会,写出来以供大家讨论 。
总的说来,Java API中所用的集合类,都是实现了Collection接口,他的一个类继承结构如下:
Collection<--List<--Vector
Collection<--List<--ArrayList
Collection<--List<--LinkedList
Collection<--Set<--HashSet
Collection<--Set<--HashSet<--LinkedHashSet
Collection<--Set<--SortedSet<--TreeSet
Vector : 基于Array的List,其实就是封装了Array所不具备的一些功能方便我们使用,它不可能走入Array的限制。性能也就不可能
超越Array。所以,在可能的情况下,我们要多运用Array。另外很重要的一点就是Vector“sychronized”的,这个也是Vector和
ArrayList的唯一的区别。
ArrayList:同Vector一样是一个基于Array上的链表,但是不同的是ArrayList不是同步的。所以在性能上要比Vector优越一些,但
是当运行到多线程环境中时,可需要自己在管理线程的同步问题。
LinkedList:LinkedList不同于前面两种List,它不是基于Array的,所以不受Array性能的限制。它每一个节点(Node)都包含两方
面的内容:1.节点本身的数据(data);2.下一个节点的信息(nextNode)。所以当对LinkedList做添加,删除动作的时候就不用像
基于Array的List一样,必须进行大量的数据移动。只要更改nextNode的相关信息就可以实现了。这就是LinkedList的优势。
List总结:
1. 所有的List中只能容纳单个不同类型的对象组成的表,而不是Key-Value键值对。例如:[ tom,1,c ];
2. 所有的List中可以有相同的元素,例如Vector中可以有 [tom,koo,too,koo ];
3. 所有的List中可以有null元素,例如[tom,null,1 ];
4. 基于Array的List(Vector,ArrayList)适合查询,而LinkedList(链表)适合添加,删除操作。
HashSet:虽然Set同List都实现了Collection接口,但是他们的实现方式却大不一样。List基本上都是以Array为基础。但是Set则是
在HashMap的基础上来实现的,这个就是Set和List的根本区别。HashSet的存储方式是把HashMap中的Key作为Set的对应存储项。看看
HashSet的add(Object obj)方法的实现就可以一目了然了。
public booleanadd(Object obj)
{
return map.put(obj,PRESENT) == null;
}
这个也是为什么在Set中不能像在List中一样有重复的项的根本原因,因为HashMap的key是不能有重复的。
LinkedHashSet:HashSet的一个子类,一个链表。
TreeSet:SortedSet的子类,它不同于HashSet的根本就是TreeSet是有序的。它是通过SortedMap来实现的。
Set总结:
1. Set实现的基础是Map(HashMap);
2. Set中的元素是不能重复的,如果使用add(Object obj)方法添加已经存在的对象,则会覆盖前面的对象
为什么要使用集合类
当你事先不知道要存放数据的个数,或者你需要一种比数组下标存取机制更灵活的方法时,你就需要用到集合类。
理解集合类
集合类存放于java.util包中。
集合类存放的都是对象的引用,而非对象本身,出于表达上的便利,我们称集合中的对象就是指集合中对象的引用(reference)。
集合类型主要有3种:set(集)、list(列表)和map(映射)。
(1)集
集(set)是最简单的一种集合,它的对象不按特定方式排序,只是简单的把对象加入集合中,就像往口袋里放东西。
对集中成员的访问和操作是通过集中对象的引用进行的,所以集中不能有重复对象。
集也有多种变体,可以实现排序等功能,如TreeSet,它把对象添加到集中的操作将变为按照某种比较规则将其插入到有序的对象序列中。它实现的是SortedSet接口,也就是加入了对象比较的方法。通过对集中的对象迭代,我们可以得到一个升序的对象集合。
(2)列表
列表的主要特征是其对象以线性方式存储,没有特定顺序,只有一个开头和一个结尾,当然,它与根本没有顺序的集是不同的。
列表在数据结构中分别表现为:数组和向量、链表、堆栈、队列。
关于实现列表的集合类,是我们日常工作中经常用到的,将在后边的笔记详细介绍。
(3)映射
映射与集或列表有明显区别,映射中每个项都是成对的。映射中存储的每个对象都有一个相关的关键字(Key)对象,关键字决定了对象在映射中的存储位置,检索对象时必须提供相应的关键字,就像在字典中查单词一样。关键字应该是唯一的。
关键字本身并不能决定对象的存储位置,它需要对过一种散列(hashing)技术来处理,产生一个被称作散列码(hash code)的整数值,散列码通常用作一个偏置量,该偏置量是相对于分配给映射的内存区域起始位置的,由此确定关键字/对象对的存储位置。理想情况下,散列处理应该产生给定范围内均匀分布的值,而且每个关键字应得到不同的散列码。
集合类简介
java.util中共有13个类可用于管理集合对象,它们支持集、列表或映射等集合,以下是这些类的简单介绍集:
HashSet:使用HashMap的一个集的实现。虽然集定义成无序,但必须存在某种方法能相当高效地找到一个对象。使用一个HashMap对象实现集的存储和检索操作是在固定时间内实现的.
TreeSet:在集中以升序对对象排序的集的实现。这意味着从一个TreeSet对象获得第一个迭代器将按升序提供对象。TreeSet类使用了一个TreeMap.
列表:
Vector:实现一个类似数组一样的表,自动增加容量来容纳你所需的元素。使用下标存储和检索对象就象在一个标准的数组中一样。你也可以用一个迭代器从一个Vector中检索对象。Vector是唯一的同步容器类??当两个或多个线程同时访问时也是性能良好的。
Stsck: 这个类从Vector派生而来,并且增加了方法实现栈??一种后进先出的存储结构。
LinkedList: 实现一个链表。由这个类定义的链表也可以像栈或队列一样被使用。
ArrayList: 实现一个数组,它的规模可变并且能像链表一样被访问。它提供的功能类似Vector类但不同步。
映射:
HashTable:实现一个映象,所有的键必须非空。为了能高效的工作,定义键的类必须实现hashcode()方法和equal()方法。这个类是前面java实现的一个继承,并且通常能在实现映象的其他类中更好的使用。
HashMap:实现一个映象,允许存储空对象,而且允许键是空(由于键必须是唯一的,当然只能有一个)。
WeakHashMap:实现这样一个映象:通常如果一个键对一个对象而言不再被引用,键/对象对将被舍弃。这与HashMap形成对照,映象中的键维持键/对象对的生命周期,尽管使用映象的程序不再有对键的引用,并且因此不能检索对象。
TreeMap:实现这样一个映象,对象是按键升序排列的。
Set和List都是由公共接口Collection扩展而来,所以它们都可以使用一个类型为Collection的变量来引用。这就意味着任何列表或集构成的集合都可以用这种方式引用,只有映射类除外(但也不是完全排除在外,因为可以从映射获得一个列表。)所以说,把一个列表或集传递给方法的标准途径是使用Collection类型的参数。
Vector 还是ArrayList,哪一个更好,为什么?
要回答这个问题不能一概而论,有时候使用Vector比较好;有时是ArrayList,有时候这两个都不是最好的选择。你别指望能够获得一个简单肯定答案,因为这要看你用它们干什么。下面有4个要考虑的因素:
(1)API
(2)同步处理
(3)数据增长性
(4)使用模式
下面针对这4个方面进行一一探讨
API
在由Ken Arnold等编著的《JavaProgramming Language》(Addison-Wesley, June 2000)一书中有这样的描述,Vector类似于ArrayList.。所有从API的角度来看这两个类非常相似。但他们之间也还是有一些主要的区别的。
同步性
Vector是同步的。这个类中的一些方法保证了Vector中的对象是线程安全的。而ArrayList则是异步的,因此ArrayList中的对象并不是线程安全的。因为同步的要求会影响执行的效率,所以如果你不需要线程安全的集合那么使用ArrayList是一个很好的选择,这样可以避免由于同步带来的不必要的性能开销。
数据增长
从内部实现机制来讲ArrayList和Vector都是使用数组(Array)来控制集合中的对象。当你向这两种类型中增加元素的时候,如果元素的数目超出了内部数组目前的长度它们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度,ArrayList是原来的50%,所以最后你获得的这个集合所占的空间总是比你实际需要的要大。所以如果你要在集合中保存大量的数据那么使用Vector有一些优势,因为你可以通过设置集合的初始化大小来避免不必要的资源开销。
使用模式
在ArrayList和Vector中,从一个指定的位置(通过索引)查找数据或是在集合的末尾增加、移除一个元素所花费的时间是一样的,这个时间我们用O(1)表示。但是,如果在集合的其他位置增加或移除元素那么花费的时间会呈线形增长:O(n-i),其中n代表集合中元素的个数,i代表元素增加或移除元素的索引位置。为什么会这样呢?以为在进行上述操作的时候集合中第i和第i个元素之后的所
有元素都要执行位移的操作。这一切意味着什么呢?
这意味着,你只是查找特定位置的元素或只在集合的末端增加、移除元素,那么使用Vector或ArrayList都可以。如果是其他操作,你最好选择其他的集合操作类。比如,LinkList集合类在增加或移除集合中任何位置的元素所花费的时间都是一样的—O(1),但它在索引一个元素的使用缺比较慢-O(i),其中i是索引的位置.使用ArrayList也很容易,因为你可以简单的使用索引来代替创建iterator对象的操作。LinkList也会为每个插入的元素创建对象,所有你要明白它也会带来额外的开销。
最后,在《Practical Java》一书中PeterHaggar建议使用一个简单的数组(Array)来代替Vector或ArrayList。尤其是对于执行效率要求高的程序更应如此。因为使用数组(Array)避免了同步、额外的方法调用和不必要的重新分配空间的操作
通通透透理解ThreadLocal
概述
我们知道Spring通过各种DAO模板类降低了开发者使用各种数据持久技术的难度。这些模板类都是线程安全的,也就是说,多个DAO可以复用同一个模板实例而不会发生冲突。
我们使用模板类访问底层数据,根据持久化技术的不同,模板类需要绑定数据连接或会话的资源。但这些资源本身是非线程安全的,也就是说它们不能在同一时刻被多个线程共享。
虽然模板类通过资源池获取数据连接或会话,但资源池本身解决的是数据连接或会话的缓存问题,并非数据连接或会话的线程安全问题。
按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步。但Spring的DAO模板类并未采用线程同步机制,因为线程同步限制了并发访问,会带来很大的性能损失。
此外,通过代码同步解决性能安全问题挑战性很大,可能会增强好几倍的实现难度。那模板类究竟仰丈何种魔法神功,可以在无需同步的情况下就化解线程安全的难题呢?答案就是ThreadLocal!
ThreadLocal在Spring中发挥着重要的作用,在管理request作用域的Bean、事务管理、任务调度、AOP等模块都出现了它们的身影,起着举足轻重的作用。要想了解Spring事务管理的底层技术,ThreadLocal是必须攻克的山头堡垒。
ThreadLocal是什么
早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。
线程局部变量并不是Java的新发明,很多语言(如IBMIBM XL FORTRAN)在语法层面就提供线程局部变量。在Java中没有提供在语言级支持,而是变相地通过ThreadLocal的类提供支持。
所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。
ThreadLocal的接口方法
ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:
voidset(Object value)
设置当前线程的线程局部变量的值。
publicObject get()
该方法返回当前线程所对应的线程局部变量。
publicvoid remove()
将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
protectedObject initialValue()
返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版本:
代码清单1 SimpleThreadLocal
public classSimpleThreadLocal {
private MapvalueMap = Collections.synchronizedMap(new HashMap());
public voidset(Object newValue) {
valueMap.put(Thread.currentThread(), newValue);①键为线程对象,值为本线程的变量副本
}
public Object get(){
ThreadcurrentThread = Thread.currentThread();
Object o =valueMap.get(currentThread);②返回本线程对应的变量
if (o == null&& !valueMap.containsKey(currentThread)) {③如果在Map中不存在,放到Map
中保存起来。
o = initialValue();
valueMap.put(currentThread,o);
}
return o;
}
public voidremove() {
valueMap.remove(Thread.currentThread());
}
public ObjectinitialValue() {
return null;
}
}
虽然代码清单9‑3这个ThreadLocal实现版本显得比较幼稚,但它和JDK所提供的ThreadLocal类在实现思路上是相近的。
一个TheadLocal实例
下面,我们通过一个具体的实例了解一下ThreadLocal的具体使用方法。
代码清单2 SequenceNumber
packagecom.baobaotao.basic;
public classSequenceNumber {
①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
private staticThreadLocal
public IntegerinitialValue(){
return 0;
}
};
②获取下一个序列值
public intgetNextNum(){
seqNum.set(seqNum.get()+1);
returnseqNum.get();
}
public static voidmain(String[] args)
{
SequenceNumber sn = new SequenceNumber();
③ 3个线程共享sn,各自产生序列号
TestClient t1 = newTestClient(sn);
TestClient t2 = newTestClient(sn);
TestClient t3 = newTestClient(sn);
t1.start();
t2.start();
t3.start();
}
private staticclass TestClient extends Thread
{
privateSequenceNumber sn;
publicTestClient(SequenceNumber 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()+"]");
}
}
}
}
通常我们通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量值,如例子中①处所示。TestClient线程产生一组序列号,在③处,我们生成3个TestClient,它们共享同一个SequenceNumber实例。运行以上代码,在控制台上输出以下的结果:
thread[Thread-2]sn[1]
thread[Thread-0]sn[1]
thread[Thread-1]sn[1]
thread[Thread-2]sn[2]
thread[Thread-0]sn[2]
thread[Thread-1]sn[2]
thread[Thread-2]sn[3]
thread[Thread-0]sn[3]
thread[Thread-1]sn[3]
考察输出的结果信息,我们发现每个线程所产生的序号虽然都共享同一个SequenceNumber实例,但它们并没有发生相互干扰的情况,而是各自产生独立的序列号,这是因为我们通过ThreadLocal为每一个线程提供了单独的副本。
Thread同步机制的比较
ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK 5.0新的ThreadLocal
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
Spring使用ThreadLocal解决线程安全问题
我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9‑2所示:
图1同一线程贯通三层
这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。
下面的实例能够体现Spring对有状态Bean的改造思路:
代码清单3 TopicDao:非线程安全
public classTopicDao {
private Connectionconn;①一个非线程安全的变量
public voidaddTopic(){
Statement stat =conn.createStatement();②引用非线程安全变量
…
}
}
由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:
代码清单4 TopicDao:线程安全
importjava.sql.Connection;
importjava.sql.Statement;
public classTopicDao {
①使用ThreadLocal保存Connection变量
private static ThreadLocal
public staticConnection getConnection(){
②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,
并将其保存到线程本地变量中。
if(connThreadLocal.get() == null) {
Connection conn =ConnectionManager.getConnection();
connThreadLocal.set(conn);
return conn;
}else{
returnconnThreadLocal.get();③直接返回线程本地变量
}
}
public voidaddTopic() {
④从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。但这个实例基本上说明了Spring对有状态类线程安全化的解决思路。
小结
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
J2EE的线程安全-ThreadLocal
1.在用Servlet时,我们都知道Servlet只会被初始化一次,只有一个实例。
2.在Struts1中,ActionServlet也仅是初始化一次,也是单实例。
为什么会这样,无非是为了提高效率。但是线程安全不容忽视。但是在WebWork、Struts2却做好了线程安全。
下面具体介绍:
Servlet线程安全
概述
在探讨java线程安全前,让我们先简要介绍一下Java语言。
任何语言,如C++,C#,Java,它们都有相通之处,特别是语法,但如果有人问你,Java语言的核心是什么?类库?关键字?语法?似乎都不是。Java语言的核心,也就是Sun始终不愿意开源的东西:Java虚拟机的实现(不过sun公开了其Java虚拟机规范),也就有了BEA的JRockit,IBM的Jikes,Sun的Hotspot。
Java的核心有两点,Java类加载(JavaClass Loader)和Java内存管理,它们具体体现在Java类库的以下几个类:
java.lang.ClassLoader(java.lang.Class):我们调用的类,包括其接口和超类,import的类是怎么被Java虚拟机载入的?为什么static的字段在servlet容器里面可以一直生存下去(Spring容器中)?
java.lang.Thread(java.lang.ThreadLocal):垃圾回收是怎么进行的(垃圾回收线程)?我们的程序是怎么退出的?
java.lang.refelect.Proxy(java.lang.refelect.Method):为什么Tomcat、Tapestry、Webwork、Spring等容器和框架可以通过配置文件来调用我们写的类?Servlet规范、JSF规范、EJB规范、JDBC规范究竟是怎么回事?为什么它们几乎都是一些接口,而不是具体类?
Servlet线程安全
在Java的server side开发过程中,线程安全(ThreadSafe)是一个尤为突出的问题。因为容器,如Servlet、EJB等一般都是多线程运行的。虽然在开发过程中,我们一般不考虑这些问题,但诊断问题(Robust),程序优化(Performance),我们必须深入它们。
什么是线程安全?
Thread-safe describes a program portion or routine that can be called frommultiple programming threads without unwanted interaction between the threads。
在Java里,线程安全一般体现在两个方面:
1. 多个thread对同一个java实例的访问(read和modify)不会相互干扰,它主要体现在关键字synchronized。如ArrayList和Vector,HashMap和Hashtable(后者每个方法前都有synchronized关键字)。如果你在interator一个List对象时,其它线程remove一个element,问题就出现了。
2. 每个线程都有自己的字段,而不会在多个线程之间共享。它主要体现在java.lang.ThreadLocal类,而没有Java关键字支持,如像static、transient那样。
一个普遍的疑问,我们的Servlet中能够像JavaBean那样declareinstance或static字段吗?如果不可以?会引发什么问题?
答案是:不可以。我们下面以实例讲解:
首先,我们写一个普通的Servlet,里面有instance字段count:
public classSimpleServlet extendsHttpServlet {
// A variablethat is NOT thread-safe!
privateint counter = 0;
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException,IOException {
doPost(req, resp);
}
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException,IOException {
resp.getWriter().println("
");resp.getWriter().println(this+ " ==> ");
resp.getWriter().println(Thread.currentThread() + ":
");
for (int c = 0; c < 10; c++) {
resp.getWriter().println("Counter = " + counter +"
");
try
Thread.sleep((long)Math.random() * 1000);
counter++;
} catch (InterruptedException exc) {
}
}
resp.getWriter().println("");
}
}
然后,我们通过一个html页面向该servlet发出三次请求:
刷新页面几次后,产生的结果为:
com.zwchen.servlet.SimpleServlet@11e1bbf ==>Thread[http-8081-Processor23,5,main]:
Counter = 60
Counter = 61
Counter = 62
Counter = 65
Counter = 68
Counter = 71
Counter = 74
Counter = 77
Counter = 80
Counter = 83
com.zwchen.servlet.SimpleServlet@11e1bbf ==>Thread[http-8081-Processor22,5,main]:
Counter = 61
Counter = 63
Counter = 66
Counter = 69
Counter = 72
Counter = 75
Counter = 78
Counter = 81
Counter = 84
Counter = 87
com.zwchen.servlet.SimpleServlet@11e1bbf ==>Thread[http-8081-Processor24,5,main]:
Counter = 61
Counter = 64
Counter = 67
Counter = 70
Counter = 73
Counter = 76
Counter = 79
Counter = 82
Counter = 85
Counter = 88
我们会发现三点:
servlet只产生了一个Servlet对象,因为输出this时,其hashcode都一样,
servlet在不同的线程(线程池)中运行,如http-8081-Processor22,http-8081-Processor23
Count被这三个doGet方法共享,并且并行修改。
上面的结果,违反了线程安全的两个方面。
那么,我们怎样保证按照我们期望的结果运行呢?首先,我想保证产生的count都是顺序执行的。
我们将Servlet代码重构如下:
public class SimpleServlet extendsHttpServlet {
// A variablethat is NOT thread-safe!
private int counter = 0;
private String mutex ="";
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException,IOException {
doPost(req, resp);
}
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException,IOException {
resp.getWriter().println("
"); resp.getWriter().println(this+ ":
");
synchronized (mutex) {
for (int c = 0; c < 10; c++) {
resp.getWriter().println("Counter =" + counter + "
");
try {
Thread.sleep((long)Math.random() * 1000);
counter++;
}
catch (InterruptedException exc) {
}
}
}
resp.getWriter().println("");
}
}
我们的输出结果为:
com.zwchen.servlet.SimpleServlet@109da93:
Counter = 0
Counter = 1
Counter = 2
Counter = 3
Counter = 4
Counter = 5
Counter = 6
Counter = 7
Counter = 8
Counter = 9
com.zwchen.servlet.SimpleServlet@109da93:
Counter = 10
Counter = 11
Counter = 12
Counter = 13
Counter = 14
Counter = 15
Counter = 16
Counter = 17
Counter = 18
Counter = 19
com.zwchen.servlet.SimpleServlet@109da93:
Counter = 20
Counter = 21
Counter = 22
Counter = 23
Counter = 24
Counter = 25
Counter = 26
Counter = 27
Counter = 28
Counter = 29
这符合了我们的要求,输出都是按顺序的,这正式synchronized的含义。
附带说一下,我现在synchronized的是一个字符串变量mutex,不是this对象,这主要是从performance和Scalability考虑。Synchronized用在this对象上,会带来严重的可伸缩性的问题(Scalability),所有的并发请求都要排队!
现在,我们保证了顺序,但是我们怎么保证Counter字段(不是局部变量!)在每个Servlet的线程下都是独立的呢?也就是说,并发请求时,它们都不相互干扰。
我现在将Servlet代码重构如下:
public classSimpleServlet extends HttpServlet{
private ThreadLocal counter = new ThreadLocal() {
protected synchronized Object initialValue() {
return new Integer(0);
}
};
publicvoid doGet(HttpServletRequest req,HttpServletResponse resp)
throws ServletException,IOException {
doPost(req, resp);
}
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throwsServletException, IOException {
resp.getWriter().println("
");resp.getWriter().println(
this + "[" +Thread.currentThread() + "]:
");
for (int c = 0; c < 10; c++) {
resp.getWriter().println(counter.get() +"
");
try {
Thread.sleep((long)Math.random() * 1000);
int c1 = ((Integer)counter.get()).intValue();
c1++;
counter.set(newInteger(c1));
} catch (InterruptedException exc) {
}
}
resp.getWriter().println("");
}
}
现在,我刷新html页面三次,第三次结果如下:
com.zwchen.servlet.SimpleServlet@124e935[Thread[http-8081-Processor22,5,main]]:
20
21
22
23
24
25
26
27
28
29
com.zwchen.servlet.SimpleServlet@124e935[Thread[http-8081-Processor25,5,main]]:
20
21
22
23
24
25
26
27
28
29
com.zwchen.servlet.SimpleServlet@124e935[Thread[http-8081-Processor23,5,main]]:
20
21
22
23
24
25
26
27
28
29
从以上结果,我们可以发现:
1、在该html页面内的并发三次请求中,该Servlet里面的counter字段都不相互干扰
2、counter字段还是实例字段,并且都会保留状态,不是每次都用0开始
3、html页面内的三次请求都在不同的线程,但在同一个实例中。
总之,在Java里面,字段(不是局部变量)有三个共享范围:instance field,staticfield,local thread field,而后者往往在服务器端这种多线程环境必须考虑到的。
在J2EE项目开发过程中,ThreadLocal类有时有非常重要的作用,下面是我碰到的,但可以延伸:
1、在用Hibernate做web开发的持久化时,有个模式叫做Open Session In View,也就是将session保留到页面中,在response结束后,在OpenSessionInViewFilter中关闭session,这对于延迟加载非常有效,例如,我们在页面上显示User的详细信息,需要显示该user的所属Department的信息; 但是,在list users这种不需要显示department信息的地方,那个user的department信息就不会加载,也就是说加载相关信息是动态的,但不会出现LazyInitializationException,也就是Load on demand。不过,注意慎用该模式。
2、在工作流开发,例如OSWorflow,每次调用其服务前,都需要将caller对象传入,这样会导致我们的方法非常臃肿,如果我们在调用该方法的上层,如在Servlet里调用它之前,将User对象置于ThreadLocal中,那么可以在工作流方法内通过get()方法获取,而不用传入参数。
3、为什么Web框架中,Webwork的action中可以有field,但Struts却不能?其实,也就是说,Struts不是线程安全的,而Webwork是线程安全的。大家可以参考Webwork的ActionContext类:
public class ActionContext implementsSerializable {
static ThreadLocal actionContext = new ActionContextThreadLocal();
……………
而对于Struts,我们可以从ActionServlet.process() =>RequestProcessor. processActionPerform,在RequestProcessor中有字段 protectedHashMap actions = new HashMap();我们不难发现,我们所写的action是共享的,那么内部字段必然也是共享。注意,这种共享类似于Servlet里面的字段。