為什么要放棄session
- 現在的互聯網環境中,集群是后臺比較常見的情況,眾所周知,session其實是一個jvm內的用戶副本,如果我們要把一個集群中的用戶session做共享處理還是比較麻煩的。
- app的客戶端對session的支持會比較麻煩
基于上面的兩點,我們才會想自己來管理這一個會話。
ThreadLocal
在提到會話管理這個之前我們需要先了解一個東西ThreadLocal
.
那么ThreadLocal
是什么呢?
JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多線程程序的并發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多線程程序,ThreadLocal并不是一個Thread,而是Thread的局部變量。
那么我們這個ThreadLocal
一般用來做什么事呢?
首先,ThreadLocal 不是用來解決共享對象的多線程訪問問題的,一般情況下,通過ThreadLocal.set() 到線程中的對象是該線程自己使用的對象,其他線程是不需要訪問的,也訪問不到的。各個線程中訪問的是不同的對象。
另外,說ThreadLocal使得各線程能夠保持各自獨立的一個對象,并不是通過ThreadLocal.set()來實現的,而是通過每個線程中的new 對象 的操作來創建的對象,每個線程創建一個,不是什么對象的拷貝或副本。通過ThreadLocal.set()將這個新創建的對象的引用保存到各線程的自己的一個map中,每個線程都有這樣一個map,執行ThreadLocal.get()時,各線程從自己的map中取出放進去的對象,因此取出來的是各自自己線程中的對象,ThreadLocal實例是作為map的key來使用的。
如果ThreadLocal.set()進去的東西本來就是多個線程共享的同一個對象,那么多個線程的ThreadLocal.get()取得的還是這個共享對象本身,還是有并發訪問問題。
看了上面的描述,你應該能很清晰的明白了ThreadLocal
的定義。對,他就是用Thread作為key來存儲對應的線程副本變量的。
如何用ThreadLocal來達到我們的效果
大家應該知道,我們部署在tomcat容器下的jersey服務,每次請求都會對應著一個新開啟的用戶線程。
這樣也就意味著我們的每次請求都是一個會話開啟到結束的過程,那么從我們會話開啟的過程中,如何在我們的前置請求中去攔截我們的用戶請求,達到一個驗證是否是我們的用戶,然后如果是我們的用戶的話,那么他對應的是哪個用戶呢?
帶著這樣的疑問,我們想到了之前我寫的那篇文章jersey利用filter和Dynamic binding來實現token攔截過濾請求.
在我們的fifter中的請求攔截的時候,我們會找到我們的token,根據token來判斷是否是我們的用戶。
那么在我們fifter中我就可以做這樣的一件事。我們利用在fifter時候攔截token的用戶鑒別來吧用戶信息存儲到一個中間介質的ThreadLocal
變量中,在我們的下游api層的時候就可以直接去ThreadLocal
中取得是哪一個用戶來進行的請求。
但是這其中有一個問題,那就是我們的fifter和下游的api層是不是同一個線程呢?因為ThreadLocal
變量的介質如果不是同一個線程就會取不到值。但是很幸運,我們的fifter和下游的api層是在我們的jersey中是同一個線程。
那ok,我們的所有規劃都已完成,那具體的ThreadLocal
實現是什么樣子的呢?
public class InvocationContext {
private static final ThreadLocal<InvocationContext> context =
new ThreadLocal<InvocationContext>();
private UserInfo userInfo;
private Map<String, String> params;
private InvocationContext(Map<String, String> params) {
this.params = params;
}
private InvocationContext(UserInfo userInfo) {
this.userInfo = userInfo;
}
public static InvocationContext getContext() {
return context.get();
}
public static void initContext(Map<String, String> params) {
context.set(new InvocationContext(params));
}
public static void clear() {
context.set(null);
context.remove();
}
public Map<String, String> getParams() {
return params;
}
public void setParams(Map<String, String> params) {
this.params = params;
}
public String getParam(String param) {
return params.get(param);
}
public String getUserId() {
return userInfo == null ? "" : userInfo.getId();
}
public UserInfo getUserInfo() {
return userInfo;
}
public void setUserInfo(UserInfo userInfo) {
this.userInfo = userInfo;
}
/**
* 設置會話級別的session元素值
* @param userInfo
*/
public static void initThreadContext(UserInfo userInfo) {
context.set(new InvocationContext(userInfo));
}
}
具體怎么使用我們這一塊的InvocationContext呢?
首先在fifter的token攔截鑒別用戶成功之后調用initThreadContext方法傳遞userInfo的信息,然后在我們的整個api會話層的下游的只需要調用InvocationContext.getContext().getUserInfo()
方法就能獲取本次請求的userInfo的信息了。
那么請注意的一點ThreadLocal是可能引起內存泄露的。
解決ThreadLocal的內存泄露
那么ThreadLocal的內存泄露是由什么原因引起的呢?
threadlocal里面使用了一個存在弱引用的map,當釋放掉threadlocal的強引用以后,map里面的value卻沒有被回收.而這塊value永遠不會被訪問到了. 所以存在著內存泄露. 最好的做法是將調用threadlocal的remove方法.
每個thread中都存在一個map, map的類型是ThreadLocal.ThreadLocalMap. Map中的key為一個threadlocal實例. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向threadlocal. 當把threadlocal實例置為null以后,沒有任何強引用指向threadlocal實例,所以threadlocal將會被gc回收. 但是,我們的value卻不能回收,因為存在一條從current thread連接過來的強引用. 只有當前thread結束以后, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將全部被GC回收.
所以得出一個結論就是只要這個線程對象被gc回收,就不會出現內存泄露,但在threadLocal設為null和線程結束這段時間不會被回收的,就發生了我們認為的內存泄露。其實這是一個對概念理解的不一致,也沒什么好爭論的。最要命的是線程對象不被回收的情況,這就發生了真正意義上的內存泄露。比如使用線程池的時候,線程結束是不會銷毀的,會再次使用的。就可能出現內存泄露。
PS.Java為了最小化減少內存泄露的可能性和影響,在ThreadLocal的get,set的時候都會清除線程Map里所有key為null的value。所以最怕的情況就是,threadLocal對象設null了,開始發生“內存泄露”,然后使用線程池,這個線程結束,線程放回線程池中不銷毀,這個線程一直不被使用,或者分配使用了又不再調用get,set方法,那么這個期間就會發生真正的內存泄露。
看了上面的解釋之后,我們知道在本次線程會話結束后需要設置threadlocal
的set
方法為null
。并且調用remove
方法就可以解決threadlocal
的內存泄露問題。
大家應該也注意到我上面InvocationContext
代碼中的clear
方法了,那么什么時候該調用clear
方法呢。
我們是選擇在一次serverlet請求結束的時候調用該方法。具體代碼如下:
public class HttpServletRequestListener implements ServletRequestListener {
/**
* 銷毀會話session,防止內存泄露
* @param sre
*/
@Override
public void requestDestroyed(ServletRequestEvent sre) {
InvocationContext.clear();
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
}
}
然后在我們的項目中的Clear
web.xml文件中聲明這個
listener`就ok了