背景 : access_token是公眾號(hào)的全局唯一票據(jù),公眾號(hào)調(diào)用各接口時(shí)都需使用access_token。開發(fā)者需要進(jìn)行妥善保存。access_token的存儲(chǔ)至少要保留512個(gè)字符空間。access_token的有效期目前為2個(gè)小時(shí),需定時(shí)刷新,重復(fù) 獲取 將導(dǎo)致上次 獲取 的access_token
背景:
access_token是公眾號(hào)的全局唯一票據(jù),公眾號(hào)調(diào)用各接口時(shí)都需使用access_token。開發(fā)者需要進(jìn)行妥善保存。access_token的存儲(chǔ)至少要保留512個(gè)字符空間。access_token的有效期目前為2個(gè)小時(shí),需定時(shí)刷新,重復(fù)獲取將導(dǎo)致上次獲取的access_token失效。
1、為了保密appsecrect,第三方需要一個(gè)access_token獲取和刷新的中控服務(wù)器。而其他業(yè)務(wù)邏輯服務(wù)器所使用的access_token均來自于該中控服務(wù)器,不應(yīng)該各自去刷新,否則會(huì)造成access_token覆蓋而影響業(yè)務(wù); 2、目前access_token的有效期通過返回的expire_in來傳達(dá),目前是7200秒之內(nèi)的值。中控服務(wù)器需要根據(jù)這個(gè)有效時(shí)間提前去刷新新access_token。在刷新過程中,中控服務(wù)器對(duì)外
簡(jiǎn)單起見,使用一個(gè)隨servlet容器一起啟動(dòng)的servlet來實(shí)現(xiàn)獲取access_token的功能,具體為:因?yàn)樵搒ervlet隨著web容器而啟動(dòng),在該servlet的init方法中觸發(fā)一個(gè)線程來獲得access_token,該線程是一個(gè)無線循環(huán)的線程,每隔2個(gè)小時(shí)刷新一次access_token。相關(guān)代碼如下:
1)servlet代碼:
public class InitServlet extends HttpServlet { private static final long serialVersionUID = 1L; public void init(ServletConfig config) throws ServletException { new Thread(new AccessTokenThread()).start(); } }
2)線程代碼:
public class AccessTokenThread implements Runnable { public static AccessToken accessToken; @Override public void run() { while(true) { try{ AccessToken token = AccessTokenUtil.freshAccessToken(); // 從微信服務(wù)器刷新access_token if(token != null){ accessToken = token; }else{ System.out.println("get access_token failed------------------------------"); } }catch(IOException e){ e.printStackTrace(); } try{ if(null != accessToken){ Thread.sleep((accessToken.getExpire_in() - 200) * 1000); // 休眠7000秒 }else{ Thread.sleep(60 * 1000); // 如果access_token為null,60秒后再獲取 } }catch(InterruptedException e){ try{ Thread.sleep(60 * 1000); }catch(InterruptedException e1){ e1.printStackTrace(); } } } } }
3)AccessToken代碼:
public class AccessToken { private String access_token; private long expire_in; // access_token有效時(shí)間,單位為妙 public String getAccess_token() { return access_token; } public void setAccess_token(String access_token) { this.access_token = access_token; } public long getExpire_in() { return expire_in; } public void setExpire_in(long expire_in) { this.expire_in = expire_in; } }
4)servlet在web.xml中的配置
initServlet com.sinaapp.wx.servlet.InitServlet 0
因?yàn)閕nitServlet設(shè)置了load-on-startup=0,所以保證了在所有其它servlet之前啟動(dòng)。
其它servlet要使用access_token的只需要調(diào)用 AccessTokenThread.accessToken即可。
引出多線程并發(fā)問題:
1)上面的實(shí)現(xiàn)似乎沒有什么問題,但是仔細(xì)一想,AccessTokenThread類中的accessToken,它存在并發(fā)訪問的問題,它僅僅由AccessTokenThread每隔2小時(shí)更新一次,但是會(huì)有很多線程來讀取它,它是一個(gè)典型的讀多寫少的場(chǎng)景,而且只有一個(gè)線程寫。既然存在并發(fā)的讀寫,那么上面的代碼肯定是存在問題的。
一般想到的最簡(jiǎn)單的方法是使用synchronized來處理:
public class AccessTokenThread implements Runnable { private static AccessToken accessToken; @Override public void run() { while(true) { try{ AccessToken token = AccessTokenUtil.freshAccessToken(); // 從微信服務(wù)器刷新access_token if(token != null){ AccessTokenThread.setAccessToken(token); }else{ System.out.println("get access_token failed"); } }catch(IOException e){ e.printStackTrace(); } try{ if(null != accessToken){ Thread.sleep((accessToken.getExpire_in() - 200) * 1000); // 休眠7000秒 }else{ Thread.sleep(60 * 1000); // 如果access_token為null,60秒后再獲取 } }catch(InterruptedException e){ try{ Thread.sleep(60 * 1000); }catch(InterruptedException e1){ e1.printStackTrace(); } } } } public synchronized static AccessToken getAccessToken() { return accessToken; } private synchronized static void setAccessToken(AccessToken accessToken) { AccessTokenThread2.accessToken = accessToken; } }
accessToken變成了private,setAccessToken也變成了private,增加了同步synchronized訪問accessToken的方法。
那么到這里是不是就完美了呢?就沒有問題了呢?仔細(xì)想想,還是有問題,問題出在AccessToken類的定義上,它提供了public的set方法,那么所有的線程都在使用AccessTokenThread.getAccessToken()獲得了所有線程共享的accessToken之后,任何線程都可以修改它的屬性!!!!而這肯定是不對(duì)的,不應(yīng)該的。
2)解決方法一:
我們讓AccessTokenThread.getAccessToken()方法返回一個(gè)accessToken對(duì)象的copy,副本,這樣其它的線程就無法修改AccessTokenThread類中的accessToken了。如下修改AccessTokenThread.getAccessToken()方法即可:
public synchronized static AccessToken getAccessToken() { AccessToken at = new AccessToken(); at.setAccess_token(accessToken.getAccess_token()); at.setExpire_in(accessToken.getExpire_in()); return at; }
也可以在AccessToken類中實(shí)現(xiàn)clone方法,原理都是一樣的。當(dāng)然setAccessToken也變成了private。
3)解決方法二:
既然我們不應(yīng)該讓AccessToken的對(duì)象被修改,那么我們?yōu)槭裁床粚ccessToken定義成一個(gè)“不可變對(duì)象”?相關(guān)修改如下:
public class AccessToken { private final String access_token; private final long expire_in; // access_token有效時(shí)間,單位為妙 public AccessToken(String access_token, long expire_in) { this.access_token = access_token; this.expire_in = expire_in; } public String getAccess_token() { return access_token; } public long getExpire_in() { return expire_in; } }
如上所示,AccessToken所有的屬性都定義成了final類型了,只提供構(gòu)造函數(shù)和get方法。這樣的話,其他的線程在獲得了AccessToken的對(duì)象之后,就無法修改了。改修改要求AccessTokenUtil.freshAccessToken()中返回的AccessToken的對(duì)象只能通過有參的構(gòu)造函數(shù)來創(chuàng)建。同時(shí)AccessTokenThread的setAccessToken也要修改成private,getAccessToken無須返回一個(gè)副本了。
注意不可變對(duì)象必須滿足下面的三個(gè)條件:
a) 對(duì)象創(chuàng)建之后其狀態(tài)就不能修改;
b) 對(duì)象的所有域都是final類型;
c) 對(duì)象是正確創(chuàng)建的(即在對(duì)象的構(gòu)造函數(shù)中,this引用沒有發(fā)生逸出);
4)解決方法三:
還有沒有其他更加好,更加完美,更加高效的方法呢?我們分析一下,在解決方法二中,AccessTokenUtil.freshAccessToken()返回的是一個(gè)不可變對(duì)象,然后調(diào)用private的AccessTokenThread.setAccessToken(AccessToken accessToken)方法來進(jìn)行賦值。這個(gè)方法上的synchronized同步起到了什么作用呢?因?yàn)閷?duì)象時(shí)不可變的,而且只有一個(gè)線程可以調(diào)用setAccessToken方法,那么這里的synchronized沒有起到"互斥"的作用(因?yàn)橹挥幸粋€(gè)線程修改),而僅僅是起到了保證“可見性”的作用,讓修改對(duì)其它的線程可見,也就是讓其他線程訪問到的都是最新的accessToken對(duì)象。而保證“可見性”是可以使用volatile來進(jìn)行的,所以這里的synchronized應(yīng)該是沒有必要的,我們使用volatile來替代它。相關(guān)修改代碼如下:
public class AccessTokenThread implements Runnable { private static volatile AccessToken accessToken; @Override public void run() { while(true) { try{ AccessToken token = AccessTokenUtil.freshAccessToken(); // 從微信服務(wù)器刷新access_token if(token != null){ AccessTokenThread2.setAccessToken(token); }else{ System.out.println("get access_token failed"); } }catch(IOException e){ e.printStackTrace(); } try{ if(null != accessToken){ Thread.sleep((accessToken.getExpire_in() - 200) * 1000); // 休眠7000秒 }else{ Thread.sleep(60 * 1000); // 如果access_token為null,60秒后再獲取 } }catch(InterruptedException e){ try{ Thread.sleep(60 * 1000); }catch(InterruptedException e1){ e1.printStackTrace(); } } } } private static void setAccessToken(AccessToken accessToken) { AccessTokenThread2.accessToken = accessToken; }
public static AccessToken getAccessToken() {
return accessToken;
} }
也可以這樣改:
public class AccessTokenThread implements Runnable { private static volatile AccessToken accessToken; @Override public void run() { while(true) { try{ AccessToken token = AccessTokenUtil.freshAccessToken(); // 從微信服務(wù)器刷新access_token if(token != null){ accessToken = token; }else{ System.out.println("get access_token failed"); } }catch(IOException e){ e.printStackTrace(); } try{ if(null != accessToken){ Thread.sleep((accessToken.getExpire_in() - 200) * 1000); // 休眠7000秒 }else{ Thread.sleep(60 * 1000); // 如果access_token為null,60秒后再獲取 } }catch(InterruptedException e){ try{ Thread.sleep(60 * 1000); }catch(InterruptedException e1){ e1.printStackTrace(); } } } } public static AccessToken getAccessToken() { return accessToken; } }
還可以這樣改:
public class AccessTokenThread implements Runnable { public static volatile AccessToken accessToken; @Override public void run() { while(true) { try{ AccessToken token = AccessTokenUtil.freshAccessToken(); // 從微信服務(wù)器刷新access_token if(token != null){ accessToken = token; }else{ System.out.println("get access_token failed"); } }catch(IOException e){ e.printStackTrace(); } try{ if(null != accessToken){ Thread.sleep((accessToken.getExpire_in() - 200) * 1000); // 休眠7000秒 }else{ Thread.sleep(60 * 1000); // 如果access_token為null,60秒后再獲取 } }catch(InterruptedException e){ try{ Thread.sleep(60 * 1000); }catch(InterruptedException e1){ e1.printStackTrace(); } } } } }
accesToken變成了public,可以直接是一個(gè)AccessTokenThread.accessToken來訪問。
其實(shí)這個(gè)問題的關(guān)鍵是:在多線程并發(fā)訪問的環(huán)境中如何正確的發(fā)布一個(gè)共享對(duì)象。
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com