배경:
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의 서블릿 구성
<servlet> <servlet-name>initServlet</servlet-name> <servlet-class>com.sinaapp.wx.servlet.InitServlet</servlet-class> <load-on-startup>0</load-on-startup> </servlet>
initServlet은 load-on-startup=0을 설정하기 때문에 다른 모든 서블릿보다 먼저 시작되도록 보장됩니다.
access_token을 사용하려는 다른 서블릿은 AccessTokenThread.accessToken만 호출하면 됩니다.
멀티 스레드 동시성 문제로 이어짐 :
1) 위 구현에는 문제가 없을 것 같지만, 생각해보면 AccessTokenThread 클래스의 accessToken에는 동시 액세스 문제가 있습니다. 이는 AccessTokenThread에 의해 2시간마다 업데이트되지만 이를 읽을 스레드가 많아지는 것은 읽기가 많고 쓰기가 적은 일반적인 시나리오입니다. 단 하나의 스레드만 씁니다. 동시 읽기와 쓰기가 있으므로 위 코드에는 문제가 있는 것으로 보입니다.
가장 쉽게 생각하는 방법은 동기화를 사용하는 것입니다.
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.
그럼 이제 완벽해진 걸까요? 문제 없나요? 신중하게 생각해 보면 여전히 문제가 있습니다. 이는 공개 설정 메소드를 제공합니다. 그런 다음 모든 스레드는 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도 비공개가 되었습니다.
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도 비공개로 변경해야 하며, getAccessToken은 복사본을 반환할 필요가 없습니다.
불변 객체는 다음 세 가지 조건을 충족해야 합니다.
a) 객체가 생성된 후에는 객체의 상태를 수정할 수 없습니다.
b) 객체는 최종 유형입니다.
c) 객체가 올바르게 생성되었습니다(즉, 객체 생성자에서 이 참조가 이스케이프되지 않습니다). 해결책 3
: 더 좋고, 더 완벽하고, 더 효율적인 다른 방법이 있나요? 이를 분석해 보겠습니다. 두 번째 솔루션에서는 AccessTokenUtil.freshAccessToken()이 변경 불가능한 개체를 반환한 다음 개인 AccessTokenThread.setAccessToken(AccessToken accessToken) 메서드를 호출하여 값을 할당합니다. 이 방법에서 동기화된 동기화는 어떤 역할을 합니까? 객체는 불변이고 하나의 스레드만 setAccessToken 메소드를 호출할 수 있기 때문에 여기서 동기화는 "상호 배제" 역할을 수행하지 않고(하나의 스레드만 수정할 수 있기 때문에) "가시성"을 보장하는 역할만 수행합니다. 즉, 다른 스레드가 최신 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){ 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; } }
OK 이렇게 바꾸세요:
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中文网!