本文整理自《Struts2技术内幕——深入解析Struts2架构设计与实现原理》
线程安全问题的由来
在传统的Web开发中,我们处理Http请求最常用的方式是通过实现Servlet对象来进行Http请求的响应.Servlet是J2EE的重要标准之一,规定了Java如何响应Http请求的规范.通过HttpServletRequest和HttpServletResponse对象,我们能够轻松地与Web容器交互.
当Web容器收到一个Http请求时,Web容器中的一个主调度线程会从事先定义好的线程中分配一个当前工作线程,将请求分配给当前的工作线程,由该线程来执行对应的Servlet对象中的service方法.当这个工作线程正在执行的时候,Web容器收到另外一个请求,主调度线程会同样从线程池中选择另外一个工作线程来服务新的请求.Web容器本身并不关心这个新的请求是否访问的是同一个Servlet实例.
结论:
对于同一个Servlet对象的多个请求,Servlet的service方法将在一个多线程的环境中并发执行.所以,Web容器默认采用单实例(单Servlet实例)多线程的方式来处理Http请求.这种处理方式能够减少新建Servlet实例的开销,从而缩短了对Http请求的响应时间.但是,这样的处理方式会导致变量访问的线程安全问题.也就是说,Servlet对象并不是一个线程安全的对象.
public class ThreadSafeTestServlet extends HttpServlet {
// 定义一个实例变量,并非一个线程安全的变量
private int 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 {
// 输出当前Servlet的信息以及当前线程的信息
System.out.println(this + ":" + Thread.currentThread());
// 循环,并增加实例变量counter的值
for (int i = 0; i < 5; i++) {
System.out.println("Counter = " + counter);
try {
Thread.sleep((long) Math.random() * 1000);
counter++;
} catch (InterruptedException exc) {
}
}
}
}
输出结果为:
sample.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
sample.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
sample.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对象的运行特
ThreadLocal本身并不是一个线程,而是通过操作当前线程(Thread)中的一个内部变量来达到与其他线程隔离的目的.之所以取名为ThreadLocal,所期望表达的含义是其操作的对象是线程(Thread)的一个本地变量.如果我们看一下Thread的源码实现,就会发现这一变量,代码如下:
public class Thread implements Runnable {
// 这里省略了许多其他的代码
ThreadLocal.ThreadLocalMap threadLocals = null;
}
这是JDK中Thread源码的一部分,从中我们可以看出ThreadLocalMap跟随着当前的线程而存在.不同的线程Thread,拥有不同的ThreadLocalMap的本地实例变量,这也就是“副本”的含义.接下来我们再来看看ThreadLocal.ThreadLocalMap是如何定义的,以及ThreadLocal如何来操作它
public class ThreadLocal<T> {
// 这里省略了许多其他代码
// 将value的值保存于当前线程的本地变量中
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 调用getMap方法获得当前线程中的本地变量ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果ThreadLocalMap已存在,直接使用
if (map != null)
// 以当前的ThreadLocal的实例作为key,存储于当前线程的
// ThreadLocalMap中,如果当前线程中被定义了多个不同的ThreadLocal
// 的实例,则它们会作为不同key进行存储而不会互相干扰
map.set(this, value);
else
// ThreadLocalMap不存在,则为当前线程创建一个新的
createMap(t, value);
}
// 获取当前线程中以当前ThreadLocal实例为key的变量值
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程中的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取当前线程中以当前ThreadLocal实例为key的变量值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T) e.value;
}
// 当map不存在时,设置初始值
return setInitialValue();
}
// 从当前线程中获取与之对应的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 创建当前线程中的ThreadLocalMap
void createMap(Thread t, T firstValue) {
// 调用构造函数生成当前线程中的ThreadLocalMap
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// ThreadLoaclMap的定义
static class ThreadLocalMap {
// 这里省略了许多代码
}
}
从上述代码中,我们看到了ThreadLocal类的大致结构和进行ThreadLocalMap的操作.我们可以从中得出以下的结论:
/**
* 当一个线程还没有运行完成时,如果不想传递数据,
* 可以通过ThreadLocal来保存与这个Thread相关数据。
* @author qjc
*/
public class BaseDemo {
public static void main(String[] args) {
//声明Map<Object key,Object value>
//Object是值,key是当前线程的引用=Thread.currentThread();
ThreadLocal<Object> tl = new ThreadLocal<Object>();
//保存数据
tl.set("Helllo");
//获取数据
Object val = tl.get();
System.err.println(val);
}
}
当多个线程共同访问同一个资源时,用threadLocal来维护某个线程的变量,一个应用项目中,一般只要有一个(static)threadlocal的实例就可以了
public class MyThreadLocal {
//声明一个唯一的ThreadLocal
private static ThreadLocal<Object> tl = new ThreadLocal<Object>();//声明线程局部的容器对象。
public static Object getObject(){
//先从tl中读取数据
Object o = tl.get();//先从tl中获取与当前线程相关的对象 如果没有保存过,map.get(Thread.currentThread());
if(o==null){
//生成一个随机
o = new Random().nextInt(100);
//放到tl
tl.set(o);//如果没有就声明一个新的放到tl中
}
return o;
}
public static void remove(){
tl.remove();//可选的删除这个对象
}
}
对ThreadLocal内部保存的对象来说。你可以执行remove(无参数)方法删除与当前thread相关的对象。也可以不执行:
因为:Threadlocal内部使用的是弱引用WeakReferences
public class UserDao2 {
public void save(){
String sql = "insert into users values(?,?,?)";
QueryRunner run = new QueryRunner();
try {
run.update(DataSourceUtils.getConn(),sql,"U002","Jack","333");
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
service注入成员
public class UserService {
//声明两个dao
private UserDao1 dao1 = new UserDao1();
private UserDao2 dao2 = new UserDao2();
public void save(){
dao1.save();
dao2.save();
}
}
修改datasourceutils.java
public class DataSourceUtils {
// 声明线程局部的容器
private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
private static DataSource ds;
static {
ds = // 默认的读取c3p0-config.xml中默认配置
new ComboPooledDataSource("qjc");
}
public static DataSource getDatasSource() {
return ds;
}
public static Connection getConn() {
// 先从tl这个容器中获取一次数据,如果当前线程已经保存过connection则直接返回这个connecton
Connection con = tl.get();
if (con == null) {
try {
con = ds.getConnection();// 每一次从ds中获取一个新的连接
//将这个con放到tl中
tl.set(con);
} catch (Exception e) {
e.printStackTrace();
}
}
return con;
}
}
声明一个过虑器在过虑器开始事务
public class TxFilter implements Filter{
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
//获取连接
Connection con = null;
//在try中开始事务
try{
con = DataSourceUtils.getConn();
//开始事务
con.setAutoCommit(false);
//放行
chain.doFilter(request, response);
//如果没有出错。
con.commit();
}catch(Exception e){
System.err.println("出错了");
try {
con.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
throw new RuntimeException(e);
}finally{
try {
con.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void destroy() {
}
}
将过虑器配置到web.xml中。且对某个路径设置过虑
<filter> <filter-name>tx</filter-name> <filter-class>cn.qjc.filter.TxFilter</filter-class> </filter> <filter-mapping> <filter-name>tx</filter-name> <url-pattern>/tx/*</url-pattern>[W31] </filter-mapping>优化: 在 datasourceutls.java 实现一个删除 thredlocal 中与线程相关的对象
public class DataSourceUtils {
// 声明线程局部的容器
private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
private static DataSource ds;
static {
ds = // 默认的读取c3p0-config.xml中默认配置
new ComboPooledDataSource("qjc");
}
public static DataSource getDatasSource() {
return ds;
}
public static Connection getConn() {
// 先从tl这个容器中获取一次数据,如果当前线程已经保存过connection则直接返回这个connecton
Connection con = tl.get();
if (con == null) {
try {
con = ds.getConnection();// 每一次从ds中获取一个新的连接
//将这个con放到tl中
tl.set(con);
} catch (Exception e) {
e.printStackTrace();
}
}
return con;
}
public static void remove(){
tl.remove();
}
}
在TxFilter中调用一个remove:
public class TxFilter implements Filter{
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
System.err.println("thread:"+Thread.currentThread().getName());
//获取连接
Connection con = null;
//在try中开始事务
try{
con = DataSourceUtils.getConn();
//开始事务
con.setAutoCommit(false);
//放行
chain.doFilter(request, response);
//如果没有出错。
con.commit();
}catch(Exception e){
System.err.println("出错了");
try {
if(e instanceof SQLException){
con.rollback();
}else{
con.commit();
}
} catch (SQLException e1) {
e1.printStackTrace();
}
throw new RuntimeException(e);
}finally{
try {
con.close();
DataSourceUtils.remove();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void destroy() {
}
}
比较TheadLocal模式与synchronized关键字
ThreadLocal模式synchronized关键字都用于处理多线程并发访问变量的问题,只是二者处理问题的角度和思路不同.
1)ThreadLocal是一个java类,通过对当前线程中的局部变量的操作来解决不同线程的变量访问的冲突问题.所以,ThreadLocal提供了线程安全的共享对象机制,每个线程都拥有其副本.
2)Java中的synchronized是一个保留字,它依靠JVM的锁机制来实现临界区的函数或者变量的访问中的原子性.在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量.此时,被用作“锁机制”的变量时多个线程共享的.
同步机制(synchronized关键字)采用了“以时间换空间”的方式,提供一份变量,让不同的线程排队访问.而ThreadLocal采用了“以空间换时间”的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响
ThreadLocal模式的核心元素
要完成ThreadLocal模式,其中最关键的地方就是创建一个任何地方都可以访问到的ThreadLocal实例(也就是执行示意图中的菱形部分)。而这一点,我们可以通过类的静态实例变量来实现,这个用于承载静态实例变量的类就被视作是一个共享环境.我们来看一个例子,如代码清单如下所示:
public class Counter {
//新建一个静态的ThreadLocal变量,并通过get方法将其变为一个可访问的对象
private static ThreadLocal<Integer> counterContext = new ThreadLocal<Integer>(){
protected synchronized Integer initialValue(){
return 10;
}
};
// 通过静态的get方法访问ThreadLocal中存储的值
public static Integer get(){
return counterContext.get();
}
// 通过静态的set方法将变量值设置到ThreadLocal中
public static void set(Integer value) {
counterContext.set(value);
}
// 封装业务逻辑,操作存储于ThreadLocal中的变量
public static Integer getNextCounter() {
counterContext.set(counterContext.get() + 1);
return counterContext.get();
}
}
public class ThreadLocalTest extends Thread {
public void run(){
for(int i = 0; i < 3; i++){
System.out.println("Thread[" + Thread.currentThread().getName() + "],counter=" + Counter.getNextCounter());
}
}
}
public class Test {
public static void main(String[] args) {
ThreadLocalTest testThread1 = new ThreadLocalTest();
ThreadLocalTest testThread2 = new ThreadLocalTest();
ThreadLocalTest testThread3 = new ThreadLocalTest();
testThread1.start();
testThread2.start();
testThread3.start();
}
}
输出结果:
Thread[Thread-2],counter=11
Thread[Thread-2],counter=12
Thread[Thread-2],counter=13
Thread[Thread-0],counter=11
Thread[Thread-0],counter=12
Thread[Thread-0],counter=13
Thread[Thread-1],counter=11
Thread[Thread-1],counter=12
Thread[Thread-1],counter=13
上面的输出结果也证实了,counter的值在多线程环境中的访问是线程安全的.从对例子的分析中我们可以再次体会到,ThreadLocal模式最合适的使用场景:在同一个线程(Thread)的不同开发层次中共享数据.
从上面的例子中,我们可以简单总结出实现ThreadLocal模式的两个主要步骤:
1. 建立一个类,并在其中封装一个静态的ThreadLocal变量,使其成为一个共享数据环境
2. 在类中实现访问静态ThreadLocal变量的静态方法(设值和取值)
建立在ThreadLocal模式的实现步骤之上,ThreadLocal的使用则更加简单.在线程执行的任何地方,我们都可以通过访问共享数据类中所提供的ThreadLocal变量的设值和取值方法安全地获得当前线程中安全的变量值.
这两个步骤,我们之后会在Struts2的实现中多次提及,读者只要能充分理解ThreadLocal处理多线程访问的基本原理,就能对Struts2的数据访问和数据共享的设计有一个整体的认识.
讲到这里,我们回过头来看看ThreadLocal模式的引入,到底对我们的编程模型有什么重要的意义呢?
结论 :
1、使用ThreadLocal模式,可以使得数据在不同的编程层次得到有效地共享,
这一点,是由ThreadLocal模式的实现机理决定的.因为实现ThreadLocal模式的一个重要步骤,就是构建一个静态的共享存储空间.从而使得任何对象在任何时刻都可以安全地对数据进行访问.
2、使用ThreadLocal模式,可以对执行逻辑与执行数据进行有效解耦
这一点是ThreadLocal模式给我们带来的最为核心的一个影响,因为在一般情况下,Java对象之间的协作关系,主要通过参数和返回值来进行消息传递,这也是对象协作之间的一个重要依赖,而ThreadLocal模式彻底打破了这种依赖关系,通过线程安全的共享对象来进行数据共享,可以有效避免在编程层次之间形成数据依赖,这也成为了XWork事件处理体系设计的核心.