聊一聊Spring中的線程安全性


Spring作為一個IOC/DI容器,幫助我們管理了許許多多的“bean”。但其實,Spring並沒有保證這些對象的線程安全,需要由開發者自己編寫解決線程安全問題的代碼。

Spring對每個bean提供了一個scope屬性來表示該bean的作用域。它是bean的生命周期。例如,一個scope為singleton的bean,在第一次被注入時,會創建為一個單例對象,該對象會一直被復用到應用結束。

  • singleton:默認的scope,每個scope為singleton的bean都會被定義為一個單例對象,該對象的生命周期是與Spring IOC容器一致的(但在第一次被注入時才會創建)。

  • prototype:bean被定義為在每次注入時都會創建一個新的對象。

  • request:bean被定義為在每個HTTP請求中創建一個單例對象,也就是說在單個請求中都會復用這一個單例對象。

  • session:bean被定義為在一個session的生命周期內創建一個單例對象。

  • application:bean被定義為在ServletContext的生命周期中復用一個單例對象。

  • websocket:bean被定義為在websocket的生命周期中復用一個單例對象。

我們交由Spring管理的大多數對象其實都是一些無狀態的對象,這種不會因為多線程而導致狀態被破壞的對象很適合Spring的默認scope,每個單例的無狀態對象都是線程安全的(也可以說只要是無狀態的對象,不管單例多例都是線程安全的,不過單例畢竟節省了不斷創建對象與GC的開銷)。

無狀態的對象即是自身沒有狀態的對象,自然也就不會因為多個線程的交替調度而破壞自身狀態導致線程安全問題。無狀態對象包括我們經常使用的DO、DTO、VO這些只作為數據的實體模型的貧血對象,還有Service、DAO和Controller,這些對象並沒有自己的狀態,它們只是用來執行某些操作的。例如,每個DAO提供的函數都只是對數據庫的CRUD,而且每個數據庫Connection都作為函數的局部變量(局部變量是在用戶棧中的,而且用戶棧本身就是線程私有的內存區域,所以不存在線程安全問題),用完即關(或交還給連接池)。

有人可能會認為,我使用request作用域不就可以避免每個請求之間的安全問題了嗎?這是完全錯誤的,因為Controller默認是單例的,一個HTTP請求是會被多個線程執行的,這就又回到了線程的安全問題。當然,你也可以把Controller的scope改成prototype,實際上Struts2就是這么做的,但有一點要注意,Spring MVC對請求的攔截粒度是基於每個方法的,而Struts2是基於每個類的,所以把Controller設為多例將會頻繁的創建與回收對象,嚴重影響到了性能。

通過閱讀上文其實已經說的很清楚了,Spring根本就沒有對bean的多線程安全問題做出任何保證與措施。對於每個bean的線程安全問題,根本原因是每個bean自身的設計。不要在bean中聲明任何有狀態的實例變量或類變量,如果必須如此,那么就使用ThreadLocal把變量變為線程私有的,如果bean的實例變量或類變量需要在多個線程之間共享,那么就只能使用synchronized、lock、CAS等這些實現線程同步的方法了。

下面將通過解析ThreadLocal的源碼來了解它的實現與作用,ThreadLocal是一個很好用的工具類,它在某些情況下解決了線程安全問題(在變量不需要被多個線程共享時)。

本文作者為SylvanasSun(sylvanas.sun@gmail.com),首發於SylvanasSun’s Blog
原文鏈接:sylvanassun.github.io/2017/11/06/…
(轉載請務必保留本段聲明,並且保留超鏈接。)

ThreadLocal


ThreadLocal是一個為線程提供線程局部變量的工具類。它的思想也十分簡單,就是為線程提供一個線程私有的變量副本,這樣多個線程都可以隨意更改自己線程局部的變量,不會影響到其他線程。不過需要注意的是,ThreadLocal提供的只是一個淺拷貝,如果變量是一個引用類型,那么就要考慮它內部的狀態是否會被改變,想要解決這個問題可以通過重寫ThreadLocal的initialValue()函數來自己實現深拷貝,建議在使用ThreadLocal時一開始就重寫該函數。

ThreadLocal與像synchronized這樣的鎖機制是不同的。首先,它們的應用場景與實現思路就不一樣,鎖更強調的是如何同步多個線程去正確地共享一個變量,ThreadLocal則是為了解決同一個變量如何不被多個線程共享。從性能開銷的角度上來講,如果鎖機制是用時間換空間的話,那么ThreadLocal就是用空間換時間。

一、何謂“ThreadLocal”

ThreadLocal是一個線程局部變量,我們都知道全局變量和局部變量的區別,拿Java舉例就是定義在類中的是全局的變量,各個方法中都能訪問得到,而局部變量定義在方法中,只能在方法內訪問。那線程局部變量(ThreadLocal)就是每個線程都會有一個局部變量,獨立於變量的初始化副本,而各個副本是通過線程唯一標識相關聯的。

二、ThreadLocal的用法

(1)方法摘要

作用域 類型 方法 描述
public T get() 返回此線程局部變量的當前線程副本中的值
protected T initialValue() 返回此線程局部變量的當前線程的“初始值”
public void remove() 移除此線程局部變量當前線程的值
public void set(T value) 將此線程局部變量的當前線程副本中的值設置為指定值

注意事項: 
==initialValue()== 這個方法是為了讓子類覆蓋設計的,默認缺省null。如果get()后又remove()則可能會在調用一下此方法。 
==remove()== 移除此線程局部變量當前線程的值。如果此線程局部變量隨后被當前線程 讀取,且這期間當前線程沒有 設置其值,則將調用其 initialValue() 方法重新初始化其值。這將導致在當前線程多次調用 initialValue 方法。

(2)常規用法

在開始之前貼出一個公共的線程測試類

 1 public class TaskThread<T> extends Thread{
 2 
 3     private T t;
 4 
 5     public TaskThread(String threadName,T t) {
 6         this.setName(threadName);
 7         this.t = t;
 8     }
 9 
10     @Override
11     public void run() {
12         for (int i = 0; i < 2; i++) {
13 
14             try {
15                 Class[] argsClass = new Class[0];
16                 Method method = t.getClass().getMethod("getUniqueId",argsClass);
17                 int value = (int) method.invoke(t);
18                 System.out.println("thread[" + Thread.currentThread().getName() + "] --> uniqueId["+value+ "]");
19 
20             } catch (NoSuchMethodException e) {
21                 // TODO 暫不處理
22                 continue;
23 
24             } catch (IllegalAccessException e) {
25                 // TODO 暫不處理
26                 continue;
27 
28             } catch (InvocationTargetException e) {
29                 // TODO 暫不處理
30                 continue;
31 
32             }
33 
34 
35         }
36     }
37 
38 }

例1:為每個線程生成一個唯一的局部標識

 1 public class UniqueThreadIdGenerator {
 2 
 3     // 原子整型
 4     private static final AtomicInteger uniqueId = new AtomicInteger(0);
 5 
 6     // 線程局部整型變量
 7     private static final ThreadLocal <Integer> uniqueNum =
 8             new ThreadLocal < Integer > () {
 9                 @Override protected Integer initialValue() {
10                     return uniqueId.getAndIncrement();
11                 }
12             };
13 
14     //變量值
15     public static int getUniqueId() {
16         return uniqueId.get();
17     }
18 
19     public static void main(String[] args) {
20         UniqueThreadIdGenerator uniqueThreadId = new UniqueThreadIdGenerator();
21         // 為每個線程生成一個唯一的局部標識
22         TaskThread t1 = new TaskThread<UniqueThreadIdGenerator>("custom-thread-1", uniqueThreadId);
23         TaskThread t2 = new TaskThread<UniqueThreadIdGenerator>("custom-thread-2", uniqueThreadId);
24         TaskThread t3 = new TaskThread<UniqueThreadIdGenerator>("custom-thread-3", uniqueThreadId);
25         t1.start();
26         t2.start();
27         t3.start();
28     }
29 
30 }

運行結果:

//每個線程的局部變量都是唯一的
thread[custom-thread-2] --> uniqueId[0]
thread[custom-thread-2] --> uniqueId[0]
thread[custom-thread-1] --> uniqueId[0]
thread[custom-thread-1] --> uniqueId[0]
thread[custom-thread-3] --> uniqueId[0]
thread[custom-thread-3] --> uniqueId[0]

例2:為每個線程創建一個局部唯一的序列

 1 public class UniqueSequenceGenerator {
 2 
 3     // 線程局部整型變量
 4     private static final ThreadLocal <Integer> uniqueNum =
 5             new ThreadLocal < Integer > () {
 6                 @Override protected Integer initialValue() {
 7                     return 0;
 8                 }
 9             };
10 
11     //變量值
12     public static int getUniqueId() {
13         uniqueNum.set(uniqueNum.get() + 1);
14         return uniqueNum.get();
15     }
16 
17     public static void main(String[] args) {
18         UniqueSequenceGenerator uniqueThreadId = new UniqueSequenceGenerator();
19         // 為每個線程生成內部唯一的序列號
20         TaskThread t1 = new TaskThread<UniqueSequenceGenerator>("custom-thread-1", uniqueThreadId);
21         TaskThread t2 = new TaskThread<UniqueSequenceGenerator>("custom-thread-2", uniqueThreadId);
22         TaskThread t3 = new TaskThread<UniqueSequenceGenerator>("custom-thread-3", uniqueThreadId);
23         t1.start();
24         t2.start();
25         t3.start();
26     }
27 
28 }

運行結果:

thread[custom-thread-2] --> uniqueId[1]
thread[custom-thread-2] --> uniqueId[2]
thread[custom-thread-1] --> uniqueId[1]
thread[custom-thread-1] --> uniqueId[2]
thread[custom-thread-3] --> uniqueId[1]
thread[custom-thread-3] --> uniqueId[2]

三、ThreadLocal的原理(摘自網上)

(1)源碼解析

源碼實現片段:set

 1 /** 
 2     * Sets the current thread's copy of this thread-local variable 
 3     * to the specified value.  Most subclasses will have no need to 
 4     * override this method, relying solely on the {@link #initialValue} 
 5     * method to set the values of thread-locals. 
 6     * 
 7     * @param value the value to be stored in the current thread's copy of 
 8     *        this thread-local. 
 9     */  
10    public void set(T value) {  
11        Thread t = Thread.currentThread();  
12        ThreadLocalMap map = getMap(t);  
13        if (map != null)  
14            map.set(this, value);  
15        else  
16            createMap(t, value);  
17    } 

在這個方法內部我們看到,首先通過getMap(Thread t)方法獲取一個和當前線程相關的ThreadLocalMap,然后將變量的值設置到這個ThreadLocalMap對象中,當然如果獲取到的ThreadLocalMap對象為空,就通過createMap方法創建。

==線程隔離的秘密,就在於ThreadLocalMap這個類。ThreadLocalMap是ThreadLocal類的一個靜態內部類,它實現了鍵值對的設置和獲取(對比Map對象來理解),每個線程中都有一個獨立的ThreadLocalMap副本,它所存儲的值,只能被當前線程讀取和修改。ThreadLocal類通過操作每一個線程特有的ThreadLocalMap副本,從而實現了變量訪問在不同線程中的隔離。因為每個線程的變量都是自己特有的,完全不會有並發錯誤。還有一點就是,ThreadLocalMap存儲的鍵值對中的鍵是this對象指向的ThreadLocal對象,而值就是你所設置的對象了。== 這個就是實現原理

源碼實現片段:getMap、createMap

 1 /** 
 2  * Get the map associated with a ThreadLocal. Overridden in 
 3  * InheritableThreadLocal. 
 4  * 
 5  * @param  t the current thread 
 6  * @return the map 
 7  */  
 8 ThreadLocalMap getMap(Thread t) {  
 9     return t.threadLocals;  
10 }  
11   
12 /** 
13  * Create the map associated with a ThreadLocal. Overridden in 
14  * InheritableThreadLocal. 
15  * 
16  * @param t the current thread 
17  * @param firstValue value for the initial entry of the map 
18  * @param map the map to store. 
19  */  
20 void createMap(Thread t, T firstValue) {  
21     t.threadLocals = new ThreadLocalMap(this, firstValue);  
22 }  

源碼實現片段:get

 1 /** 
 2  * Returns the value in the current thread's copy of this 
 3  * thread-local variable.  If the variable has no value for the 
 4  * current thread, it is first initialized to the value returned 
 5  * by an invocation of the {@link #initialValue} method. 
 6  * 
 7  * @return the current thread's value of this thread-local 
 8  */  
 9 public T get() {  
10     Thread t = Thread.currentThread();  
11     ThreadLocalMap map = getMap(t);  
12     if (map != null) {  
13         ThreadLocalMap.Entry e = map.getEntry(this);  
14         if (e != null)  
15             return (T)e.value;  
16     }  
17     return setInitialValue();  
18 }  

源碼實現片段:setInitialValue

 1 /** 
 2     * Variant of set() to establish initialValue. Used instead 
 3     * of set() in case user has overridden the set() method. 
 4     * 
 5     * @return the initial value 
 6     */  
 7    private T setInitialValue() {  
 8        T value = initialValue();  
 9        Thread t = Thread.currentThread();  
10        ThreadLocalMap map = getMap(t);  
11        if (map != null)  
12            map.set(this, value);  
13        else  
14            createMap(t, value);  
15        return value;  
16    }  
17    
18    //獲取和當前線程綁定的值時,ThreadLocalMap對象是以this指向的ThreadLocal對象為鍵
19    //進行查找的,這當然和前面set()方法的代碼是相呼應的。進一步地,我們可以創建不同的
20    //ThreadLocal實例來實現多個變量在不同線程間的訪問隔離,為什么可以這么做?因為不
21    //同的ThreadLocal對象作為不同鍵,當然也可以在線程的ThreadLocalMap對象中設置不同
22    //的值了。通過ThreadLocal對象,在多線程中共享一個值和多個值的區別,就像你在一個
23    //HashMap對象中存儲一個鍵值對和多個鍵值對一樣,僅此而已。

四、ThreadLocal實際用途

例1:在數據庫管理中的連接管理類是下面這樣的:(摘自網上)

 1 public class ConnectionManager {
 2     private static Connection connect = null;
 3 
 4     public static Connection getConnection() {
 5         if(connect == null){
 6             connect = DriverManager.getConnection();
 7         }
 8         return connect;
 9     }
10 
11     ...
12 
13 } 

在單線程的情況下這樣寫並沒有問題,但如果在多線程情況下回出現線程安全的問題。你可能會說用同步關鍵字或鎖來保障線程安全,這樣做當然是可行的,但考慮到性能的問題所以這樣子做並是很優雅。 
下面是改造后的代碼:

 1 public class ConnectionManager {
 2 
 3     private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
 4 
 5     public static Connection getConnection() {
 6         if(connThreadLocal.get() != null)
 7             return connThreadLocal.get();
 8         
 9         //獲取一個連接並設置到當前線程變量中
10         Connection conn = getConnection();
11         connThreadLocal.set(conn);
12         return conn;
13     }
14     
15     ...
16 
17 }

例2:日期格式(摘自網上)

使用這個日期格式類主要作用就是將枚舉對象轉成Map而map的值則是使用ThreadLocal存儲,那么在實際的開發中可以在同一線程中的不同方法中使用日期格式而無需在創建日期格式的實例。

 1 public class DateFormatFactory {
 2 
 3     public enum DatePattern {
 4 
 5         TimePattern("yyyy-MM-dd HH:mm:ss"),
 6         DatePattern("yyyy-MM-dd");
 7 
 8         public String pattern;
 9 
10         private DatePattern(String pattern) {
11             this.pattern = pattern;
12         }
13     }
14 
15     private static final Map<DatePattern, ThreadLocal<DateFormat>> pattern2ThreadLocal;
16 
17     static {
18         DatePattern[] patterns = DatePattern.values();
19         int len = patterns.length;
20         pattern2ThreadLocal = new HashMap<DatePattern, ThreadLocal<DateFormat>>(len);
21 
22         for (int i = 0; i < len; i++) {
23             DatePattern datePattern = patterns[i];
24             final String pattern = datePattern.pattern;
25 
26             pattern2ThreadLocal.put(datePattern, new ThreadLocal<DateFormat>() {
27                 @Override
28                 protected DateFormat initialValue() {
29                     return new SimpleDateFormat(pattern);
30                 }
31             });
32         }
33     }
34 
35     //獲取DateFormat
36     public static DateFormat getDateFormat(DatePattern pattern) {
37         ThreadLocal<DateFormat> threadDateFormat = pattern2ThreadLocal.get(pattern);
38         //不需要判斷threadDateFormat是否為空
39         return threadDateFormat.get();
40     }
41 
42     public static void main(String[] args) {
43          String dateStr = DateFormatFactory.getDateFormat(DatePattern.TimePattern).format(new Date());
44          System.out.println(dateStr);
45     }
46 
47 
48 }

ThreadLocal中的內存泄漏


我們要考慮一種會發生內存泄漏的情況,如果ThreadLocal被設置為null后,而且沒有任何強引用指向它,根據垃圾回收的可達性分析算法,ThreadLocal將會被回收。這樣一來,ThreadLocalMap中就會含有key為null的Entry,而且ThreadLocalMap是在Thread中的,只要線程遲遲不結束,這些無法訪問到的value會形成內存泄漏。為了解決這個問題,ThreadLocalMap中的getEntry()、set()和remove()函數都會清理key為null的Entry,以下面的getEntry()函數的源碼為例。

 1 /**
 2          * Get the entry associated with key.  This method
 3          * itself handles only the fast path: a direct hit of existing
 4          * key. It otherwise relays to getEntryAfterMiss.  This is
 5          * designed to maximize performance for direct hits, in part
 6          * by making this method readily inlinable.
 7          *
 8          * @param  key the thread local object
 9          * @return the entry associated with key, or null if no such
10          */
11         private Entry getEntry(ThreadLocal<?> key) {
12             int i = key.threadLocalHashCode & (table.length - 1);
13             Entry e = table[i];
14             if (e != null && e.get() == key)
15                 return e;
16             else
17                 return getEntryAfterMiss(key, i, e);
18         }
19 
20         /**
21          * Version of getEntry method for use when key is not found in
22          * its direct hash slot.
23          *
24          * @param  key the thread local object
25          * @param  i the table index for key's hash code
26          * @param  e the entry at table[i]
27          * @return the entry associated with key, or null if no such
28          */
29         private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
30             Entry[] tab = table;
31             int len = tab.length;
32 
33             // 清理key為null的Entry
34             while (e != null) {
35                 ThreadLocal<?> k = e.get();
36                 if (k == key)
37                     return e;
38                 if (k == null)
39                     expungeStaleEntry(i);
40                 else
41                     i = nextIndex(i, len);
42                 e = tab[i];
43             }
44             return null;
45         }

在上文中我們發現了ThreadLocalMap的key是一個弱引用,那么為什么使用弱引用呢?使用強引用key與弱引用key的差別如下:

  • 強引用key:ThreadLocal被設置為null,由於ThreadLocalMap持有ThreadLocal的強引用,如果不手動刪除,那么ThreadLocal將不會回收,產生內存泄漏。

  • 弱引用key:ThreadLocal被設置為null,由於ThreadLocalMap持有ThreadLocal的弱引用,即便不手動刪除,ThreadLocal仍會被回收,ThreadLocalMap在之后調用set()、getEntry()和remove()函數時會清除所有key為null的Entry。

但要注意的是,ThreadLocalMap僅僅含有這些被動措施來補救內存泄漏問題。如果你在之后沒有調用ThreadLocalMap的set()、getEntry()和remove()函數的話,那么仍然會存在內存泄漏問題。

在使用線程池的情況下,如果不及時進行清理,內存泄漏問題事小,甚至還會產生程序邏輯上的問題。所以,為了安全地使用ThreadLocal,必須要像每次使用完鎖就解鎖一樣,在每次使用完ThreadLocal后都要調用remove()來清理無用的Entry。




免責聲明!

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



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