ホームページ  >  記事  >  WeChat アプレット  >  WeChat access_token の取得によって発生する Java マルチスレッド同時実行の問題

WeChat access_token の取得によって発生する Java マルチスレッド同時実行の問題

高洛峰
高洛峰オリジナル
2017-02-28 09:44:062102ブラウズ

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
  20d42bb762ac7d7e594da3a264e47fcc

initServlet は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中文网!

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。