由獲取微信access_token引出的Java多線程並發問題


背景

      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。相關代碼如下:
1)servlet代碼
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)servlet在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,所以保證了在所有其它servlet之前啟動。

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

引出多線程並發問題

1)上面的實現似乎沒有什么問題,但是仔細一想,AccessTokenThread類中的accessToken,它存在並發訪問的問題,它僅僅由AccessTokenThread每隔2小時更新一次,但是會有很多線程來讀取它,它是一個典型的讀多寫少的場景,而且只有一個線程寫。既然存在並發的讀寫,那么上面的代碼肯定是存在問題的。

     一般想到的最簡單的方法是使用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變成了private,setAccessToken也變成了private,增加了同步synchronized訪問accessToken的方法。

那么到這里是不是就完美了呢?就沒有問題了呢?仔細想想,還是有問題,問題出在AccessToken類的定義上,它提供了public的set方法,那么所有的線程都在使用AccessTokenThread.getAccessToken()獲得了所有線程共享的accessToken之后,任何線程都可以修改它的屬性!!!!而這肯定是不對的,不應該的。

2)解決方法一

    我們讓AccessTokenThread.getAccessToken()方法返回一個accessToken對象的copy,副本,這樣其它的線程就無法修改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)解決方法二

    既然我們不應該讓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所有的屬性都定義成了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來替代它。相關修改代碼如下:

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();

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM