# 有状态和无状态的基本概念
1、有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。
2、无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象.不能保存数据,是不变类,是线程安全的。
# 示例
/**
* 有状态bean,有state,user等属性,并且user有存偖功能,是可变的。
*/
public class StatefulBean {
public int state;
// 由于多线程环境下,user是引用对象,是非线程安全的
public User user;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
/**
* 无状态bean,不能存偖数据。因为没有任何属性,所以是不可变的。只有一系统的方法操作。
*/
public class StatelessBeanService {
// 虽然有billDao属性,但billDao是没有状态信息的,是Stateless Bean.
BillDao billDao;
public BillDao getBillDao() {
return billDao;
}
public void setBillDao(BillDao billDao) {
this.billDao = billDao;
}
public List<User> findUser(String Id) {
return null;
}
}
# Spring 中的有状态(Stateful)和无状态(Stateless)
无状态的 Bean 适合用不变模式,技术就是单例模式,这样可以共享实例,提高性能。有状态的 Bean,多线程环境下不安全,那么适合用 Prototype 原型模式。Prototype: 每次对 bean 的请求都会创建一个新的 bean 实例。
默认情况下,从 Spring bean 工厂所取得的实例为 singleton(scope 属性为 singleton),容器只存在一个共享的 bean 实例。
scope 选择的原则:有状态的 bean 都使用 prototype 作用域,而对无状态的 bean 则应该使用 singleton 作用域。
如 Service 层、Dao 层用默认 singleton 就行,虽然 Service 类也有 dao 这样的属性,但 dao 这些类都是没有状态信息的,也就是相当于不变(immutable) 类,所以不影响。Struts2 中的 Action 因为会有 User、BizEntity 这样的实例对象,是有状态信息的,在多线程环境下是不安全的,所以 Struts2 默认的实现是 Prototype 模式。在 Spring 中,Struts2 的 Action 中,scope 要配成 prototype 作用域。
# Servlet 是单例模式
Servlet体系结构是建立在 Java 多线程机制之上的,它的生命周期是由 Web
容器负责的。一个 Servlet 类在 Application 中只有一个实例存在,也就是有多个线程在使用这个实例。这是单例模式的应用。无状态的单例是线程安全的,但我们如果在 Servlet 里用了实例变量,那么就变成有状态了,是非线程安全的。如下面的用法就是不安全的,因为 user,out 都是有状态信息的。
/**
* 非线程安全的Servlet。
*/
public class UnSafeServlet HttpServlet{
User user;
PrintWriter out;
public void doGet (HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException{
//do something...
}
}
Out,Request,Response,Session,Config,Page,PageContext 是线程安全的,Application 在整个系统内被使用,所以不是线程安全的.
# SpringMvc 默认是单例默认,Struts2 默认的实现是 Prototype 模式。
# 程序设计中的有状态和无状态
在程序设计中,状态的概念是非常抽象的,要给出一个所有人都能接受的定义真的太难了,所以我只能根据我自己的理解尝试一下。我理解的状态是这样的:在两次或多次不同的进程(或线程)调用间有目的地引用了同一组数据,这组数据就称为状态,这样的调用就叫有状态调用,相反就是无状态调用。从这个定义中我们至少可以得出以下三点:
- 状态是一组数据。数据有可变与不可变之分,对其访问的方法是不一样的。
- 不同的进程或线程间调用。可以是同一个程序的不同的线程间调用,也可以是不同进程间,甚至是不同的机器间。要满足上面的三种情景,被访问的状态数据必须是被共享的,而且在本次访问中对状态的修改,在下次的访问中是可见的。
- 有目的地引用同一组数据。所谓有目的地引用,言外之意是我们在程序设计时是故意这么做的。所以,程序有没有状态是由程序设计人员决定的。
状态存在于程序设计的各个方面,即存在于类的对象中,也存在于各种同步和异步的通信中。我们有目的地设计有状态的程序,其实是为了满足某种需求,但盲目地使用有状态的程序却会带来性能以及拓展性的问题。无状态的程序始终会在性能和拓展性方面优于有状态的程序,所以在设计有状态的程序时,我们需要兼顾性能和拓展性。下面我们从几个场景来分析状态的使用。
# 对象的状态。
具体到类的对象上,状态其实是一组全局变量(或叫对象的变量),由于局部变量在方法体运行完成后就可能被 Java 虚拟机回收了,所以局部变量天生就是无状态的。类的静态变量永远都是有状态的,因为类变量的设计是为了在此类的所有对象中共享数据。
我们都知道,对象的创建和初始化是非常耗时间和资源的,所以在设计类的时候我们会考虑对象是如何创建以及创建多少的问题。大致分为三种方案:
- 单例,即一个类只有创建一个对象,所有线程共用这个对象。这样可以节省大量的系统开销,便于在系统内共享数据,也便于满足某种特殊的需求,比如 Java Web 程序的 ServletContext 对象,容器只创建一个这种对象,可以将程序级别的共享数据放入其中。为了实现单例,我们通常会使用单例模式来控制单例对象的创建和初始化。单例中对外开放写功能的全局变量都是有状态的,在不同线程中使用时必须考虑其线程安全的问题。
- 对象池,即初始化一定数量的对象,并将其放入一个有界容器中,容器的最大容量既是对象的最大数量,当需要新的对象时从池中取出一空闲对象,如果没有空闲的,在未达容器最大容量前会新建一个新的对象,在达到容器最大容量后,只能等待空闲对象的出现,最后将使用完的对象放回池中。为了保证对象的线程安全性,对象池中的对象必须是无状态的,或者状态为不可改变。Tomcat 对 Servlet 的管理就是采用对象池的做法,这样可以避免频繁的对象创建,可以显著的提高系统性能和容量,加快对客服端的响应速度。
- 按需创建对象,即需要时创建,用完后抛弃。这个方案有以下两种情景,
- 单线程情景,不存在并发修改或读取同一对象状态的情况,所以我们无需考虑状态的问题。
- 多线程情景,不同线程会并发的修改或读取同一个对象的状态,要使得对象是线程安全的,需要采用同步机制来协同对对象可变状态的访问。
# 多线程中的状态。
在多线程的情形中,无状态的对象跟单线程中的对象没有任何区别,因为无状态的对象不共享数据,也就没有线程安全的问题,所以在这里我们只讨论有状态的对象。但状态的可变与不可变是有本质区别的,不可变的状态是线程安全的,可变的状态是线程不安全的,所以在访问可变状态时我们需要借助某种机制来同步状态,以达到安全的访问。
要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问。“共享”意味着状态可以由多个线程同时访问,而“可变”则意味着状态的值在其生命周期内可以发生变化。当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问。Java 中的主要同步机制是关键字 synchronized,它提供了一种独占锁方式,但“同步”这个术语还包括 volatile 类型的变量,显示锁以及原子变量。
下面所列为在设计并发程序时,对象状态设计及同步的技巧,细节请参考书籍《Java 并发编程实战》:
- 所有的并发问题都可以归结为如何协调对并发状态变量的访问。可变状态越少就越容易确保线程安全性。
- 尽量将状态变量声明为 final 类型,除非他们是可变的。
- 不可变对象一定是线程安全的。不可变对象能极大地降低并发编程的复杂性。他们更为简单而且安全,可以任意共享而无须使用加锁或保护性复制等机制。
- 封装有助于管理复杂性。在编写线程安全的程序时,虽然可以将所有数据都保存在全局变量中,但为什么要这样做?将数据封装在对象中,更容易维护不变性条件:将同步机制封装在对象中,更易于遵循同步策略。
- 用锁来保护每个可变状态变量。
- 当保护同一个不变性条件中的所有状态变量时,要使用同一个锁。
- 在执行复合操作间,要持有锁。
- 如果从多个线程中访问同一个可变状态变量时没有同步机制,那么程序会出现问题。
- 不要故作聪明地推断出不需要使用同步。
- 在设计过程中考虑线程安全,或者在文档中明确地指出它不是线程安全的。
- 将同步策略文档化。
# 分布式系统的状态。
在分布式系统中,有状态的数据是非常昂贵的,这需要消耗大量的资源,而且很难拓展。下面我们用 Java Web 分布式集群中 Session 同步的设计来探讨分布式集群系统中状态数据的设计和实现。
我们都知道 Java Web 中的 Session 是一个有状态的对象,可以用来在同一个用户的多次访问间共享数据,比如记录用户的登录状态,或者在多个页面间共享数据。对于高访问量、高并发的网站或 Web 程序来说,目前比较常见的解决方案应该是利用负载均衡进行 server 集群,例如比较流行的 nginx+memcache+tomcat。集群之后我们会有多个 Tomcat,用户在访问我们的网站时有可能第一次请求分发到 tomcat1 下,而第二次请求却分发到了 tomcat2,有过 web 开发经验的朋友都会知道如果这时两个 tomcat 中的 session 不一致会导致怎样的后果,可以想象这个用户会收到未登录的信息,或者部分数据的丢失,因此,在 Web 集群环境下,我们需要解决多个 Tomcat 之间 Session 同步的问题。目前比较流行的解决方案有以下两个:
- 所有 Tomcat 共享同一份 Session,即每个 Tomcat 中都保存了整个网站中所有访问用户的 Session。为了达到这点,需要将 Session 同步到所有的 Tomcat 中,我们可以用 Tomcat 自带的同步插件来实现。如何实现请参考 Tomcat 官方文档。
- Tomcat 中并不在 Session 中保存任何用户数据,数据保存在独立的缓存服务器或 DB 中,每次用户请求到来时,从缓存服务器或 DB 中取出用户数据,并重构 Session。在此种方案中虽然 Session 同样具有状态,但并不保存任何用户数据,所以从这个角度来讲 Session 并无状态,不管是哪个 Tomcat 中的 Session 来服务用户,都不会丢失数据,Tomcat 间无需同步 Session。
上面两种方案都可以解决 Tomcat 分布式集群 Session 同步的问题。第一种方案是利用 Session 的有状态性来保存用户数据,但需要在所有节点中同步保证一份完整的 Session,当访问量大时,可能会耗尽 tomcat 的内存资源,同时 Session 的同步也可能导致内部网络资源的紧张,最终导致用户响应时间变长甚至系统崩溃。而第二种方案却是避免了 Session 的有状态性,非常优雅地解决了第一种方案中的问题,不但可以响应更大的访问量,而且具有非常好的拓展性,当系统无法响应更多的访问量时,可以简单地加入更多 Tomcat 来解决。从上面的分析可知,无状态的数据比有状态的数据具有更好的性能和拓展性,所以在程序设计时我们应该尽量避免设计有状态的程序。