背景

access_token是公众号的全局唯一票据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。

  1. 1、为了保密appsecrect,第三方需要一个access_token获取和刷新的中控服务器。而其他业务逻辑服务器所使用的access_token均来自于该中控服务器,不应该各自去刷新,否则会造成access_token覆盖而影响业务;
  2. 2、目前access_token的有效期通过返回的expire_in来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新access_token。在刷新过程中,中控服务器对外输出的依然是老access_token,此时公众平台后台会保证在刷新短时间内,新老access_token都可用,这保证了第三方业务的平滑过渡;
  3. 3access_token的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新access_token的接口,这样便于业务服务器在API调用获知access_token已超时的情况下,可以触发access_token的刷新流程。
  4.  
  5. 简单起见,使用一个随servlet容器一起启动的servlet来实现获取access_token的功能,具体为:因为该servlet随着web容器而启动,在该servletinit方法中触发一个线程来获得access_token,该线程是一个无线循环的线程,每隔2个小时刷新一次access_token。相关代码如下:
    1servlet代码
  1. public class InitServlet extends HttpServlet
  2. {
  3. private static final long serialVersionUID = 1L;
  4.  
  5. public void init(ServletConfig config) throws ServletException
  6. {
  7. new Thread(new AccessTokenThread()).start();
  8. }
  9.  
  10. }

 2)线程代码

  1. public class AccessTokenThread implements Runnable
  2. {
  3. public static AccessToken accessToken;
  4.  
  5. @Override
  6. public void run()
  7. {
  8. while(true)
  9. {
  10. try{
  11. AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新access_token
  12. if(token != null){
  13. accessToken = token;
  14. }else{
  15. System.out.println("get access_token failed------------------------------");
  16. }
  17. }catch(IOException e){
  18. e.printStackTrace();
  19. }
  20.  
  21. try{
  22. if(null != accessToken){
  23. Thread.sleep((accessToken.getExpire_in() - 200) * 1000); // 休眠7000秒
  24. }else{
  25. Thread.sleep(60 * 1000); // 如果access_token为null,60秒后再获取
  26. }
  27. }catch(InterruptedException e){
  28. try{
  29. Thread.sleep(60 * 1000);
  30. }catch(InterruptedException e1){
  31. e1.printStackTrace();
  32. }
  33. }
  34. }
  35. }
  36. }

3)AccessToken代码

  1. public class AccessToken
  2. {
  3. private String access_token;
  4. private long expire_in; // access_token有效时间,单位为妙
  5.  
  6. public String getAccess_token() {
  7. return access_token;
  8. }
  9. public void setAccess_token(String access_token) {
  10. this.access_token = access_token;
  11. }
  12. public long getExpire_in() {
  13. return expire_in;
  14. }
  15. public void setExpire_in(long expire_in) {
  16. this.expire_in = expire_in;
  17. }
  18. }

 4)servlet在web.xml中的配置

  1. <servlet>
  2. <servlet-name>initServlet</servlet-name>
  3. <servlet-class>com.sinaapp.wx.servlet.InitServlet</servlet-class>
  4. <load-on-startup>0</load-on-startup>
  5. </servlet>

因为initServlet设置了load-on-startup=0,所以保证了在所有其它servlet之前启动。

其它servlet要使用access_token的只需要调用 AccessTokenThread.accessToken即可。

引出多线程并发问题

1)上面的实现似乎没有什么问题,但是仔细一想,AccessTokenThread类中的accessToken,它存在并发访问的问题,它仅仅由AccessTokenThread每隔2小时更新一次,但是会有很多线程来读取它,它是一个典型的读多写少的场景,而且只有一个线程写。既然存在并发的读写,那么上面的代码肯定是存在问题的。

一般想到的最简单的方法是使用synchronized来处理:

  1. public class AccessTokenThread implements Runnable
  2. {
  3. private static AccessToken accessToken;
  4.  
  5. @Override
  6. public void run()
  7. {
  8. while(true)
  9. {
  10. try{
  11. AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新access_token
  12. if(token != null){
  13. AccessTokenThread.setAccessToken(token);
  14. }else{
  15. System.out.println("get access_token failed");
  16. }
  17. }catch(IOException e){
  18. e.printStackTrace();
  19. }
  20.  
  21. try{
  22. if(null != accessToken){
  23. Thread.sleep((accessToken.getExpire_in() - 200) * 1000); // 休眠7000秒
  24. }else{
  25. Thread.sleep(60 * 1000); // 如果access_token为null,60秒后再获取
  26. }
  27. }catch(InterruptedException e){
  28. try{
  29. Thread.sleep(60 * 1000);
  30. }catch(InterruptedException e1){
  31. e1.printStackTrace();
  32. }
  33. }
  34. }
  35. }
  36.  
  37. public synchronized static AccessToken getAccessToken() {
  38. return accessToken;
  39. }
  40.  
  41. private synchronized static void setAccessToken(AccessToken accessToken) {
  42. AccessTokenThread2.accessToken = accessToken;
  43. }
  44. }

accessToken变成了private,setAccessToken也变成了private,增加了同步synchronized访问accessToken的方法。

那么到这里是不是就完美了呢?就没有问题了呢?仔细想想,还是有问题,问题出在AccessToken类的定义上,它提供了public的set方法,那么所有的线程都在使用AccessTokenThread.getAccessToken()获得了所有线程共享的accessToken之后,任何线程都可以修改它的属性!!!!而这肯定是不对的,不应该的。

2)解决方法一

我们让AccessTokenThread.getAccessToken()方法返回一个accessToken对象的copy,副本,这样其它的线程就无法修改AccessTokenThread类中的accessToken了。如下修改AccessTokenThread.getAccessToken()方法即可:

  1. public synchronized static AccessToken getAccessToken() {
  2. AccessToken at = new AccessToken();
  3. at.setAccess_token(accessToken.getAccess_token());
  4. at.setExpire_in(accessToken.getExpire_in());
  5. return at;
  6. }

也可以在AccessToken类中实现clone方法,原理都是一样的。当然setAccessToken也变成了private。

3)解决方法二

既然我们不应该让AccessToken的对象被修改,那么我们为什么不将accessToken定义成一个“不可变对象”?相关修改如下:

  1. public class AccessToken
  2. {
  3. private final String access_token;
  4. private final long expire_in; // access_token有效时间,单位为妙
  5.  
  6. public AccessToken(String access_token, long expire_in)
  7. {
  8. this.access_token = access_token;
  9. this.expire_in = expire_in;
  10. }
  11.  
  12. public String getAccess_token() {
  13. return access_token;
  14. }
  15.  
  16. public long getExpire_in() {
  17. return expire_in;
  18. }
  19. }

如上所示,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来替代它。相关修改代码如下:

  1. public class AccessTokenThread implements Runnable
  2. {
  3. private static volatile AccessToken accessToken;
  4.  
  5. @Override
  6. public void run()
  7. {
  8. while(true)
  9. {
  10. try{
  11. AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新access_token
  12. if(token != null){
  13. AccessTokenThread2.setAccessToken(token);
  14. }else{
  15. System.out.println("get access_token failed");
  16. }
  17. }catch(IOException e){
  18. e.printStackTrace();
  19. }
  20.  
  21. try{
  22. if(null != accessToken){
  23. Thread.sleep((accessToken.getExpire_in() - 200) * 1000); // 休眠7000秒
  24. }else{
  25. Thread.sleep(60 * 1000); // 如果access_token为null,60秒后再获取
  26. }
  27. }catch(InterruptedException e){
  28. try{
  29. Thread.sleep(60 * 1000);
  30. }catch(InterruptedException e1){
  31. e1.printStackTrace();
  32. }
  33. }
  34. }
  35. }
  36.  
  37. private static void setAccessToken(AccessToken accessToken) {
  38. AccessTokenThread2.accessToken = accessToken;
  39. }
        public static AccessToken getAccessToken() {
             return accessToken;
         }
  40. }

也可以这样改:

  1. public class AccessTokenThread implements Runnable
  2. {
  3. private static volatile AccessToken accessToken;
  4.  
  5. @Override
  6. public void run()
  7. {
  8. while(true)
  9. {
  10. try{
  11. AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新access_token
  12. if(token != null){
  13. accessToken = token;
  14. }else{
  15. System.out.println("get access_token failed");
  16. }
  17. }catch(IOException e){
  18. e.printStackTrace();
  19. }
  20.  
  21. try{
  22. if(null != accessToken){
  23. Thread.sleep((accessToken.getExpire_in() - 200) * 1000); // 休眠7000秒
  24. }else{
  25. Thread.sleep(60 * 1000); // 如果access_token为null,60秒后再获取
  26. }
  27. }catch(InterruptedException e){
  28. try{
  29. Thread.sleep(60 * 1000);
  30. }catch(InterruptedException e1){
  31. e1.printStackTrace();
  32. }
  33. }
  34. }
  35. }
  36.  
  37. public static AccessToken getAccessToken() {
  38. return accessToken;
  39. }
  40. }

还可以这样改:

  1. public class AccessTokenThread implements Runnable
  2. {
  3. public static volatile AccessToken accessToken;
  4.  
  5. @Override
  6. public void run()
  7. {
  8. while(true)
  9. {
  10. try{
  11. AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新access_token
  12. if(token != null){
  13. accessToken = token;
  14. }else{
  15. System.out.println("get access_token failed");
  16. }
  17. }catch(IOException e){
  18. e.printStackTrace();
  19. }
  20.  
  21. try{
  22. if(null != accessToken){
  23. Thread.sleep((accessToken.getExpire_in() - 200) * 1000); // 休眠7000秒
  24. }else{
  25. Thread.sleep(60 * 1000); // 如果access_token为null,60秒后再获取
  26. }
  27. }catch(InterruptedException e){
  28. try{
  29. Thread.sleep(60 * 1000);
  30. }catch(InterruptedException e1){
  31. e1.printStackTrace();
  32. }
  33. }
  34. }
  35. }
  36. }

accesToken变成了public,可以直接是一个AccessTokenThread.accessToken来访问。但是为了后期维护,最好还是不要改成public.

其实这个问题的关键是:在多线程并发访问的环境中如何正确的发布一个共享对象。

其实我们也可以使用Executors.newScheduledThreadPool来搞定:

  1. public class InitServlet2 extends HttpServlet
  2. {
  3. private static final long serialVersionUID = 1L;
  4.  
  5. public void init(ServletConfig config) throws ServletException
  6. {
  7. ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
  8. executor.scheduleAtFixedRate(new AccessTokenRunnable(), 0, 7200-200, TimeUnit.SECONDS);
  9. }
  10. }
  1. public class AccessTokenRunnable implements Runnable
  2. {
  3. private static volatile AccessToken accessToken;
  4.  
  5. @Override
  6. public void run()
  7. {
  8. try{
  9. AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新access_token
  10. if(token != null){
  11. accessToken = token;
  12. }else{
  13. System.out.println("get access_token failed");
  14. }
  15. }catch(IOException e){
  16. e.printStackTrace();
  17. }
  18. }
  19.  
  20. public static AccessToken getAccessToken()
  21. {
  22. while(accessToken == null){
  23. try {
  24. TimeUnit.SECONDS.sleep(1);
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. }
  29. return accessToken;
  30. }
  31.  
  32. }

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

由获取微信access_token引出的Java多线程并发问题的更多相关文章

  1. 获取微信access_token

    /** * 获取微信access_token * @return mixed */function get_access_token() { $appId = C('APPID'); $secret ...

  2. python之获取微信access_token

    # -*- coding: cp936 -*- #python 27 #xiaodeng #获取微信access_token #办法一:将该url直接填写到浏览器地址中可以获得access_token ...

  3. Java 多线程并发编程一览笔录

    Java 多线程并发编程一览笔录 知识体系图: 1.线程是什么? 线程是进程中独立运行的子任务. 2.创建线程的方式 方式一:将类声明为 Thread 的子类.该子类应重写 Thread 类的 run ...

  4. Java多线程并发技术

    Java多线程并发技术 参考文献: http://blog.csdn.net/aboy123/article/details/38307539 http://blog.csdn.net/ghsau/a ...

  5. Java多线程并发02——线程的生命周期与常用方法,你都掌握了吗

    在上一章,为大家介绍了线程的一些基础知识,线程的创建与终止.本期将为各位带来线程的生命周期与常用方法.关注我的公众号「Java面典」了解更多 Java 相关知识点. 线程生命周期 一个线程不是被创建了 ...

  6. Java多线程并发05——那么多的锁你都了解了吗

    在多线程或高并发情境中,经常会为了保证数据一致性,而引入锁机制,本文将为各位带来有关锁的基本概念讲解.关注我的公众号「Java面典」了解更多 Java 相关知识点. 根据锁的各种特性,可将锁分为以下几 ...

  7. Java多线程并发04——合理使用线程池

    在此之前,我们已经了解了关于线程的基本知识,今天将为各位带来,线程池这一技术.关注我的公众号「Java面典」了解更多 Java 相关知识点. 为什么使用线程池?线程池做的工作主要是控制运行的线程的数量 ...

  8. Java多线程并发07——锁在Java中的实现

    上一篇文章中,我们已经介绍过了各种锁,让各位对锁有了一定的了解.接下来将为各位介绍锁在Java中的实现.关注我的公众号「Java面典」了解更多 Java 相关知识点. 在 Java 中主要通过使用sy ...

  9. Java多线程并发06——CAS与AQS

    在进行更近一步的了解Java锁的知识之前,我们需要先了解与锁有关的两个概念 CAS 与 AQS.关注我的公众号「Java面典」了解更多 Java 相关知识点. CAS(Compare And Swap ...

随机推荐

  1. mysql 行锁一则

       CREATE TABLE `t1` (  `id` int(11) NOT NULL DEFAULT '0',  `name` varchar(20) DEFAULT NULL,  PRIMAR ...

  2. Sql Server来龙去脉系列之二 框架和配置

    本节主要讲维持数据的元数据,以及数据库框架结构.内存管理.系统配置等.这些技术点在我们使用数据库时很少接触到,但如果要深入学习Sql Server这一章节也是不得不看.本人能力有限不能把所有核心的知识 ...

  3. 如何手动让HttpRequestBase.IsAuthenticated 和 HttpContext.User.Identity.IsAuthenticated 为true.

    今天为了重写权限验证这块需要重写AuthorizeAttribute 这个属性,看了官方文档:HttpContextBase.User.Identity.IsAuthenticated 这个必须是tr ...

  4. 自定义tab在地图进行分页显示

    @{ ViewBag.Title = "GIS地图"; Layout = null; } @model HFSoft.Plat.UIWeb.Models.MapShowDataVO ...

  5. 重新想象 Windows 8 Store Apps (41) - 打印

    [源码下载] 重新想象 Windows 8 Store Apps (41) - 打印 作者:webabcd 介绍重新想象 Windows 8 Store Apps 之 打印 示例1.需要打印的文档Pr ...

  6. jython 2.7 b3发布

    Jython 2.7b3 Bugs Fixed - [ 2108 ] Cannot set attribute to instances of AST/PythonTree (blocks pyfla ...

  7. 2015-2016 ACM-ICPC, NEERC, Southern Subregional Contest, B. Layer Cake

    Description Dasha decided to bake a big and tasty layer cake. In order to do that she went shopping ...

  8. 【Asphyre引擎】关于AsphyreTypes中OverlapRect的改动,都是泪啊!!!

    OverlapRect改动:两个参数对调了.想问问LP,这样真的好吗? Sphinx304版本的代码: function OverlapRect(const Rect1, Rect2: TRect): ...

  9. DirectShow程序运行过程简析

    这段时间一直在学习陆其明老师的<DirectShow开发指南>一书,书中对DirectShow的很多细节讲解清晰,但是却容易让人缺少对全局的把握.在学习过程中,整理了关于DirectSho ...

  10. 一个H5的3D滑动组件实现(兼容2D模式)

    起由 原始需求来源于一个项目的某个功能,要求实现3D图片轮播效果,而已有的组件大多是普通的2D图片轮播,于是重新造了一个轮子,实现了一个既支持2D,又支持3D的滑动.轮播组件. 实现思路 刚一开始肯定 ...