學了ConcurrentHashMap
殊不知如何應用?用了Tomcat的Session殊不知其是如何實現的,Session是怎麼被建立和銷燬的?往下看你就知道了。java
很少廢話,直接上圖 程序員
仔細觀察上圖,咱們能夠得出如下結論HttpSession
是JavaEE標準中操做Session的接口類,所以咱們實際上操做的是StandardSessionFacade
類算法
Session
保存數據所使用的數據結構是ConcurrentHashMap
, 如你在圖上看到的咱們往Session
中保存了一個msgapache
爲何須要使用ConcurrentHashMap
呢?緣由是,在處理Http請求並非只有一個線程會訪問這個Session, 現代Web應用訪問一次頁面,一般須要同時執行屢次請求, 而這些請求可能會在同一時刻內被Web容器中不一樣線程同時執行,所以若是採用HashMap
的話,很容易引起線程安全的問題。設計模式
讓咱們先來看看HttpSession的包裝類。數組
在此類中咱們能夠學習到外觀模式(Facde)的實際應用。其定義以下所示。瀏覽器
public class StandardSessionFacade implements HttpSession 複製代碼
那麼此類是如何實現Session的功能呢?觀察如下代碼不可貴出,此類並非HttpSession的真正實現類,而是將真正的HttpSession實現類進行包裝,只暴露HttpSession接口中的方法,也就是設計模式中的外觀(Facde)模式。安全
private final HttpSession session;
public StandardSessionFacade(HttpSession session) {
this.session = session;
}
複製代碼
那麼咱們爲何不直接使用HttpSession的實現類呢?session
根據圖1,咱們能夠知道HttpSession的真正實現類是StandardSession
,假設在該類內定義了一些本應由Tomcat調用而非由程序調用的方法,那麼因爲Java的類型系統咱們將能夠直接操做該類,這將會帶來一些不可預見的問題,如如下代碼所示。數據結構
而若是咱們將StandardSession
再包裝一層,上圖代碼執行的時候將會發生錯誤。以下圖所示,將會拋出類型轉換的異常,從而阻止此處非法的操做。
再進一步,咱們由辦法繞外觀類直接訪問StandardSession
嗎?
事實上是能夠的,咱們能夠經過反射機制來獲取StandardSession
,但你最好清楚本身在幹啥。代碼以下所示
@GetMapping("/s")
public String sessionTest(HttpSession httpSession) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
StandardSessionFacade session = (StandardSessionFacade) httpSession;
Class targetClass = Class.forName(session.getClass().getName());
//修改可見性
Field standardSessionField = targetClass.getDeclaredField("session");
standardSessionField.setAccessible(true);
//獲取
StandardSession standardSession = (StandardSession) standardSessionField.get(session);
return standardSession.getManager().toString();
}
複製代碼
該類的定義以下
public class StandardSession implements HttpSession, Session, Serializable 複製代碼
經過其接口咱們能夠看出此類除了具備JavaEE標準中HttpSession
要求實現的功能以外,還有序列化的功能。
在圖1中咱們已經知道StandardSession
是用ConcurrentHashMap
來保存的數據,所以接下來咱們主要關注StandardSession
的序列化以及反序列化的實現,以及監聽器的功能。
還記得上一節咱們經過反射機制獲取到了StandardSession
嗎?利用如下代碼咱們能夠直接觀察到反序列化出來的StandardSession
是咋樣的。
@GetMapping("/s")
public void sessionTest(HttpSession httpSession, HttpServletResponse response) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, IOException {
StandardSessionFacade session = (StandardSessionFacade) httpSession;
Class targetClass = Class.forName(session.getClass().getName());
//修改可見性
Field standardSessionField = targetClass.getDeclaredField("session");
standardSessionField.setAccessible(true);
//獲取
StandardSession standardSession = (StandardSession) standardSessionField.get(session);
//存點數據以便觀察
standardSession.setAttribute("msg","hello,world");
standardSession.setAttribute("user","kesan");
standardSession.setAttribute("password", "點贊");
standardSession.setAttribute("tel", 10086L);
//將序列化的結果直接寫到Http的響應中
ObjectOutputStream objectOutputStream = new ObjectOutputStream(response.getOutputStream());
standardSession.writeObjectData(objectOutputStream);
}
複製代碼
若是不出意外,訪問此接口瀏覽器將會執行下載操做,最後獲得一個文件
使用WinHex
打開分析,如圖所示爲序列化以後得結果,主要是一大堆分隔符,以及類型信息和值,如圖中紅色方框標準的信息。
不建議你們去死磕序列化文件是如何組織數據的,由於意義不大
若是你真的有興趣建議你閱讀如下代碼
org.apache.catalina.session.StandardSession.doWriteObject
在JavaEE的標準中,咱們能夠經過配置HttpSessionAttributeListener
來監聽Session的變化,那麼在StandardSession
中是如何實現的呢,若是你瞭解觀察者模式,那麼想必你已經知道答案了。 以setAttribute爲例,在調用此方法以後會當即在本線程調用監聽器的方法進行處理,這意味着咱們不該該在監聽器中執行阻塞時間過長的操做。
public void setAttribute(String name, Object value, boolean notify) {
//省略無關代碼
//獲取上文中配置的事件監聽器
Object listeners[] = context.getApplicationEventListeners();
if (listeners == null) {
return;
}
for (int i = 0; i < listeners.length; i++) {
//只有HttpSessionAttributeListener才能夠執行
if (!(listeners[i] instanceof HttpSessionAttributeListener)) {
continue;
}
HttpSessionAttributeListener listener = (HttpSessionAttributeListener) listeners[i];
try {
//在當前線程調用監聽器的處理方法
if (unbound != null) {
if (unbound != value || manager.getNotifyAttributeListenerOnUnchangedValue()) {
//若是是某個鍵的值被修改則調用監聽器的attributeReplaced方法
context.fireContainerEvent("beforeSessionAttributeReplaced", listener);
if (event == null) {
event = new HttpSessionBindingEvent(getSession(), name, unbound);
}
listener.attributeReplaced(event);
context.fireContainerEvent("afterSessionAttributeReplaced", listener);
}
} else {
//若是是新添加某個鍵則執行attributeAdded方法
context.fireContainerEvent("beforeSessionAttributeAdded", listener);
if (event == null) {
event = new HttpSessionBindingEvent(getSession(), name, value);
}
listener.attributeAdded(event);
context.fireContainerEvent("afterSessionAttributeAdded", listener);
}
} catch (Throwable t) {
//異常處理
}
}
}
複製代碼
在瞭解完Session的結構以後,咱們有必要明確StandardSession
是在什麼時候被建立的,以及須要注意的點。
首先咱們來看看StandardSession
的構造函數, 其代碼以下所示。
public StandardSession(Manager manager) {
//調用Object類的構造方法,默認已經調用了
//此處再聲明一次,不知其用意,或許以前此類有父類?
super();
this.manager = manager;
//是否開啓訪問計數
if (ACTIVITY_CHECK) {
accessCount = new AtomicInteger();
}
}
複製代碼
在建立StandardSession
的時候都必須傳入Manager
對象以便與此StandardSession
關聯,所以咱們能夠將目光轉移到Manager
,而Manager
與其子類之間的關係以下圖所示。
ManagerBase
中能夠發現如下代碼。
protected Map<String, Session> sessions = new ConcurrentHashMap<>();
複製代碼
Session
是Tomcat自定義的接口,StandardSession
實現了HttpSession
以及Session
接口,此接口功能更加豐富,但並不向程序員提供。
查找此屬性能夠發現,與Session相關的操做都是經過操做sessions
來實現的,所以咱們能夠明確保存Session的數據結構是ConcurrentHashMap
。
那麼Session究竟是如何建立的呢?我找到了如下方法ManagerBase.creaeSession
, 總結其流程以下。
StandardSession
對象LazySessionIdGenerator
(此算法與其餘算法不一樣之處就在於並不會在一開始就加載隨機數數組,而是在用到的時候才加載,此處的隨機數組並非普通的隨機數組而是SecureRandom
,相關信息能夠閱讀大佬的文章)100
個session的建立速率,所以sessionCreationTiming
是固定大小爲100的鏈表(一開始爲100個值爲null
的元素),所以在將新的數據添加到鏈表中時必需要將舊的數據移除鏈表以保證其固定的大小。session建立速率計算公式以下(1000*60*counter)/(int)(now - oldest)
now
爲獲取統計數據時的時間System.currentTimeMillis()
oldest
爲隊列中最先建立session的時間counter
爲隊列中值不爲null
的元素的數量- 因爲計算的是
每分鐘的速率
所以在此處必須將1000乘以60(一分鐘內有60000毫秒)
public Session createSession(String sessionId) {
//檢查Session是否超過限制,若是是則拋出異常
if ((maxActiveSessions >= 0) &&
(getActiveSessions() >= maxActiveSessions)) {
rejectedSessions++;
throw new TooManyActiveSessionsException(
sm.getString("managerBase.createSession.ise"),
maxActiveSessions);
}
//該方法會建立StandardSession對象
Session session = createEmptySession();
//初始化Session中必要的屬性
session.setNew(true);
//session是否可用
session.setValid(true);
//建立時間
session.setCreationTime(System.currentTimeMillis());
//設置session最大超時時間
session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
String id = sessionId;
if (id == null) {
id = generateSessionId();
}
session.setId(id);
sessionCounter++;
//記錄建立session的時間,用於統計數據session的建立速率
//相似的還有ExpireRate即Session的過時速率
//因爲可能會有其餘線程對sessionCreationTiming操做所以須要加鎖
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized (sessionCreationTiming) {
//sessionCreationTiming是LinkedList
//所以poll會移除鏈表頭的數據,也就是最舊的數據
sessionCreationTiming.add(timing);
sessionCreationTiming.poll();
}
return session;
}
複製代碼
要銷燬Session,必然要將Session從ConcurrentHashMap
中移除,順藤摸瓜咱們能夠發現其移除session的代碼以下所示。
@Override
public void remove(Session session, boolean update) {
//檢查是否須要將統計過時的session的信息
if (update) {
long timeNow = System.currentTimeMillis();
int timeAlive =
(int) (timeNow - session.getCreationTimeInternal())/1000;
updateSessionMaxAliveTime(timeAlive);
expiredSessions.incrementAndGet();
SessionTiming timing = new SessionTiming(timeNow, timeAlive);
synchronized (sessionExpirationTiming) {
sessionExpirationTiming.add(timing);
sessionExpirationTiming.poll();
}
}
//將session從Map中移除
if (session.getIdInternal() != null) {
sessions.remove(session.getIdInternal());
}
}
複製代碼
主動銷燬
咱們能夠經過調用HttpSession.invalidate()
方法來執行session銷燬操做。此方法最終調用的是StandardSession.invalidate()
方法,其代碼以下,能夠看出使session
銷燬的關鍵方法是StandardSession.expire()
public void invalidate() {
if (!isValidInternal())
throw new IllegalStateException
(sm.getString("standardSession.invalidate.ise"));
// Cause this session to expire
expire();
}
複製代碼
expire
方法的代碼以下
@Override
public void expire() {
expire(true);
}
public void expire(boolean notify) {
//省略代碼
//將session從ConcurrentHashMap中移除
manager.remove(this, true);
//被省略的代碼主要是將session被銷燬的消息通知
//到各個監聽器上
}
複製代碼
超時銷燬
除了主動銷燬以外,咱們能夠爲session設置一個過時時間,當時間到達以後session會被後臺線程主動銷燬。咱們能夠爲session設置一個比較短的過時時間,而後經過JConsole
來追蹤其調用棧,其是哪一個對象哪一個線程執行了銷燬操做。
以下圖所示,咱們爲session設置了一個30秒的超時時間。
ManagerBase.remove
方法上打上斷點,等待30秒以後,以下圖所示
Tomcat會開啓一個後臺線程,來按期執行子組件的
backgroundProcess
方法(前提是子組件被Tomcat管理且實現了
Manager
接口)
@Override
public void backgroundProcess() {
count = (count + 1) % processExpiresFrequency;
if (count == 0)
processExpires();
}
public void processExpires() {
long timeNow = System.currentTimeMillis();
Session sessions[] = findSessions();
int expireHere = 0 ;
if(log.isDebugEnabled())
log.debug("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length);
//從JConsole的圖中能夠看出isValid可能致使expire方法被調用
for (int i = 0; i < sessions.length; i++) {
if (sessions[i]!=null && !sessions[i].isValid()) {
expireHere++;
}
}
long timeEnd = System.currentTimeMillis();
if(log.isDebugEnabled())
log.debug("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + " expired sessions: " + expireHere);
processingTime += ( timeEnd - timeNow );
}
複製代碼
咱們能夠來看看接口中Manager.backgroundProcess
中註釋,簡略翻譯一下就是backgroundProcess
會被容器按期的執行,能夠用來執行session清理任務等。
/** * This method will be invoked by the context/container on a periodic * basis and allows the manager to implement * a method that executes periodic tasks, such as expiring sessions etc. */
public void backgroundProcess();
複製代碼
ConcurrentHashMap
來保存Session
,而Session
則用ConcurrentHashMap
來保存鍵值對,其結構以下圖所示。
這意味着,不要拼命的往Session裏面添加離散的數據, 把離散的數據封裝成一個對象性能會更加好 以下所示
//bad
httpSession.setAttribute("user","kesan");
httpSession.setAttribute("nickname","點贊");
httpSession.setAttribute("sex","男");
....
複製代碼
//good
User kesan = userDao.getUser()
httpSession.setAttribute("user", kesan);
複製代碼
若是你爲Session配置了監聽器,那麼對Session執行任何變動都將直接在當前線程執行監聽器的方法,所以最好不要在監聽器中執行可能會發生阻塞的方法。
Tomcat會開啓一個後臺線程來按期執行ManagerBase.backgroundProcess
方法用來檢測過時的Session並將其銷燬。
對象生成速率算法 此算法設計比較有趣,而且也能夠應用到其餘項目中,所以作以下總結。