聊聊程序设计(一)——有状态、无状态

  在程序设计中,状态的概念是非常抽象的,要给出一个所有人都能接受的定义真的太难了,所以我只能根据我自己的理解尝试一下。我理解的状态是这样的:在两次或多次不同的进程(或线程)调用间有目的地引用了同一组数据,这组数据就称为状态,这样的调用就叫有状态调用,相反就是无状态调用。从这个定义中我们至少可以得出以下三点:

  1. 状态是一组数据。数据有可变与不可变之分,对其访问的方法是不一样的。
  2. 不同的进程或线程间调用。可以是同一个程序的不同的线程间调用,也可以是不同进程间,甚至是不同的机器间。要满足上面的三种情景,被访问的状态数据必须是被共享的,而且在本次访问中对状态的修改,在下次的访问中是可见的。
  3. 有目的地引用同一组数据。所谓有目的地引用,言外之意是我们在程序设计时是故意这么做的。所以,程序有没有状态是由程序设计人员决定的。

  状态存在于程序设计的各个方面,即存在于类的对象中,也存在于各种同步和异步的通信中。我们有目的地设计有状态的程序,其实是为了满足某种需求,但盲目地使用有状态的程序却会带来性能以及拓展性的问题。无状态的程序始终会在性能和拓展性方面优于有状态的程序,所以在设计有状态的程序时,我们需要兼顾性能和拓展性。下面我们从几个场景来分析状态的使用。

一、对象的状态。

  具体到类的对象上,状态其实是一组全局变量(或叫对象的变量),由于局部变量在方法体运行完成后就可能被Java虚拟机回收了,所以局部变量天生就是无状态的。类的静态变量永远都是有状态的,因为类变量的设计是为了在此类的所有对象中共享数据。

  我们都知道,对象的创建和初始化是非常耗时间和资源的,所以在设计类的时候我们会考虑对象是如何创建以及创建多少的问题。大致分为三种方案:

  1. 单例,即一个类只有创建一个对象,所有线程共用这个对象。这样可以节省大量的系统开销,便于在系统内共享数据,也便于满足某种特殊的需求,比如Java Web程序的ServletContext对象,容器只创建一个这种对象,可以将程序级别的共享数据放入其中。为了实现单例,我们通常会使用单例模式来控制单例对象的创建和初始化。单例中对外开放写功能的全局变量都是有状态的,在不同线程中使用时必须考虑其线程安全的问题。
  2. 对象池,即初始化一定数量的对象,并将其放入一个有界容器中,容器的最大容量既是对象的最大数量,当需要新的对象时从池中取出一空闲对象,如果没有空闲的,在未达容器最大容量前会新建一个新的对象,在达到容器最大容量后,只能等待空闲对象的出现,最后将使用完的对象放回池中。为了保证对象的线程安全性,对象池中的对象必须是无状态的,或者状态为不可改变。Tomcat对Servlet的管理就是采用对象池的做法,这样可以避免频繁的对象创建,可以显著的提高系统性能和容量,加快对客服端的响应速度。
  3. 按需创建对象,即需要时创建,用完后抛弃。这个方案有以下两种情景,
    • 单线程情景,不存在并发修改或读取同一对象状态的情况,所以我们无需考虑状态的问题。
    • 多线程情景,不同线程会并发的修改或读取同一个对象的状态,要使得对象是线程安全的,需要采用同步机制来协同对对象可变状态的访问。

二、多线程中的状态。

  在多线程的情形中,无状态的对象跟单线程中的对象没有任何区别,因为无状态的对象不共享数据,也就没有线程安全的问题,所以在这里我们只讨论有状态的对象。但状态的可变与不可变是有本质区别的,不可变的状态是线程安全的,可变的状态是线程不安全的,所以在访问可变状态时我们需要借助某种机制来同步状态,以达到安全的访问。

  要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问。“共享”意味着状态可以由多个线程同时访问,而“可变”则意味着状态的值在其生命周期内可以发生变化。当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问。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同步的问题。目前比较流行的解决方案有以下两个:

  1. 所有Tomcat共享同一份Session,即每个Tomcat中都保存了整个网站中所有访问用户的Session。为了达到这点,需要将Session同步到所有的Tomcat中,我们可以用Tomcat自带的同步插件来实现。如何实现请参考Tomcat官方文档。
  2. Tomcat中并不在Session中保存任何用户数据,数据保存在独立的缓存服务器或DB中,每次用户请求到来时,从缓存服务器或DB中取出用户数据,并重构Session。在此种方案中虽然Session同样具有状态,但并不保存任何用户数据,所以从这个角度来讲Session并无状态,不管是哪个Tomcat中的Session来服务用户,都不会丢失数据,Tomcat间无需同步Session。

  上面两种方案都可以解决Tomcat分布式集群Session同步的问题。第一种方案是利用Session的有状态性来保存用户数据,但需要在所有节点中同步保证一份完整的Session,当访问量大时,可能会耗尽tomcat的内存资源,同时Session的同步也可能导致内部网络资源的紧张,最终导致用户响应时间变长甚至系统崩溃。而第二种方案却是避免了Session的有状态性,非常优雅地解决了第一种方案中的问题,不但可以响应更大的访问量,而且具有非常好的拓展性,当系统无法响应更多的访问量时,可以简单地加入更多Tomcat来解决。从上面的分析可知,无状态的数据比有状态的数据具有更好的性能和拓展性,所以在程序设计时我们应该尽量避免设计有状态的程序。

 

posted on 2017-11-23 16:29  笑看风云变幻  阅读(5558)  评论(0编辑  收藏  举报