`

Struts2中的模式--ThreadLocal模式

阅读更多

本文整理自《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对象的运行特
         1、Servlet对象是一个无状态的单例对象(Singleton),因为我们看到多次请求的this指针所打印出来的hashcode值都相同
         2、 Servlet在不同的线程(线程池)中运行,如http-8081-Processor22和http-8081-Processor23等输出值可以明显区分出不同的线程执行了同一段Servlet逻辑代码.
          3、Counter变量在不同的线程中共享,而且它的值被不同的线程修改,输出时已经不是顺序输出.也就是说,其他的线程会篡改当前线程中实例变量的值,针对这些对象的访问不是线程安全的.
ThreadLocal模式的实现机理
在JDK的早期版本中,提供了一种解决多线程并发问题的方案: java.lang.ThreadLocal类.ThreadLocal类在维护变量时,实际使用了当前线程(Thread)中的一个叫做ThreadLocalMap的独立副本,每个线程可以独立修改属于自己的副本而不会互相影响,从而隔离了线程和线程,避免了线程访问实例变量发生冲突的问题.
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的操作.我们可以从中得出以下的结论:
           1. ThreadLocalMap变量属于线程(Thread)的内部属性,不同的线程(Thread)拥有完全不同的ThreadLocalMap变量.
          2. 线程(Thread)中的ThreadLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的.
          3. 在创建ThreadLocalMap之前,会首先检查当前线程(Thread)中的ThreadLocalMap变量是否已经存在,如果不存在则创建一个;如果已经存在,则使用当前线程(Thread)已创建的ThreadLocalMap.
         4. 使用当前线程(Thread)的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key进行存储
ThreadLocal模式,至少从两个方面完成了数据访问隔离,有了横向和纵向的两种不同的隔离方式,ThreadLocal模式就能真正地做到线程安全:
             纵向隔离:线程(Thread)与线程(Thread)之间的数据访问隔离.这一点由线程(Thread)的数据结构保证.因为每个线程(Thread)在进行对象访问时,访问的都是各自线程自己的ThreadLocalMap.
            横向隔离:同一个线程中,不同的ThreadLocal实例操作的对象之间的相互隔离.这一点由ThreadLocalMap在存储时,采用当前ThreadLocal的实例作为key来保证.
TheadLocal的应用
用ThreadLocal保存和获取数据的示例:
/**
 * 当一个线程还没有运行完成时,如果不想传递数据,
 * 可以通过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

 

案例2用ThreadLocal管理事务:
开发两个dao(此处省略一个)
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事件处理体系设计的核心.

分享到:
评论

相关推荐

    struts2源码研究

    Struts2 源码分析主要涉及其在Tomcat启动过程中的初始化步骤以及请求处理流程。首先,我们来看Tomcat启动时Struts2框架如何准备和执行。 在Tomcat启动时,Struts2的Filter文件被加载,具体是`...

    struts1,struts2,webwork,线程安全问题

    综上所述,Struts1、Struts2和WebWork这三个框架都面临着线程安全问题,但在Struts2中这个问题得到了较好的解决。Struts2通过使用“prototype”作用域管理`Action`实例,有效地避免了线程安全问题。而对于Struts1和...

    struts2+hibernate3 open session in view

    1. 配置拦截器:在Struts2的配置文件中,我们需要添加一个专门处理Hibernate Session的拦截器,如`OpenSessionInViewInterceptor`。这个拦截器会在请求开始时打开Session,在请求结束时关闭Session,确保Session在...

    Struts Struts1例子Struts注意事项

    Struts1作为早期版本,虽然现在已经被Struts2所取代,但仍然在很多老项目中广泛使用,对于理解Web应用架构和MVC设计模式有着重要的学习价值。本篇文章将深入探讨Struts1框架的基本用法、例子以及需要注意的事项。 ...

    spring+struts+Hibernate面试题

    - 在 Struts 1 中,Action 实例是单例模式的,这意味着同一个 Action 实例会被多个请求共享。因此,如果不采取特别措施,默认情况下 Struts 1 的 Action 是不线程安全的。 - **确保线程安全的方法**: - **避免在...

    设计模式面试题

    - Struts框架就是一个典型的前端控制器模式的例子。 **9. 职责链模式是什么?** - 职责链模式允许多个处理对象有机会处理请求,直到其中一个对象处理它为止。 - 这种模式可以用来简化请求处理过程,降低发送者和...

    JAVA三大框架面试题.pdf

    在面试中,了解Struts2的工作流程、设计模式以及与其它组件如拦截器和过滤器的区别是非常重要的。 1. **Struts2工作流程**: - 客户端发起一个请求到Servlet容器。 - 请求通过一系列过滤器,包括可选的...

    JAVA三大框架面试题(2)参照.pdf

    在Struts2中,任何包含execute标识的方法的POJO对象都可作为Action使用。 2. 灵活性:Struts2比Struts1更灵活,允许Action类实现多种服务接口,且支持OGNL(Object-Graph Navigation Language),增强了数据绑定和...

    java 反模式 卷1 pdf

    "Thread Local"反模式则警告我们在多线程环境中滥用ThreadLocal变量可能带来的资源泄露风险。 书中的内容不仅包含理论分析,还会提供实际案例和改进建议。例如,对于"Lazy Initialization"反模式,书中可能会介绍...

    三大框架面试题

    【三大框架面试题】主要涉及Java中的Web开发框架,尤其是Struts2的面试知识点。这里我们详细探讨Struts2的工作流程、设计模式、拦截器与过滤器的区别以及Struts1与Struts2的对比。 首先,Struts2的工作流程如下: 1...

    JAVA三大框架面试题(2)[定义].pdf

    Java三大框架之一的Struts2是企业级应用中常见的MVC框架,它的设计目标是简化Web应用程序的开发,提供一种更高效、灵活的控制层。本文将深入探讨Struts2的工作流程、设计模式以及它与Struts1的区别,并对比拦截器和...

    Struts+hibarnate源码

    2. **Session管理**:在Action中,应使用ThreadLocal或Spring的AOP来管理Session的生命周期,避免并发问题。 3. **事务控制**:通过编程式或声明式事务管理确保数据的一致性,一般在Service层处理。 4. **结果集...

    Struts+Hibernate+Spring常见面试题

    2. 理解MVC模式,知道Struts如何实现MVC,以及MVC的组成部分。 3. 熟悉Spring的AOP和IOC概念,理解它们在Spring中的应用。 4. 掌握Hibernate的查询方式,理解load()和get()的区别。 5. 熟悉Tiles框架,它是如何实现...

    Java面试总结~~~~.docx

    - Spring MVC与Struts2的区别。 - Spring中的AOP(面向切面编程)及其主要用途。 - Spring的设计模式,如依赖注入、工厂模式等。 - Mybatis的动态SQL、$和#的区别,以及resultType和resultMap的使用场景。 7. *...

    springmvc+mybatis面试题

    1. 性能上:Struts1 &gt; SpringMVC &gt; Struts2,因为Struts2的拦截器链相比SpringMVC更复杂。 2. 开发效率:SpringMVC和Struts2相似,比Struts1更快。 【核心入口类】 SpringMVC的核心入口类是DispatchServlet;Struts...

    servlet与Struts action线程安全问题分析

    Struts2框架提供了多种策略来处理线程安全问题,包括Action实例的多例模式、使用拦截器进行同步控制等。开发者需要理解框架的工作原理,并结合最佳实践来避免线程安全问题。 总之,理解和处理Servlet和Struts ...

Global site tag (gtag.js) - Google Analytics