Maison > Applet WeChat > Développement WeChat > le corps du texte

Problèmes de concurrence multithread Java causés par l'obtention du jeton d'accès WeChat

高洛峰
Libérer: 2017-02-28 09:44:06
original
2159 Les gens l'ont consulté

Contexte :

access_token est le ticket globalement unique du compte officiel. L'access_token est requis lorsque le compte officiel appelle chaque interface. Les développeurs doivent le stocker correctement. Au moins 512 caractères d'espace doivent être réservés pour le stockage access_token. La période de validité de access_token est actuellement de 2 heures et doit être actualisée régulièrement. Une acquisition répétée rendra le dernier access_token invalide.

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。相关代码如下:
:
Copier après la connexion

public class InitServlet extends HttpServlet 
{
	private static final long serialVersionUID = 1L;

	public void init(ServletConfig config) throws ServletException 
	{
		new Thread(new AccessTokenThread()).start();  
	}

}
Copier après la connexion

2) Code du fil  :

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();
				}
			}
		}
	}
}
Copier après la connexion

3) Code 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;
	}
}
Copier après la connexion

4) servlet dans la configuration web.xml

  <servlet>
    <servlet-name>initServlet</servlet-name>
    <servlet-class>com.sinaapp.wx.servlet.InitServlet</servlet-class>
    <load-on-startup>0</load-on-startup>
  </servlet>
Copier après la connexion

Étant donné que initServlet définit load-on-startup=0, il est garanti de démarrer avant toutes les autres servlets.

Les autres servlets qui souhaitent utiliser access_token n'ont qu'à appeler AccessTokenThread.accessToken.

Entraîne des problèmes de concurrence multithread  :

1) Il ne semble y avoir aucun problème avec la mise en œuvre ci-dessus, mais si vous y réfléchissez soigneusement, le accessToken dans la classe AccessTokenThread a le problème de l'accès simultané. Il n'est mis à jour que toutes les 2 heures par AccessTokenThread, mais il y aura de nombreux threads pour le lire. C'est un scénario typique de plus de lecture et moins d'écriture. et un seul thread écrit. Puisqu'il y a des lectures et des écritures simultanées, il doit y avoir un problème avec le code ci-dessus.

La façon la plus simple de penser est d'utiliser synchronisé :

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;
	}
}
Copier après la connexion

accessToken devient privé, et setAccessToken devient également remplacé privé et ajouté une méthode synchronisée pour accéder à accessToken.

Alors c'est parfait maintenant ? Il n'y a pas de problème ? En y réfléchissant bien, il y a toujours un problème. Le problème réside dans la définition de la classe AccessToken. Elle fournit ensuite une méthode d'ensemble publique. Ensuite, tous les threads peuvent utiliser AccessTokenThread.getAccessToken() pour obtenir le accessToken partagé par tous les threads. Modifiez ses propriétés ! ! ! ! Et c’est définitivement faux et cela ne devrait pas être fait.

2) Solution 1 :

On laisse la méthode AccessTokenThread.getAccessToken() renvoyer une copie de l'objet accessToken, afin que les autres Le thread ne peut pas modifier le accessToken dans la classe AccessTokenThread. Modifiez simplement la méthode AccessTokenThread.getAccessToken() comme suit :

	public synchronized static AccessToken getAccessToken() {
		AccessToken at = new AccessToken();
		at.setAccess_token(accessToken.getAccess_token());		
		at.setExpire_in(accessToken.getExpire_in());
		return at;
	}
Copier après la connexion

Vous pouvez également implémenter la méthode clone dans la classe AccessToken. Les principes sont les mêmes. . Bien entendu, setAccessToken est également devenu privé.

3) Solution deux  :

Puisque nous ne devrions pas laisser l'objet AccessToken être modifié, alors pourquoi ne définissons-nous pas accessToken comme un Des « objets immuables » ? Les modifications pertinentes sont les suivantes :

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;
	}
}
Copier après la connexion

Comme indiqué ci-dessus, toutes les propriétés d'AccessToken sont définies comme types finaux, et seuls les constructeurs et les méthodes get sont fournis. . Dans ce cas, les autres threads ne peuvent pas le modifier après avoir obtenu l'objet AccessToken. La modification nécessite que l'objet AccessToken renvoyé dans AccessTokenUtil.freshAccessToken() ne puisse être créé que via le constructeur avec des paramètres. Dans le même temps, setAccessToken de AccessTokenThread doit également être modifié en privé et getAccessToken n'a pas besoin de renvoyer une copie.

Notez que les objets immuables doivent remplir les trois conditions suivantes :

a) L'état de l'objet ne peut pas être modifié après sa création

b) Tous les champs du ; les objets sont de type final

c) L'objet est créé correctement (c'est-à-dire que dans le constructeur de l'objet, cette référence ne s'échappe pas

4) Solution 3

Existe-t-il une autre méthode meilleure, plus parfaite et plus efficace ? Analysons-le. Dans la deuxième solution, AccessTokenUtil.freshAccessToken() renvoie un objet immuable, puis appelle la méthode privée AccessTokenThread.setAccessToken(AccessToken accessToken) pour attribuer la valeur. Quel rôle joue la synchronisation synchronisée dans cette méthode ? Parce que l'objet est immuable et qu'un seul thread peut appeler la méthode setAccessToken, synchronisé ici ne joue pas de rôle "d'exclusion mutuelle" (car un seul thread peut le modifier), mais joue uniquement un rôle en assurant la "visibilité". visible pour les autres threads, c'est-à-dire permettre aux autres threads d'accéder au dernier objet accessToken. Pour assurer la "visibilité", volatile peut être utilisé, donc la synchronisation ici ne devrait pas être nécessaire. Nous utilisons volatile pour le remplacer. Le code modifié pertinent est le suivant :

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;
        }
}
Copier après la connexion

Vous pouvez également le modifier comme ceci :

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;
	}
}
Copier après la connexion

Vous pouvez également le modifier comme ceci :

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();
                }
            }
        }
    }
}
Copier après la connexion

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);
    }
}
Copier après la connexion

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;
    }
    
}
Copier après la connexion

获取accessToken方式变成了:AccessTokenRunnable.getAccessToken();

 更多由获取微信access_token引出的Java多线程并发问题相关文章请关注PHP中文网!

Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal