背景:
access_token是公有號的全域唯一票據,公眾號呼叫各介面時需使用access_token。開發者需要進行妥善保存。 access_token的儲存至少要保留512個字元空間。 access_token的有效期限目前為2小時,需定時刷新,重複取得將導致上次取得的access_token失效。
1、为了保密appsecrect,第三方需要一个access_token获取和刷新的中控服务器。而其他业务逻辑服务器所使用的access_token均来自于该中控服务器,不应该各自去刷新,否则会造成access_token覆盖而影响业务; 2、目前access_token的有效期通过返回的expire_in来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新access_token。在刷新过程中,中控服务器对外输出的依然是老access_token,此时公众平台后台会保证在刷新短时间内,新老access_token都可用,这保证了第三方业务的平滑过渡; 3、access_token的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新access_token的接口,这样便于业务服务器在API调用获知access_token已超时的情况下,可以触发access_token的刷新流程。 简单起见,使用一个随servlet容器一起启动的servlet来实现获取access_token的功能,具体为:因为该servlet随着web容器而启动,在该servlet的init方法中触发一个线程来获得access_token,该线程是一个无线循环的线程,每隔2个小时刷新一次access_token。相关代码如下: :
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(); // 从微信服务器刷新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有效时间,单位为妙 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中的設定
## 4)servlet在web.xml中的設定 #46309ed845064fdb06e746051efff9e0 700b5f17c4d842e4bd410f680f40946binitServlet72eca723e64ddd01187c8b4d58572fcb b472d9135dbff3dd7fcc77f5995c97d0com.sinaapp.wx.servlet.InitServlet4f01b97d64aea37f699ead4eb7bd2bbd 4781e2cbaa93c386271b418d3a01af0803065abc64b27fbca30c0905ab93e8ea0 20d42bb762ac7d7e594da3a264e47fcc
因為initServlet設定了load-on-startup=0,所以保證了在所有其它servlet之前啟動。 其它servlet要使用access_token的只需要呼叫 AccessTokenThread.accessToken即可。
引出多執行緒並發問題
:
#1)上面的實作似乎沒有什麼問題,但是仔細一想,AccessTokenThread類別中的accessToken ,它存在並發訪問的問題,它僅僅由AccessTokenThread每隔2小時更新一次,但是會有很多線程來讀取它,它是一個典型的讀多寫少的場景,而且只有一個線程寫。既然存在並發的讀寫,那麼上面的程式碼肯定是存在問題的。
一般想到的最簡單的方法是使用synchronized來處理:
public class AccessTokenThread implements Runnable { private static AccessToken accessToken; @Override public void run() { while(true) { try{ AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新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的方法。 那麼到這裡是不是就完美了呢?就沒有問題了呢?仔細想想,還是有問題,問題出在AccessToken類別的定義上,它提供了public的set方法,那麼所有的線程都在使用AccessTokenThread.getAccessToken()獲得了所有線程共享的accessToken之後,任何線程都可以修改它的屬性! ! ! !而這肯定是不對的,不應該的。
2)解決方法一
:
我們讓AccessTokenThread.getAccessToken()方法傳回一個accessToken物件的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類別中實作clone方法,原理都是一樣的。當然setAccessToken也變成了private。
3)解決方法二
:
既然我們不應該讓AccessToken的物件被修改,那我們為什麼不將accessToken定義成一個“不可變物件”?相關修改如下:
public class AccessToken { private final String access_token; private final long expire_in; // access_token有效时间,单位为妙 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型別了,只提供建構子和get方法。這樣的話,其他的執行緒在獲得了AccessToken的物件之後,就無法修改了。改修改要求AccessTokenUtil.freshAccessToken()中傳回的AccessToken的物件只能透過有參的建構子來建立。同時AccessTokenThread的setAccessToken也要修改成private,getAccessToken無須回傳一個副本了。
注意不可變物件必須滿足下面的三個條件:
a) 物件建立之後其狀態就不能修改;
b) 物件的所有域都是final型別;c) 物件是正確建立的(即在物件的建構子中,this引用沒有發生逸出);
4)解決方法三
:
還有沒有其他更好,更完美,更有效率的方法呢?讓我們分析一下,在解決方法二中,AccessTokenUtil.freshAccessToken()傳回的是一個不可變對象,然後呼叫private的AccessTokenThread.setAccessToken(AccessToken accessToken)方法來進行賦值。這個方法上的synchronized同步起到了什麼作用呢?因為物件時不可變的,而且只有一個執行緒可以呼叫setAccessToken方法,那麼這裡的synchronized沒有起到"互斥"的作用(因為只有一個執行緒修改),而僅僅是起到了保證「可見性」的作用,讓修改對其它的執行緒可見,也就是讓其他執行緒存取到的都是最新的accessToken物件。而保證「可見性」是可以使用volatile來進行的,所以這裡的synchronized應該是沒有必要的,我們使用volatile來替代它。相關修改程式碼如下:
public class AccessTokenThread implements Runnable { private static volatile AccessToken accessToken; @Override public void run() { while(true) { try{ AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新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(); // 从微信服务器刷新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(); // 从微信服务器刷新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,可以直接是一个AccessTokenThread.accessToken来访问。但是为了后期维护,最好还是不要改成public.
其实这个问题的关键是:在多线程并发访问的环境中如何正确的发布一个共享对象。
其实我们也可以使用Executors.newScheduledThreadPool来搞定:
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(); // 从微信服务器刷新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多线程并发问题相关文章请关注PHP中文网!