為什么要放棄session
- 現(xiàn)在的互聯(lián)網(wǎng)環(huán)境中悼嫉,集群是后臺比較常見的情況,眾所周知薪介,session其實(shí)是一個jvm內(nèi)的用戶副本澜搅,如果我們要把一個集群中的用戶session做共享處理還是比較麻煩的。
- app的客戶端對session的支持會比較麻煩
基于上面的兩點(diǎn)粘咖,我們才會想自己來管理這一個會話蚣抗。
ThreadLocal
在提到會話管理這個之前我們需要先了解一個東西ThreadLocal
.
那么ThreadLocal
是什么呢?
JDK 1.2的版本中就提供java.lang.ThreadLocal瓮下,ThreadLocal為解決多線程程序的并發(fā)問題提供了一種新的思路翰铡。使用這個工具類可以很簡潔地編寫出優(yōu)美的多線程程序,ThreadLocal并不是一個Thread讽坏,而是Thread的局部變量锭魔。
那么我們這個ThreadLocal
一般用來做什么事呢?
首先,ThreadLocal 不是用來解決共享對象的多線程訪問問題的路呜,一般情況下迷捧,通過ThreadLocal.set() 到線程中的對象是該線程自己使用的對象织咧,其他線程是不需要訪問的,也訪問不到的漠秋。各個線程中訪問的是不同的對象烦感。
另外,說ThreadLocal使得各線程能夠保持各自獨(dú)立的一個對象膛堤,并不是通過ThreadLocal.set()來實(shí)現(xiàn)的手趣,而是通過每個線程中的new 對象 的操作來創(chuàng)建的對象,每個線程創(chuàng)建一個肥荔,不是什么對象的拷貝或副本绿渣。通過ThreadLocal.set()將這個新創(chuàng)建的對象的引用保存到各線程的自己的一個map中,每個線程都有這樣一個map燕耿,執(zhí)行ThreadLocal.get()時中符,各線程從自己的map中取出放進(jìn)去的對象,因此取出來的是各自自己線程中的對象誉帅,ThreadLocal實(shí)例是作為map的key來使用的淀散。
如果ThreadLocal.set()進(jìn)去的東西本來就是多個線程共享的同一個對象,那么多個線程的ThreadLocal.get()取得的還是這個共享對象本身蚜锨,還是有并發(fā)訪問問題档插。
看了上面的描述,你應(yīng)該能很清晰的明白了ThreadLocal
的定義亚再。對郭膛,他就是用Thread作為key來存儲對應(yīng)的線程副本變量的。
如何用ThreadLocal來達(dá)到我們的效果
大家應(yīng)該知道氛悬,我們部署在tomcat容器下的jersey服務(wù)则剃,每次請求都會對應(yīng)著一個新開啟的用戶線程。
這樣也就意味著我們的每次請求都是一個會話開啟到結(jié)束的過程如捅,那么從我們會話開啟的過程中棍现,如何在我們的前置請求中去攔截我們的用戶請求,達(dá)到一個驗(yàn)證是否是我們的用戶镜遣,然后如果是我們的用戶的話己肮,那么他對應(yīng)的是哪個用戶呢?
帶著這樣的疑問烈涮,我們想到了之前我寫的那篇文章jersey利用filter和Dynamic binding來實(shí)現(xiàn)token攔截過濾請求.
在我們的fifter中的請求攔截的時候朴肺,我們會找到我們的token窖剑,根據(jù)token來判斷是否是我們的用戶坚洽。
那么在我們fifter中我就可以做這樣的一件事。我們利用在fifter時候攔截token的用戶鑒別來吧用戶信息存儲到一個中間介質(zhì)的ThreadLocal
變量中西土,在我們的下游api層的時候就可以直接去ThreadLocal
中取得是哪一個用戶來進(jìn)行的請求讶舰。
但是這其中有一個問題,那就是我們的fifter和下游的api層是不是同一個線程呢?因?yàn)?code>ThreadLocal變量的介質(zhì)如果不是同一個線程就會取不到值跳昼。但是很幸運(yùn)般甲,我們的fifter和下游的api層是在我們的jersey中是同一個線程。
那ok鹅颊,我們的所有規(guī)劃都已完成敷存,那具體的ThreadLocal
實(shí)現(xiàn)是什么樣子的呢?
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;
}
/**
* 設(shè)置會話級別的session元素值
* @param userInfo
*/
public static void initThreadContext(UserInfo userInfo) {
context.set(new InvocationContext(userInfo));
}
}
具體怎么使用我們這一塊的InvocationContext呢堪伍?
首先在fifter的token攔截鑒別用戶成功之后調(diào)用initThreadContext方法傳遞userInfo的信息锚烦,然后在我們的整個api會話層的下游的只需要調(diào)用InvocationContext.getContext().getUserInfo()
方法就能獲取本次請求的userInfo的信息了。
那么請注意的一點(diǎn)ThreadLocal是可能引起內(nèi)存泄露的帝雇。
解決ThreadLocal的內(nèi)存泄露
那么ThreadLocal的內(nèi)存泄露是由什么原因引起的呢涮俄?
threadlocal里面使用了一個存在弱引用的map,當(dāng)釋放掉threadlocal的強(qiáng)引用以后,map里面的value卻沒有被回收.而這塊value永遠(yuǎn)不會被訪問到了. 所以存在著內(nèi)存泄露. 最好的做法是將調(diào)用threadlocal的remove方法.
每個thread中都存在一個map, map的類型是ThreadLocal.ThreadLocalMap. Map中的key為一個threadlocal實(shí)例. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向threadlocal. 當(dāng)把threadlocal實(shí)例置為null以后,沒有任何強(qiáng)引用指向threadlocal實(shí)例,所以threadlocal將會被gc回收. 但是,我們的value卻不能回收,因?yàn)榇嬖谝粭l從current thread連接過來的強(qiáng)引用. 只有當(dāng)前thread結(jié)束以后, current thread就不會存在棧中,強(qiáng)引用斷開, Current Thread, Map, value將全部被GC回收.
所以得出一個結(jié)論就是只要這個線程對象被gc回收,就不會出現(xiàn)內(nèi)存泄露尸闸,但在threadLocal設(shè)為null和線程結(jié)束這段時間不會被回收的彻亲,就發(fā)生了我們認(rèn)為的內(nèi)存泄露。其實(shí)這是一個對概念理解的不一致吮廉,也沒什么好爭論的苞尝。最要命的是線程對象不被回收的情況,這就發(fā)生了真正意義上的內(nèi)存泄露宦芦。比如使用線程池的時候野来,線程結(jié)束是不會銷毀的,會再次使用的踪旷。就可能出現(xiàn)內(nèi)存泄露曼氛。
PS.Java為了最小化減少內(nèi)存泄露的可能性和影響,在ThreadLocal的get,set的時候都會清除線程Map里所有key為null的value令野。所以最怕的情況就是舀患,threadLocal對象設(shè)null了,開始發(fā)生“內(nèi)存泄露”气破,然后使用線程池聊浅,這個線程結(jié)束,線程放回線程池中不銷毀现使,這個線程一直不被使用低匙,或者分配使用了又不再調(diào)用get,set方法,那么這個期間就會發(fā)生真正的內(nèi)存泄露碳锈。
看了上面的解釋之后顽冶,我們知道在本次線程會話結(jié)束后需要設(shè)置threadlocal
的set
方法為null
。并且調(diào)用remove
方法就可以解決threadlocal
的內(nèi)存泄露問題售碳。
大家應(yīng)該也注意到我上面InvocationContext
代碼中的clear
方法了强重,那么什么時候該調(diào)用clear
方法呢绞呈。
我們是選擇在一次serverlet請求結(jié)束的時候調(diào)用該方法。具體代碼如下:
public class HttpServletRequestListener implements ServletRequestListener {
/**
* 銷毀會話session,防止內(nèi)存泄露
* @param sre
*/
@Override
public void requestDestroyed(ServletRequestEvent sre) {
InvocationContext.clear();
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
}
}
然后在我們的項(xiàng)目中的Clear
web.xml文件中聲明這個
listener`就ok了