ホームページ >WeChat アプレット >WeChatの開発 >WeChat access_token の取得によって発生する Java マルチスレッド同時実行の問題
Background:
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 ) web.xml のサーブレット設定
46309ed845064fdb06e746051efff9e0 700b5f17c4d842e4bd410f680f40946binitServlet72eca723e64ddd01187c8b4d58572fcb b472d9135dbff3dd7fcc77f5995c97d0com.sinaapp.wx.servlet.InitServlet4f01b97d64aea37f699ead4eb7bd2bbd 4781e2cbaa93c386271b418d3a01af0803065abc64b27fbca30c0905ab93e8ea0 20d42bb762ac7d7e594da3a264e47fccinitServlet はload-on-startup=0 を設定するため、他のすべてのサーブレットよりも前に開始されることが保証されます。 他のサーブレットが access_token を使用したい場合は、AccessTokenThread.accessToken を呼び出すだけで済みます。
はマルチスレッドの同時アクセスの問題を引き起こします :
1) 上記の実装には問題がないようですが、よく考えてみると、AccessTokenThread クラスの accessToken には同時アクセスの問題があります。 AccessTokenThread によってのみ 2 時間ごとに使用されます。これは 1 回更新されますが、多くのスレッドが読み取りを行い、書き込みは少なくなり、1 つのスレッドのみが書き込みを行うという典型的なシナリオです。同時読み取りと書き込みがあるため、上記のコードには問題があるはずです。 一般的に思いつく最も簡単な方法は、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 がプライベートになり、setAccessToken もプライベートになり、accessToken にアクセスする同期メソッドが追加されました。 それで、これで完璧ですか?問題ないでしょうか?よく考えてみると、問題は AccessToken クラスの定義にあり、すべてのスレッドが AccessTokenThread.getAccessToken() を使用して、すべてのスレッドで共有できるようになります。プロパティを変更してください。 ! ! !そして、これは間違いなく間違っており、行うべきではありません。
2) 解決策 1:
AccessTokenThread.getAccessToken() メソッドが accessToken オブジェクトのコピーを返すようにして、他のスレッドが 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) 解決策 2:
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 のすべてのプロパティは最終型として定義され、コンストラクターと get メソッドのみが提供されます。この場合、他のスレッドは AccessToken オブジェクトを取得した後にそれを変更できません。この変更では、AccessTokenUtil.freshAccessToken() で返される AccessToken オブジェクトがパラメーターを含むコンストラクターを通じてのみ作成できることが必要です。同時に、AccessTokenThread の setAccessToken も private に変更する必要があり、getAccessToken はコピーを返す必要はありません。 不変オブジェクトは次の 3 つの条件を満たさなければならないことに注意してください: a) オブジェクトの状態は作成後に変更できません b) オブジェクトのすべてのフィールドは最終型です。正しく作成されました (つまり、オブジェクトのコンストラクターで this 参照がエスケープされません)。 4) 解決策 3
: 他にもっと優れた、より完璧で、より効率的な方法はありますか?解決策 2 では、AccessTokenUtil.freshAccessToken() が不変オブジェクトを返し、プライベート AccessTokenThread.setAccessToken(AccessToken accessToken) メソッドを呼び出して値を割り当てます。この方法では、同期はどのような役割を果たしますか?オブジェクトは不変であり、1 つのスレッドのみが setAccessToken メソッドを呼び出すことができるため、ここでの synchronized は「相互排他」の役割を果たしません (1 つのスレッドのみがオブジェクトを変更できるため)。「可視性」を確保する役割のみを果たします。他のスレッドから見えるようにします。つまり、他のスレッドが最新の accessToken オブジェクトにアクセスできるようにします。 「可視性」を確保するには volatile を使用できるため、ここでは 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中文网!