使用spring的线程安全web应用(翻译Thread-safe webapps using Spring)
程序员文章站
2022-04-01 07:57:33
...
http://www.javalobby.org/articles/thread-safe/index.jsp
利用servlet容器进行开发工作的开发人员应该知道servlet specifications 。
servlet容器为web.xml里面的对象产生一个单例servlet对象.请求有可能同时发生,这就意味着多个线程也许会同时执行一段代码。这就意味着web应用要特别重视安全问题。
回顾线程安全
首先让我们回顾一个线程安全的含义以及如何使用线程安全。各类文章基本上是这么说的:有段代码是线程安全的,它满足时reentrant 或者有通过互斥方式来保护它同步执行。
synchronized 是java线程安全的基础。但他容易带来阻塞。
阻塞对于web应用来说是一个大问题。阻塞并不是并发带来的唯一问题,还可能会有死锁发生。
除了synchronized 关键字,java还提供了java.util.Hashtable 和java.util.Vector.他们的方法是synchronized 的,所以在必要的时候才能用它们。否则就使用java.util.HashMap 和java.util.ArrayList,还有 java.util.Collections 也可以用。
Reentrance虽然容易管理,但也引入了一批问题。Reentrant 代码阻止数据在线程间共享。我们接下来看下面的代码,来理解线程安全
现在在执行这个方法时,每一个线程都有自己的独立的 栈区 。当线程进入到方法执行断的时候,一个方法变量在方法代码段中被创建,并保存在线程的 栈区 (静态方法也放在这里)。不同线程执行这段代码时,会有不同的a/b变量。所以这里是线程安全的,因为没有数据共享。
考虑一下下面的例子,多线程情况下只执行一次并可以重用结果:
这个地方虽然优化了,但可惜他不是线程安全的。两个线程并发执行的时候同时进入到pi==null这个位置,这样可能会new出一个脏的数据
Consider this example which uses ThreadLocal to make the method pi() again thread-safe while still offering performance gains:
ThreadLocal类封装了任何类型对象,并把它绑定到当前线程。线程执行pi()方法的时候,实例pi返回的是当前线程的对象。这样的调用是线程安全的。
之前人们对ThreadLocal有很多的怀疑,但是从上面这个例子被很好的保留。
Writing thread-safe code by reentrance requires you to be careful when using instance variables or static variables, especially when you are modifying objects that may be used by other threads. Synchronization may offer a outcome is some use cases. However, testing your application to identify performance bottlenecks that may be introduced by synchronization is only possible by using profiling tools and testing under load.
web应用中的 线程安全
下面我们考虑一下上面的这个事例如何用在线程安全的web应用中。我们新建一批可以有着良好定义的过程,通过这些过程,允许并发访问数据库。我们利用hibernate来做持久化对象模型。
我们现在只关注于数据库访问的线程安全问题,不关注web层页面访问的问题。类似于connections, result sets, statements or Hibernate sessions 的数据库对象都是有状态的对象。他们没有从设计角度实现线程安全。而且不能被多线程同时访问。我们要避免利用synchronization 方式来控制代码,也不用类似于vector和hashtable的同步类,这是为了避免出现死锁。相反,我们通过reentrance来实现线程安全。
Still, implementing thread-safe database access through reentrance remains a tedious task. Some clever people have come up with a solution by adding a filter in the servlet container configuration. Such a filter would amongst other things create a JDBC connection or Hibernate session at the start of the request before the web tier is invoked and bind it to the current thread by means of ThreadLocal for use in the business logic. While this approach would allow us to achieve thread-safe data access there still is the issue of managing transactions and database errors in addition to the required use a lot of boilerplate code in the business logic. Boilerplate code is particularly bad not only because we have to copy and paste it which makes our application hard to maintain but also because it's potentially buggy.
Spring comes to the rescue
Some of you may have heard of the data access abstraction offered by Spring, some may have used it. Spring's data access abstraction is known for declarative transaction demarcation and reuse of data access best practices through templates. One area that is less covered when reviewing Spring is the thread-safe way in which data access is achieved. This achievement is available in Spring for JDBC, Hibernate, JDO, iBatis and TopLink. Let's have look at how this works in our typical use case.
We start by defining a data source and session factory for Hibernate.
这是典型的方式使用hibernate配置数据源。我们定义数据源来连接数据库和本地会话实例(会话实例由hibernate会话工厂创建)。下一个目标就是添加一个通过hibernate访问数据库的业务对象,并添加一个事务管理器,这个事务管理器利用hibernate会话来管理本地事务。业务对象会暴露出一个在数据库中产生对象的方法,执行这个产生对象的操作的时机是在事务管理器在事务里面包装这个方法的时候。首选我们看一下这个事务对象和他的方法。
上面这个例子中,我们看到的HibernateTemplate 是spring提供的。这个template 类实现了下面的最佳实践和抽象的样例代码。... 类似的template类spring也为DBC, iBatis SqlMap, JDO and TopLink这些框架提供了。这些所有的template类和他们的实例变量都是通过reentrance 方式实现线程安全,而且能够安全的被多线程并发访问。这些模板的功能不仅仅是代码和最佳实践复用。更多的是满足我们线程安全的数据访问。下面让我们看看sping里面如何进行建立业务对象和进行事务管理。
从这里还看不出spring的事务管理。我们进一步来讨论这个配置文件。第一步,我们的业务对象调用HibernateCustomerDAO,此时的HibernateCustomerDAO 已经装配了hibernate会话工厂实例,注意点sping里面所有的bean都默认为单例的,所以可以看出,多线程执行createCustomer()方法是存在并发的。
第二步,我们注册一个同样组装了hibernate会话工厂的hibernate 事务管理器,这个事务管理器在每次调用的时候,都会做一系列的事情:1.先检查是否存在一个hibernate事务绑定到当前线程,如果有,就使用它,如果没有hibernate会话绑定到当前线程,事务管理器会请求hibernate session 工厂产生一个新的hibernate session,并把这个session 绑定到当前线程。2.依赖于我们自己定义的内容,如果当前没有事务,事务管理器针对hibernate session开始一个新的事务,若已经有事务了,加入到当前的事务中去。
这个动作能够以声明的方式,绑定到spring的TransactionProxyFactoryBean 。TransactionProxyFactoryBean 为通过事务管理器为管理事务的业务对象创建一个代理对象,每次createCustomer()方法都是通过proxy对象来调用的,事务的管理方式依赖于我们配置的事务属性。spring为hibernate、jdbc、djo等数据源提供一系列的事务管理实现
现在我们返回到我们的业务对象。下面部分依赖于spring的AOP。在我们调用createCustomer() 方法是,HibernateTemplate 会去找一个hibernate session绑定到当前线程上,因为我们把false传递给了HibernateTemplate 构造函数的第二个参数,如果没有hibernate session绑定在当前线程上,将会抛出一个unchecked exception,这样也是提供了另外一种针对没有注册transaction management而又想执行createCustomer()方法的安全控制网。我们是在讨论事务,如果transaction management上注册了hibernate session,transaction management应该绑定到了当前线程,而且事务已经处于开始状态。注意,HibernateTemplate 将不会检查一个事务当前是否保持活跃状态,或者这个事务标志为开始或者结束状态。同样要注意当前活动的事务在声明方法中如果抛出了异常,将会执行回滚。通过修改事务属性,我们可以改变事务动作行为,但这部分不属于本文讨论的内容。
结论
我们简单总结一下spring通过线程安全的数据访问功能。通过事务管理和ThreadLocal的强化,spring利用自己的data access templates和线程的ThreadLocal关联起数据库连接工具和当前线程,从这里我们可以看到,在同一个时间数据库连接并不是在多线程之间共享的。spring所做的,只是提供 1.声明式事务管理;2抽象的容器平台级的代码;3.最佳实践,4.线程安全。在通过使用spring来建立数据库连接的时候,我们的应用利用reentrance 来实现线程安全的功能也是顺理成章的。
利用servlet容器进行开发工作的开发人员应该知道servlet specifications 。
servlet容器为web.xml里面的对象产生一个单例servlet对象.请求有可能同时发生,这就意味着多个线程也许会同时执行一段代码。这就意味着web应用要特别重视安全问题。
回顾线程安全
首先让我们回顾一个线程安全的含义以及如何使用线程安全。各类文章基本上是这么说的:有段代码是线程安全的,它满足时reentrant 或者有通过互斥方式来保护它同步执行。
synchronized 是java线程安全的基础。但他容易带来阻塞。
阻塞对于web应用来说是一个大问题。阻塞并不是并发带来的唯一问题,还可能会有死锁发生。
除了synchronized 关键字,java还提供了java.util.Hashtable 和java.util.Vector.他们的方法是synchronized 的,所以在必要的时候才能用它们。否则就使用java.util.HashMap 和java.util.ArrayList,还有 java.util.Collections 也可以用。
Reentrance虽然容易管理,但也引入了一批问题。Reentrant 代码阻止数据在线程间共享。我们接下来看下面的代码,来理解线程安全
public Double pi() { int a = 22; int b = 7; return new Double(a / b); }
现在在执行这个方法时,每一个线程都有自己的独立的 栈区 。当线程进入到方法执行断的时候,一个方法变量在方法代码段中被创建,并保存在线程的 栈区 (静态方法也放在这里)。不同线程执行这段代码时,会有不同的a/b变量。所以这里是线程安全的,因为没有数据共享。
考虑一下下面的例子,多线程情况下只执行一次并可以重用结果:
private Double pi = null; public Double pi() { if (pi == null) { pi = new Double(22 / 7); } return pi; }
这个地方虽然优化了,但可惜他不是线程安全的。两个线程并发执行的时候同时进入到pi==null这个位置,这样可能会new出一个脏的数据
Consider this example which uses ThreadLocal to make the method pi() again thread-safe while still offering performance gains:
private static ThreadLocal pi = new ThreadLocal(); public Double pi() { if (pi.get() == null) { pi.set(new Double(22 / 7)); } return (Double)pi.get(); }
ThreadLocal类封装了任何类型对象,并把它绑定到当前线程。线程执行pi()方法的时候,实例pi返回的是当前线程的对象。这样的调用是线程安全的。
之前人们对ThreadLocal有很多的怀疑,但是从上面这个例子被很好的保留。
Writing thread-safe code by reentrance requires you to be careful when using instance variables or static variables, especially when you are modifying objects that may be used by other threads. Synchronization may offer a outcome is some use cases. However, testing your application to identify performance bottlenecks that may be introduced by synchronization is only possible by using profiling tools and testing under load.
web应用中的 线程安全
下面我们考虑一下上面的这个事例如何用在线程安全的web应用中。我们新建一批可以有着良好定义的过程,通过这些过程,允许并发访问数据库。我们利用hibernate来做持久化对象模型。
我们现在只关注于数据库访问的线程安全问题,不关注web层页面访问的问题。类似于connections, result sets, statements or Hibernate sessions 的数据库对象都是有状态的对象。他们没有从设计角度实现线程安全。而且不能被多线程同时访问。我们要避免利用synchronization 方式来控制代码,也不用类似于vector和hashtable的同步类,这是为了避免出现死锁。相反,我们通过reentrance来实现线程安全。
Still, implementing thread-safe database access through reentrance remains a tedious task. Some clever people have come up with a solution by adding a filter in the servlet container configuration. Such a filter would amongst other things create a JDBC connection or Hibernate session at the start of the request before the web tier is invoked and bind it to the current thread by means of ThreadLocal for use in the business logic. While this approach would allow us to achieve thread-safe data access there still is the issue of managing transactions and database errors in addition to the required use a lot of boilerplate code in the business logic. Boilerplate code is particularly bad not only because we have to copy and paste it which makes our application hard to maintain but also because it's potentially buggy.
Spring comes to the rescue
Some of you may have heard of the data access abstraction offered by Spring, some may have used it. Spring's data access abstraction is known for declarative transaction demarcation and reuse of data access best practices through templates. One area that is less covered when reviewing Spring is the thread-safe way in which data access is achieved. This achievement is available in Spring for JDBC, Hibernate, JDO, iBatis and TopLink. Let's have look at how this works in our typical use case.
We start by defining a data source and session factory for Hibernate.
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>WEB-INF/jdbc.properties</value> </list> </property> </bean> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName"><value>${jdbc.driverClassName}</value></property> <property name="url"><value>${jdbc.url}</value></property> <property name="username"><value>${jdbc.username}</value></property> <property name="password"><value>${jdbc.password}</value></property> </bean> <bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFactoryBean"> <property name="dataSource"> <ref bean="dataSource"/> </property> <property name="mappingDirectoryLocations"> <list> <value>classpath:</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">net.sf.hibernate.dialect.HSQLDialect</prop> <prop key="hibernate.show_sql">true</prop> </props> </property> </bean>
这是典型的方式使用hibernate配置数据源。我们定义数据源来连接数据库和本地会话实例(会话实例由hibernate会话工厂创建)。下一个目标就是添加一个通过hibernate访问数据库的业务对象,并添加一个事务管理器,这个事务管理器利用hibernate会话来管理本地事务。业务对象会暴露出一个在数据库中产生对象的方法,执行这个产生对象的操作的时机是在事务管理器在事务里面包装这个方法的时候。首选我们看一下这个事务对象和他的方法。
public interface CustomerDAO { public void createCustomer(Customer customer); } public class HibernateCustomerDAO implements CustomerDAO { private HibernateTemplate hibernateTemplate = null; public void setSessionFactory(SessionFactory sessionFactory) { this.hibernateTemplate = new HibernateTemplate(sessionFactory, false); } public void createCustomer(Customer customer) { this.hibernateTemplate.save(customer); } }
上面这个例子中,我们看到的HibernateTemplate 是spring提供的。这个template 类实现了下面的最佳实践和抽象的样例代码。... 类似的template类spring也为DBC, iBatis SqlMap, JDO and TopLink这些框架提供了。这些所有的template类和他们的实例变量都是通过reentrance 方式实现线程安全,而且能够安全的被多线程并发访问。这些模板的功能不仅仅是代码和最佳实践复用。更多的是满足我们线程安全的数据访问。下面让我们看看sping里面如何进行建立业务对象和进行事务管理。
<bean id="customerDAOTarget" class="test.usecase.HibernateCustomerDAO"> <property name="sessionFactory"><ref bean="sessionFactory"/></property> </bean> <bean id="transactionManager" class="org.springframework.orm.hibernate.HibernateTransactionManager"> <property name="sessionFactory"><ref bean="sessionFactory"/></property> </bean> <bean id="customerDAO" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager"><ref bean="transactionManager"/></property> <property name="target"><ref bean="customerDAOTarget"/></property> <property name="transactionAttributes"> <props> <prop key="create*">PROPAGATION_REQUIRED</prop> <prop key="*">PROPAGATION_REQUIRED</prop> </props> </property> </bean>
从这里还看不出spring的事务管理。我们进一步来讨论这个配置文件。第一步,我们的业务对象调用HibernateCustomerDAO,此时的HibernateCustomerDAO 已经装配了hibernate会话工厂实例,注意点sping里面所有的bean都默认为单例的,所以可以看出,多线程执行createCustomer()方法是存在并发的。
第二步,我们注册一个同样组装了hibernate会话工厂的hibernate 事务管理器,这个事务管理器在每次调用的时候,都会做一系列的事情:1.先检查是否存在一个hibernate事务绑定到当前线程,如果有,就使用它,如果没有hibernate会话绑定到当前线程,事务管理器会请求hibernate session 工厂产生一个新的hibernate session,并把这个session 绑定到当前线程。2.依赖于我们自己定义的内容,如果当前没有事务,事务管理器针对hibernate session开始一个新的事务,若已经有事务了,加入到当前的事务中去。
这个动作能够以声明的方式,绑定到spring的TransactionProxyFactoryBean 。TransactionProxyFactoryBean 为通过事务管理器为管理事务的业务对象创建一个代理对象,每次createCustomer()方法都是通过proxy对象来调用的,事务的管理方式依赖于我们配置的事务属性。spring为hibernate、jdbc、djo等数据源提供一系列的事务管理实现
现在我们返回到我们的业务对象。下面部分依赖于spring的AOP。在我们调用createCustomer() 方法是,HibernateTemplate 会去找一个hibernate session绑定到当前线程上,因为我们把false传递给了HibernateTemplate 构造函数的第二个参数,如果没有hibernate session绑定在当前线程上,将会抛出一个unchecked exception,这样也是提供了另外一种针对没有注册transaction management而又想执行createCustomer()方法的安全控制网。我们是在讨论事务,如果transaction management上注册了hibernate session,transaction management应该绑定到了当前线程,而且事务已经处于开始状态。注意,HibernateTemplate 将不会检查一个事务当前是否保持活跃状态,或者这个事务标志为开始或者结束状态。同样要注意当前活动的事务在声明方法中如果抛出了异常,将会执行回滚。通过修改事务属性,我们可以改变事务动作行为,但这部分不属于本文讨论的内容。
结论
我们简单总结一下spring通过线程安全的数据访问功能。通过事务管理和ThreadLocal的强化,spring利用自己的data access templates和线程的ThreadLocal关联起数据库连接工具和当前线程,从这里我们可以看到,在同一个时间数据库连接并不是在多线程之间共享的。spring所做的,只是提供 1.声明式事务管理;2抽象的容器平台级的代码;3.最佳实践,4.线程安全。在通过使用spring来建立数据库连接的时候,我们的应用利用reentrance 来实现线程安全的功能也是顺理成章的。