Java多線程--局部變量(threadlocal)

目錄

第一概述:

第二認識ThreadLocal:

第三如何實現副本:

第四ThreadLocal的原理

第五使用場景

第六ThreadLocal與其它同步機制的比較  

第七web中應用



第一概述:

我們知道,在多線程程序中,同一個線程在某個時間段只能處理一個任務.我們希望在這個時間段內,任務的某些變量能夠和處理它的線程進行綁定,在任務需要使用這個變量的時候,這個變量能夠方便的從線程中取出來.ThreadLocal能很好的滿足這個需求,用ThreadLocal變量的程序看起來也會簡潔很多,因爲減少了變量在程序中的傳遞.

線程局部變量(ThreadLocal)的功用非常簡單,就是爲每一個使用該變量的線程都提供一個變量值的副本,是每一個線程都可以獨立地改變自己的副本,而不會和其它線程的副本衝突。從線程的角度看,就好像每一個線程都完全擁有該變量。

第二認識ThreadLocal:

      ThreadLocal並非是一個線程的本地實現版本而是線程的局部變量,

其接口如下:

        Object get() :返回當前線程的線程局部變量副本

protected Object initialValue():返回線程局部變量的初始值

void set(Object value):設置線程的局部變量副本的值
  
【特別提醒】注意的是initialValue(),該方法是一個protected的方法,顯然是爲了子類重寫而特意實現的。該方法返回當前線程在該線程局部變量的初始值,這個方法是一個延遲調用方法,在一個線程第1次調用get()或者set(Object)時才執行,並且僅執行1次。ThreadLocal中的確實實現直接返回一個null:
 

  protected Object initialValue() {

      return null;

   }


第三如何實現副本:

    ThreadLocal是如何做到爲每一個線程維護變量的副本的呢?其實實現的思路很簡單,在ThreadLocal類中有一個Map,用於存儲每一個線程的變量的副本,比如下面的示例實現:    

public class ThreadLocal{

    //線程安全Map
    private Map values = Collections.synchronizedMap(new HashMap());

   public Object get(){

      Thread curThread = Thread.currentThread();
      Object o = values.get(curThread);
      if (o == null && !values.containsKey(curThread)){
          o = initialValue();
          values.put(curThread, o);
      }
      return o;
   }

   public void set(Object newValue){
     values.put(Thread.currentThread(), newValue);
   }

   public Object initialValue(){
    return null;
   }

}
  

當然,這並不是一個工業強度的實現,但JDK中的ThreadLocal的實現總體思路也類似於此。

第四ThreadLocal的原理

 每個運行的線程都會有一個類型爲ThreadLocal.ThreadLocalMap的map,這個map就是用來存儲與這個線程綁定的變量,map的key就是ThreadLocal對象,value就是線程正在執行的任務中的某個變量的包裝類Entry。

ThreadLocal保存變量的生命週期: 

 ThreadLocal保存變量的生命週期 <=任務的生命週期<=線程的生命週期

ThreadLocal的set(null)和remove方法有什麼區別?

    set(null)把當前的ThreadLocal爲key的值設爲了空,避免線程下次再執行其他任務時被使用,但此時這個key對應的Entry值還在,只是Entry.value=null

      remove方法會把這個key對應Entry的值設爲空

      所以從重用和效率的角度來說,set(null)的性能優於remove,在實際的項目中推薦使用set(null)來回收ThreadLocal設置的值.

ThreadLocalMap的Entry爲什麼是一個weakReference?     

     使用weakReference,能夠在ThreadLocal失去強引用的時候,ThreadLocal對應的Entry能夠在下次gc時被回收,回收後的空間能夠得到複用,在一定程度下能夠避免內存泄露.(在使用ThreadLocal對象,儘量使用static,不然會使線程的ThreadLocalMap產生太多Entry,從而造成內存泄露

ThreadLocalMap的內存泄漏問題

實際上 ThreadLocalMap 中使用的 key 爲 ThreadLocal 的弱引用,弱引用的特點是,如果這個對象只存在弱引用,那麼在下一次垃圾回收的時候必然會被清理掉。

所以如果 ThreadLocal 沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來 ThreadLocalMap中使用這個 ThreadLocal 的 key 也會被清理掉。但是,value 是強引用,不會被清理,這樣一來就會出現 key 爲 null 的 value。

ThreadLocalMap實現中已經考慮了這種情況,在調用 set()、get()、remove() 方法的時候,會清理掉 key 爲 null 的記錄。如果說會出現內存泄漏,那只有在出現了 key 爲 null 的記錄後,沒有手動調用 remove() 方法,並且之後也不再調用 get()、set()、remove() 方法的情況下。

第五使用場景

如上文所述,ThreadLocal 適用於如下兩種場景

  • 每個線程需要有自己單獨的實例
  • 實例需要在多個方法中共享,但不希望被多線程共享

對於第一點,每個線程擁有自己實例,實現它的方式很多。例如可以在線程內部構建一個單獨的實例。ThreadLoca 可以以非常方便的形式滿足該需求。

對於第二點,可以在滿足第一點(每個線程有自己的實例)的條件下,通過方法間引用傳遞的形式實現。ThreadLocal 使得代碼耦合度更低,且實現更優雅

1)存儲用戶Bean

public class UserContextHolder {  
  private static final ThreadLocal<User>  userThreadLocal = new ThreadLocal<User>();  
    
  public static  User get(){  
      return userThreadLocal.get();  
  }  
    
  public static void set(User user){  
       userThreadLocal.set(user);  
  }  
}  

2)存儲用戶Session

//用戶session管理
public class SessionManger{

    private static final ThreadLocal threadSession = new ThreadLocal();

    public static Session getSession() throws InfrastructureException {
        Session s = (Session) threadSession.get();
        try {
            if (s == null) {
                s = getSessionFactory().openSession();
                threadSession.set(s);
            }
        } catch (HibernateException ex) {
            throw new InfrastructureException(ex);
        }
        return s;
    }
}

3)線程安全

比如Java7中的SimpleDateFormat不是線程安全的,可以用ThreadLocal來解決這個問題

public class DateUtil {
    private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static String formatDate(Date date) {
        return format1.get().format(date);
    }
}

【注意】:這裏的DateUtil.formatDate()就是線程安全的了。(Java8裏的 java.time.format.DateTimeFormatter是線程安全的,Joda time裏的DateTimeFormat也是線程安全的)。

在線程是活動的並且ThreadLocal對象是可訪問的時,該線程就持有一個到該線程局部變量副本的隱含引用,當該線程運行結束後,該線程擁有的所以線程局部變量的副本都將失效,並等待垃圾收集器收集。

 

第六ThreadLocal與其它同步機制的比較
  

ThreadLocal和其它同步機制相比有什麼優勢呢?ThreadLocal和其它所有的同步機制都是爲了解決多線程中的對同一變量的訪問衝突,在普通的同步機制中,是通過對象加鎖來實現多個線程對同一變量的安全訪問的。這時該變量是多個線程共享的,使用這種同步機制需要很細緻地分析在什麼時候對變量進行讀寫,什麼時候需要鎖定某個對象,什麼時候釋放該對象的鎖等等很多。所有這些都是因爲多個線程共享了資源造成的。ThreadLocal就從另一個角度來解決多線程的併發訪問,ThreadLocal會爲每一個線程維護一個和該線程綁定的變量的副本,從而隔離了多個線程的數據,每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的整個變量封裝進ThreadLocal,或者把該對象的特定於線程的狀態封裝進ThreadLocal。
  
  當然ThreadLocal並不能替代同步機制,兩者面向的問題領域不同。同步機制是爲了同步多個線程對相同資源的併發訪問,是爲了多個線程之間進行通信的有效方式;而ThreadLocal是隔離多個線程的數據共享,從根本上就不在多個線程之間共享資源(變量),這樣當然不需要對多個線程進行同步了。所以,如果你需要進行多個線程之間進行通信,則使用同步機制;如果需要隔離多個線程之間的共享衝突,可以使用ThreadLocal,這將極大地簡化你的程序,使程序更加易讀、簡潔 。

概括起來說,對於多線程資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而後者爲每一個線程都提供了一份變量,因此可以同時訪問而互不影響。概括起來說,對於多線程資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而後者爲每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
 

第七web中應用

我們知道在一般情況下,只有無狀態的Bean纔可以在多線程環境下共享,在Spring中,絕大部分Bean都可以聲明爲singleton作用域。就是因爲Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全狀態採用ThreadLocal進行處理,讓它們也成爲線程安全的狀態,因此有狀態的Bean就可以在多線程中共享了。一般的Web應用劃分爲展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過接口向上層開放功能調用。在一般情況下,從接收請求到返回響應所經過的所有程序調用都同屬於一個線程。同一線程貫通三層這樣你就可以根據需要,將一些非線程安全的變量以ThreadLocal存放,在同一次請求響應的調用線程中,所有關聯的對象引用到的都是同一個變量(1程慣三層),一般會在框架中使用。

1,現在說明一下request和ThreadLocal的差別。

        (1)、存取值方式不同。

             request根據KEY存取值、一個request可以存多個值。

             ThreadLocal只能存一個值,ThreadLocal的get和set方法沒有參數KEY。

        (2)、使用地方不一樣。

             request使用在表示層、一般在Action和Servlet中使用。

             ThreadLocal在什麼地方都可以、一般用在框架基類中比較多、比如存放當前的數據庫連接等。

     2,我們在使用Hibernate的時候,經常會使用到currentSession(),而Hibernat是這樣使用ThreadLocal的。

public static final ThreadLocal session = new ThreadLocal();   
    public static Session currentSession() {
        Session s = (Session)session.get(); 
        // open a new session,if this session has none  
        if(s == null){   
              s = sessionFactory.openSession(); 
              session.set(s);   
        }   
          return s;   
    }   

     3,web中請求共享

public class RequestUtil {
  
	public static final ThreadLocal<HttpServletRequest> requestTL = new ThreadLocal<HttpServletRequest>(); // 保存request的threadlocal

	public static Object getAttribute(String name) {
		return requestTL.get().getAttribute(name);
	}
	public static void setAttribute(String name, Object value) {
		requestTL.get().setAttribute(name, value);
	}
	public static void removeAttribute(String name) {
		requestTL.get().removeAttribute(name);
	}
	public static boolean containsKey(String name) {
		Object value = getAttribute(name);
		if (value != null) {
			return true;
		}
		return false;
	}
	public static boolean notContainsKey(String name) {
		Object value = getAttribute(name);
		if (value == null) {
			return true;
		}
		return false;
	}
	public static HttpServletRequest getRequest() {
		return requestTL.get();
	}
	public static String getParameter(String name) {
		return requestTL.get().getParameter(name);
	}
}

接着最重要的就是什麼時候將這個request放入當前線程,比如在Servlet中當然可以在dosomething(HttpServletRequest request, HttpServletResponse response){}這樣的方法裏,當然也可以再一些攔截器,過濾器的時候進行設置,如Spring的preHandle(HttpServletRequest request, HttpServletResponse response, Object handler),代碼如下:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
		RequestUtil.requestTL.set(request);
	}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章