聊聊程序設計(一)——有狀態、無狀態


  在程序設計中,狀態的概念是非常抽象的,要給出一個所有人都能接受的定義真的太難了,所以我只能根據我自己的理解嘗試一下。我理解的狀態是這樣的:在兩次或多次不同的進程(或線程)調用間有目的地引用了同一組數據,這組數據就稱為狀態,這樣的調用就叫有狀態調用,相反就是無狀態調用。從這個定義中我們至少可以得出以下三點:

  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來解決。從上面的分析可知,無狀態的數據比有狀態的數據具有更好的性能和拓展性,所以在程序設計時我們應該盡量避免設計有狀態的程序。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM