背景:
????? access_token是公有號(hào)的全域唯一票據(jù),公眾號(hào)呼叫各介面時(shí)需使用access_token。開(kāi)發(fā)者需要進(jìn)行妥善保存。 access_token的儲(chǔ)存至少要保留512個(gè)字元空間。 access_token的有效期限目前為2小時(shí),需定時(shí)刷新,重複取得將導(dǎo)致上次取得的access_token失效。
1、為了保密appsecrect,第三方需要一個(gè)access_token獲取和刷新的中控服務(wù)器。而其他業(yè)務(wù)邏輯服務(wù)器所使用的access_token均來(lái)自于該中控服務(wù)器,不應(yīng)該各自去刷新,否則會(huì)造成access_token覆蓋而影響業(yè)務(wù); 2、目前access_token的有效期通過(guò)返回的expire_in來(lái)傳達(dá),目前是7200秒之內(nèi)的值。中控服務(wù)器需要根據(jù)這個(gè)有效時(shí)間提前去刷新新access_token。在刷新過(guò)程中,中控服務(wù)器對(duì)外輸出的依然是老access_token,此時(shí)公眾平臺(tái)后臺(tái)會(huì)保證在刷新短時(shí)間內(nèi),新老access_token都可用,這保證了第三方業(yè)務(wù)的平滑過(guò)渡; 3、access_token的有效時(shí)間可能會(huì)在未來(lái)有調(diào)整,所以中控服務(wù)器不僅需要內(nèi)部定時(shí)主動(dòng)刷新,還需要提供被動(dòng)刷新access_token的接口,這樣便于業(yè)務(wù)服務(wù)器在API調(diào)用獲知access_token已超時(shí)的情況下,可以觸發(fā)access_token的刷新流程。 簡(jiǎn)單起見(jiàn),使用一個(gè)隨servlet容器一起啟動(dòng)的servlet來(lái)實(shí)現(xiàn)獲取access_token的功能,具體為:因?yàn)樵搒ervlet隨著web容器而啟動(dòng),在該servlet的init方法中觸發(fā)一個(gè)線程來(lái)獲得access_token,該線程是一個(gè)無(wú)線循環(huán)的線程,每隔2個(gè)小時(shí)刷新一次access_token。相關(guān)代碼如下: :
public class InitServlet extends HttpServlet { private static final long serialVersionUID = 1L; public void init(ServletConfig config) throws ServletException { new Thread(new AccessTokenThread()).start(); } }
?2)執(zhí)行緒程式碼:
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中的設(shè)定
##?4)servlet在web.xml中的設(shè)定#<servlet> <servlet-name>initServlet</servlet-name> <servlet-class>com.sinaapp.wx.servlet.InitServlet</servlet-class> <load-on-startup>0</load-on-startup> </servlet>
因?yàn)閕nitServlet設(shè)定了load-on-startup=0,所以保證了在所有其它servlet之前啟動(dòng)。 其它servlet要使用access_token的只需要呼叫 AccessTokenThread.accessToken即可。
引出多執(zhí)行緒並發(fā)問(wèn)題
:
#1)上面的實(shí)作似乎沒(méi)有什麼問(wèn)題,但是仔細(xì)一想,AccessTokenThread類別中的accessToken ,它存在並發(fā)訪問(wèn)的問(wèn)題,它僅僅由AccessTokenThread每隔2小時(shí)更新一次,但是會(huì)有很多線程來(lái)讀取它,它是一個(gè)典型的讀多寫(xiě)少的場(chǎng)景,而且只有一個(gè)線程寫(xiě)。既然存在並發(fā)的讀寫(xiě),那麼上面的程式碼肯定是存在問(wèn)題的。
???? 一般想到的最簡(jiǎn)單的方法是使用synchronized來(lái)處理:
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訪問(wèn)accessToken的方法。 那麼到這裡是不是就完美了呢?就沒(méi)有問(wèn)題了呢?仔細(xì)想想,還是有問(wèn)題,問(wèn)題出在AccessToken類別的定義上,它提供了public的set方法,那麼所有的線程都在使用AccessTokenThread.getAccessToken()獲得了所有線程共享的accessToken之後,任何線程都可以修改它的屬性! ! ! !而這肯定是不對(duì)的,不應(yīng)該的。
2)解決方法一
:
??? 我們讓AccessTokenThread.getAccessToken()方法傳回一個(gè)accessToken物件的copy,副本,這樣其它的執(zhí)行緒就無(wú)法修改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í)作clone方法,原理都是一樣的。當(dāng)然setAccessToken也變成了private。
3)解決方法二
:
??? 既然我們不應(yīng)該讓AccessToken的物件被修改,那我們?yōu)槭颤N不將accessToken定義成一個(gè)“不可變物件”?相關(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)子和get方法。這樣的話,其他的執(zhí)行緒在獲得了AccessToken的物件之後,就無(wú)法修改了。改修改要求AccessTokenUtil.freshAccessToken()中傳回的AccessToken的物件只能透過(guò)有參的建構(gòu)子來(lái)建立。同時(shí)AccessTokenThread的setAccessToken也要修改成private,getAccessToken無(wú)須回傳一個(gè)副本了。
注意不可變物件必須滿足下面的三個(gè)條件:
a) 物件建立之後其狀態(tài)就不能修改;
b) 物件的所有域都是final型別;c) 物件是正確建立的(即在物件的建構(gòu)子中,this引用沒(méi)有發(fā)生逸出);
4)解決方法三
:
??? 還有沒(méi)有其他更好,更完美,更有效率的方法呢?讓我們分析一下,在解決方法二中,AccessTokenUtil.freshAccessToken()傳回的是一個(gè)不可變對(duì)象,然後呼叫private的AccessTokenThread.setAccessToken(AccessToken accessToken)方法來(lái)進(jìn)行賦值。這個(gè)方法上的synchronized同步起到了什麼作用呢?因?yàn)槲锛r(shí)不可變的,而且只有一個(gè)執(zhí)行緒可以呼叫setAccessToken方法,那麼這裡的synchronized沒(méi)有起到"互斥"的作用(因?yàn)橹挥幸粋€(gè)執(zhí)行緒修改),而僅僅是起到了保證「可見(jiàn)性」的作用,讓修改對(duì)其它的執(zhí)行緒可見(jiàn),也就是讓其他執(zhí)行緒存取到的都是最新的accessToken物件。而保證「可見(jiàn)性」是可以使用volatile來(lái)進(jìn)行的,所以這裡的synchronized應(yīng)該是沒(méi)有必要的,我們使用volatile來(lái)替代它。相關(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來(lái)訪問(wèn)。但是為了后期維護(hù),最好還是不要改成public.
其實(shí)這個(gè)問(wèn)題的關(guān)鍵是:在多線程并發(fā)訪問(wèn)的環(huán)境中如何正確的發(fā)布一個(gè)共享對(duì)象。
其實(shí)我們也可以使用Executors.newScheduledThreadPool來(lái)搞定:
public class InitServlet2 extends HttpServlet { private static final long serialVersionUID = 1L; public void init(ServletConfig config) throws ServletException { ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); executor.scheduleAtFixedRate(new AccessTokenRunnable(), 0, 7200-200, TimeUnit.SECONDS); } }
public class AccessTokenRunnable implements Runnable { private static volatile AccessToken accessToken; @Override public void run() { 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(); } } public static AccessToken getAccessToken() { while(accessToken == null){ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } return accessToken; } }
獲取accessToken方式變成了:AccessTokenRunnable.getAccessToken();
?更多由獲取微信access_token引出的Java多線程并發(fā)問(wèn)題相關(guān)文章請(qǐng)關(guān)注PHP中文網(wǎng)!

熱AI工具

Undress AI Tool
免費(fèi)脫衣圖片

Undresser.AI Undress
人工智慧驅(qū)動(dòng)的應(yīng)用程序,用於創(chuàng)建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費(fèi)的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門(mén)文章

熱工具

記事本++7.3.1
好用且免費(fèi)的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強(qiáng)大的PHP整合開(kāi)發(fā)環(huán)境

Dreamweaver CS6
視覺(jué)化網(wǎng)頁(yè)開(kāi)發(fā)工具

SublimeText3 Mac版
神級(jí)程式碼編輯軟體(SublimeText3)

熱門(mén)話題

PHP是一種開(kāi)源的腳本語(yǔ)言,廣泛應(yīng)用於網(wǎng)頁(yè)開(kāi)發(fā)和伺服器端編程,尤其在微信開(kāi)發(fā)中得到了廣泛的應(yīng)用。如今,越來(lái)越多的企業(yè)和開(kāi)發(fā)者開(kāi)始使用PHP進(jìn)行微信開(kāi)發(fā),因?yàn)樗蔀榱苏嬲囊讓W(xué)易用的開(kāi)發(fā)語(yǔ)言。在微信開(kāi)發(fā)中,訊息的加密和解密是一個(gè)非常重要的問(wèn)題,因?yàn)樗鼈兩婕百Y料的安全性。對(duì)於沒(méi)有加密和解密方式的消息,駭客可以輕鬆取得其中的數(shù)據(jù),對(duì)用戶造成威脅

在微信公眾號(hào)開(kāi)發(fā)中,投票功能經(jīng)常被運(yùn)用。投票功能是讓使用者快速參與互動(dòng)的好方式,也是舉辦活動(dòng)和調(diào)查意見(jiàn)的重要工具。本文將為您介紹如何使用PHP實(shí)作微信投票功能。在取得微信公眾號(hào)授權(quán)首先,你需要取得微信公眾號(hào)的授權(quán)。在微信公眾平臺(tái)上,你需要設(shè)定微信公眾號(hào)碼的api地址、官方帳號(hào)和公眾號(hào)碼對(duì)應(yīng)的token。在我們使用PHP語(yǔ)言開(kāi)發(fā)的過(guò)程中,我們需要使用微信官方提供的PH

隨著微信的普及,越來(lái)越多的企業(yè)開(kāi)始將其作為行銷(xiāo)工具。而微信群發(fā)功能,則是企業(yè)進(jìn)行微信行銷(xiāo)的重要手段之一。但是,如果只依靠手動(dòng)發(fā)送,對(duì)於行銷(xiāo)人員來(lái)說(shuō)是一件極為費(fèi)時(shí)費(fèi)力的工作。所以,開(kāi)發(fā)一款微信群發(fā)工具就顯得格外重要。本文將介紹如何使用PHP開(kāi)發(fā)微信群發(fā)工具。一、準(zhǔn)備工作開(kāi)發(fā)微信群發(fā)工具,我們需要掌握以下幾個(gè)技術(shù)點(diǎn):PHP基礎(chǔ)知識(shí)微信公眾平臺(tái)開(kāi)發(fā)開(kāi)發(fā)工具:Sub

微信是目前全球用戶規(guī)模最大的社群平臺(tái)之一,隨著行動(dòng)網(wǎng)路的普及,越來(lái)越多的企業(yè)開(kāi)始意識(shí)到微信行銷(xiāo)的重要性。在進(jìn)行微信行銷(xiāo)時(shí),客服服務(wù)是至關(guān)重要的一環(huán)。為了更好地管理客服聊天窗口,我們可以藉助PHP語(yǔ)言進(jìn)行微信開(kāi)發(fā)。一、PHP微信開(kāi)發(fā)簡(jiǎn)介PHP是一種開(kāi)源的伺服器端腳本語(yǔ)言,廣泛用於Web開(kāi)發(fā)領(lǐng)域。結(jié)合微信公眾平臺(tái)提供的開(kāi)發(fā)接口,我們可以使用PHP語(yǔ)言進(jìn)行微信

在微信公眾號(hào)開(kāi)發(fā)中,使用者標(biāo)籤管理是一個(gè)非常重要的功能,可以讓開(kāi)發(fā)者更了解和管理自己的使用者。本篇文章將介紹如何使用PHP實(shí)作微信使用者標(biāo)籤管理功能。一、取得微信用戶openid在使用微信用戶標(biāo)籤管理功能之前,我們首先需要取得用戶的openid。在微信公眾號(hào)開(kāi)發(fā)中,透過(guò)使用者授權(quán)的方式取得openid是比較常見(jiàn)的做法。在使用者授權(quán)完成後,我們可以透過(guò)以下程式碼取得用

隨著微信成為了人們生活中越來(lái)越重要的通訊工具,其敏捷的訊息傳遞功能迅速受到廣大企業(yè)和個(gè)人的青睞。對(duì)企業(yè)而言,將微信發(fā)展為一個(gè)行銷(xiāo)平臺(tái)已經(jīng)成為趨勢(shì),而微信開(kāi)發(fā)的重要性也逐漸凸顯。在其中,群發(fā)功能更是被廣泛使用,那麼,作為PHP程式設(shè)計(jì)師,如何實(shí)現(xiàn)群發(fā)訊息發(fā)送記錄呢?以下將為大家簡(jiǎn)單介紹一下。 1.了解微信公眾號(hào)相關(guān)開(kāi)發(fā)知識(shí)在了解如何實(shí)現(xiàn)群發(fā)訊息發(fā)送記錄之前,我

如何使用PHP實(shí)現(xiàn)微信公眾號(hào)開(kāi)發(fā)微信公眾號(hào)已經(jīng)成為了許多企業(yè)推廣和互動(dòng)的重要管道,而PHP作為常用的Web語(yǔ)言,也可以用來(lái)進(jìn)行微信公眾號(hào)的開(kāi)發(fā)。本文將介紹使用PHP實(shí)現(xiàn)微信公眾號(hào)開(kāi)發(fā)的具體步驟。第一步:取得微信公眾號(hào)的開(kāi)發(fā)者帳號(hào)在開(kāi)始微信公眾號(hào)開(kāi)發(fā)之前,需要先去申請(qǐng)一個(gè)微信公眾號(hào)的開(kāi)發(fā)者帳號(hào)。具體的註冊(cè)流程可參考微信公眾平臺(tái)的官方網(wǎng)

隨著網(wǎng)路和行動(dòng)智慧型裝置的發(fā)展,微信成為了社交和行銷(xiāo)領(lǐng)域不可或缺的一部分。在這個(gè)越來(lái)越數(shù)位化的時(shí)代,如何使用PHP進(jìn)行微信開(kāi)發(fā)已經(jīng)成為了許多開(kāi)發(fā)者的關(guān)注點(diǎn)。本文主要介紹如何使用PHP進(jìn)行微信發(fā)展的相關(guān)知識(shí)點(diǎn),以及其中的一些技巧和注意事項(xiàng)。一、開(kāi)發(fā)環(huán)境準(zhǔn)備在進(jìn)行微信開(kāi)發(fā)之前,首先需要準(zhǔn)備好對(duì)應(yīng)的開(kāi)發(fā)環(huán)境。具體來(lái)說(shuō),需要安裝PHP的運(yùn)作環(huán)境,以及微信公眾平臺(tái)提
