ThreadLocal案例分析

目錄

1. ThreadLocal簡介
    1.1 ThreadLocal基礎
        1.1.1 ThreadLocal和Thread的關系
        1.1.2 變量的生命周期
    1.2 可繼承的ThreadLocal
2. ThreadLocal的應用案例
    2.1 解決并發(fā)問題
        2.1.1 java.lang.ThreadLocalRandom
        2.1.2 HDFS中的Statistics:實現(xiàn)高并發(fā)下的統(tǒng)計功能
    2.2 解決數(shù)據(jù)存儲問題
        2.2.1 Struts2的ActionContext設計原理
        2.2.2 Spring中thread scope Bean
3. 總結(jié)

1. ThreadLocal簡介

這篇博客主要對ThreadLocal類的基礎知識和實踐應用進行分析。文章的重點在于應用案例的探究,同時也會對理論基礎作簡單的介紹。

1.1 ThreadLocal基礎

為什么需要ThreadLocal

要理解為什么需要ThreadLocal就不得不從線程安全問題說起。高并發(fā)是很多領域都會遇到的非常棘手的問題,其最核心的問題在于如何平衡高性能和數(shù)據(jù)一致性。當我們說某個類是線程安全的時候,也就意味著該類在多線程環(huán)境下的狀態(tài)保持一致性。

所謂的一致性,就是關聯(lián)數(shù)據(jù)之間的邏輯關系是否正確和完整。

通過下面示例對數(shù)據(jù)一致性問題進行說明:

public class ThreadLocalDemo {

    public static void main(String[] args) throws InterruptedException {
        int nThreads = 10;
        final Counter counter = new Counter();
        
        ExecutorService exec = Executors.newFixedThreadPool(nThreads);
        final CountDownLatch latch = new CountDownLatch(nThreads);
        
        for(int i = 0; i < nThreads; i++){
            exec.submit(new Runnable(){
                public void run(){
                    for(int i = 0; i < 10000; i++){
                        counter.increase();
                    }
                    latch.countDown();
                }
            });
        }
        latch.await();
        System.out.println("Expected:" + nThreads * 10000 + ",Actual:" + counter.count);
    }
    static class Counter{
        int count = 0;
        
        public void  increase(){
            this.count++;
        }
    }

輸出:

Expected:100000,Actual:71851

可見最終變量count的狀態(tài)并不符合預期的邏輯。對于并發(fā)問題來說,最簡單的解決辦法就是加鎖,本質(zhì)是并發(fā)訪問串行訪問的改變。如下:

    static class Counter{
        int count = 0;
        
        public synchronized void  increase(){
            this.count++;
        }       
    }

輸出:

Expected:100000,Actual:100000

第一次實驗中,count變量的值之所以出現(xiàn)不正確的情況,是因為其被多個線程同時訪問,而且對某個線程來說,其它線程對變量count的操作結(jié)果,該線程是不一定可見的,這是造成count變量最終數(shù)據(jù)不一致的原因。而用synchronized修飾過后,串行訪問時就不存在不可見的情況。從而保證了count變量的正確性。那么是否可以換個思路:讓變量只能被一個線程訪問,這不就不存在之前談到的線程安全問題了嗎?

讓每個線程都保存一份變量的副本,該副本只會被隸屬的線程操作,這也就不存在線程安全問題了。這就是ThreadLocal的由來。

1.1.1 ThreadLocal和Thread的聯(lián)系

在上面提到了數(shù)據(jù)副本,那么線程如何保存該副本的呢?其實,Thread類中有一個ThreadLocalMap類型的變量threadLocals,定義如下:

public class Thread implements Runnable {
    
    //。。。
    
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    //。。。    
}

ThreadLocalMap是ThreadLocal的一個內(nèi)部類,其作用相當于一個HashMap,用于保存隸屬于該線程的變量副本。下面需要考慮一個問題:ThreadLocalMap的key和value該如何設計呢?

從API角度來說,ThreadLocal的作用是提供給client訪問Thread中threadLocals變量的訪問接口,每個ThreadLocal都對應著一個Thread內(nèi)部的變量副本。所以ThreadLocalMap中的key就是ThreadLocal對象(也就是該對象的hashCode),value也就是變量副本。一個對象默認的hashcode也就是該對象的引用值,這可以保證不同對象的hashcode不同。不過ThreadLocal并沒有使用這一默認值,而是內(nèi)部聲明了一個threadLocalHashCode整型變量用以存儲該對象的hashcode值:

public class ThreadLocal<T> {

    private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    //。。。
}

變量副本的存儲問題已經(jīng)解決,那么怎么對Thread內(nèi)部的threadLocals變量進行訪問呢?這就要通過ThreadLocal了。下面對ThreadLocal的方法簡單介紹下:

  1. get()操作
    public T get() {
        Thread t = Thread.currentThread();//獲取當前Thread對象引用
        ThreadLocalMap map = getMap(t);//從Thread對象中獲取ThreadLocalMap變量
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();//如果是第一次訪問,就setInitialValue進行初始化
    }
    
    private T setInitialValue() {
        //initialValue方法是protected修飾的,默認返回null,所以需要在ThreadLocal子類中進行覆蓋。
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }    
  1. set操作
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

和setInitialValue幾乎一致,不同的是:set操作會傳入需要設置的value。而setInitialValue需要通過initialValue()獲取初始值。

  1. remove操作
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

1.1.2 變量的生命周期

這里所的變量指的是存儲在Thread對象中的變量副本。下面從init-service-destroy三個階段分析下其生命周期:

  1. Init
    第一次調(diào)用get方法的時候完成了初始化過程。這也就是為什么需要覆蓋ThreadLocal的initialValue方法。在setInitialValue方法中的createMap方法如下:
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
  1. Service
    只要線程活著且ThreadLocal可訪問即處于Service階段。

  2. Destroy
    由于threadLocals變量是Thread的成員,那么當Thread對象掛了后,那么其內(nèi)部的所有成員也都被gc了。此外,通過ThreadLocal提供remove方法也可以將threadLocals里的特定副本變量移除。

ThreadLocal變量的生命周期呢?由于ThreadLocal變量通常用private static修飾,也就是屬于類成員
變量。所以其生命周期當然也就和該類一致。

1.2 可繼承的ThreadLocal

首先看個實例:

    static class Context {

        private static final ThreadLocal<HashMap<String,String>> CONTEXT1 = new ThreadLocal<HashMap<String,String>>(){
            protected HashMap<String,String> initialValue(){
                return new HashMap<String,String>();
            }
        };
        private static final InheritableThreadLocal<HashMap<String,String>> CONTEXT2 = new InheritableThreadLocal<HashMap<String,String>>(){
            protected HashMap<String,String> initialValue(){
                return new HashMap<String,String>();
            }
        };      
        public static HashMap<String,String> getContext1() {
            return CONTEXT1.get();
        }
        public static HashMap<String,String> getContext2() {
            return CONTEXT2.get();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Context.getContext1().put("name", "wqx");
        Context.getContext2().put("name", "wqx");
        Thread thread = new Thread(new Runnable(){
            @Override
            public void run() {
                System.out.println("name:" + Context.getContext1().get("name"));
                System.out.println("name:" + Context.getContext2().get("name"));
            }
        });
        thread.start();
    }

輸出:

name:null

name:wqx

字面意思上理解InheritableThreadLocal即為可繼承的ThreadLocal,這里的可繼承的含義指的是子線程在實例化過程中,會查看當前執(zhí)行線程(可以理解為父線程)的inheritableThreadLocals是否為null,如果不為null,則將該變量賦值給子線程的inheritableThreadLocals。下面是Thread類構(gòu)造函數(shù)中的相關片段:

    Thread parent = currentThread();//當前線程,也就是執(zhí)行new Thread()的線程
    if (parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

2. ThreadLocal的應用案例

2.1 解決并發(fā)問題

2.1.1 java.lang.ThreadLocalRandom

在Java中隨機數(shù)可以用Random類,下面是java.util.Random的生成隨機數(shù)的方法:

    protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
            oldseed = seed.get();
            nextseed = (oldseed * multiplier + addend) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
    }

可見,其中通過CAS方式保證其線程安全性。這在高并發(fā)的環(huán)境中由于線程間的競爭必然帶來一定的性能損耗。ThreadLocal此時就派上用場了,ThreadLocalRandom是通過ThreadLocal改進的用于隨機數(shù)生成的工具類,每個線程單獨持有一個ThreadLocalRandom對象引用,這就完全杜絕了線程間的競爭問題。

public class ThreadLocalRandom extends Random {
    
    //。。。
    
    private static final ThreadLocal<ThreadLocalRandom> localRandom =
        new ThreadLocal<ThreadLocalRandom>() {
            protected ThreadLocalRandom initialValue() {
                return new ThreadLocalRandom();
            }
    };
    //。。。
}

ThreadLocalRandom能用于全局范圍的隨機數(shù)生成嗎?每個線程都持有一個ThreadLocalRandom對象,生成的隨機數(shù)不會重復嗎?考慮到ThreadLocal的特點,理論上也就不應該將其用于全局范圍,其更適合于線程獨享變量的存儲。But!凡事都有例外,下面看個例外的用法。

2.1.2 HDFS中的Statistics:實現(xiàn)高并發(fā)下的統(tǒng)計功能

Hadoop的分布式文件系統(tǒng)(HDFS)是其生態(tài)的基石,MR任務中涉及到的數(shù)據(jù)輸入輸出都與其密切相關。對于FileSystem來說,對大量的讀寫操作進行統(tǒng)計是非常必要的。這該如何實現(xiàn)呢?

方案一:通過加鎖的方式??紤]到Hadoop處理的數(shù)據(jù)體量及對數(shù)據(jù)操作的頻率,加鎖帶來的性能損耗不可忽視,So。。。PASS!

方案二:ThreadLocal可以嗎?對當前FileSystem進行操作的線程很多,如果只使用ThreadLocal方案的話,只能統(tǒng)計一個線程的操作次數(shù),那么在匯總操作的時候必然要進行同步synchronized處理。這可行嗎?判斷一個方案可不可行,必須要具體業(yè)務邏輯具體分析,在本例中,statistics是用于存儲統(tǒng)計數(shù)據(jù)的對象,那么對FileSystem進行操作(比如:create、mkdir、list、delete等)的同時都會記錄在statistics對象中,也就是對statistics對象進行寫操作,而對于統(tǒng)計數(shù)據(jù)的讀操作比較少。所以Hadoop考慮到寫多讀少的事實,ThreadLocal方案是可以接受的。

下面是Statistics對象的部分實現(xiàn):

public static final class Statistics {

    /**
     * Statistics data.
     * /
    public static class StatisticsData {
      volatile long bytesRead;
      volatile long bytesWritten;
      volatile int readOps;
      volatile int largeReadOps;
      volatile int writeOps;
      //。。。
    }

    //allData保存的是所有線程中StatisticsData對象的引用
    private final Set<StatisticsDataReference> allData;
    
    //ThreadLocal變量
    private final ThreadLocal<StatisticsData> threadData;
    
    public void incrementBytesWritten(long newBytes) {
      getThreadStatistics().bytesWritten += newBytes;
    }
    
    public StatisticsData getThreadStatistics() {
      StatisticsData data = threadData.get();
      if (data == null) {   //第一次統(tǒng)計操作時需要進行初始化,并與allData進行關聯(lián)
        data = new StatisticsData();
        threadData.set(data);
        StatisticsDataReference ref =
            new StatisticsDataReference(data, Thread.currentThread());
        synchronized(this) {
          allData.add(ref);
        }
      }
      return data;
    }    
    //。。。
}

下面是DistributedFileSystem中刪除操作的實現(xiàn),可見在每次執(zhí)行刪除操作的時候,都會通過statistics進行記錄。

public class DistributedFileSystem extends FileSystem {

  @Override
  public boolean delete(Path f, final boolean recursive) throws IOException {
    statistics.incrementWriteOps(1);
    // 。。。
  }
}

如果需要獲取統(tǒng)計數(shù)據(jù)時,就要將所有線程內(nèi)部的統(tǒng)計數(shù)據(jù)進行累加,這肯定需要進行同步處理的。如下所示的是獲取統(tǒng)計數(shù)據(jù)中所有寫操作的次數(shù):

    public long getBytesWritten() {
      return visitAll(new StatisticsAggregator<Long>() {
        private long bytesWritten = 0;

        @Override
        public void accept(StatisticsData data) {
          bytesWritten += data.bytesWritten;
        }

        public Long aggregate() {
          return bytesWritten;
        }
      });
    }
    //加鎖處理,保證統(tǒng)計數(shù)據(jù)的正確性
    private synchronized <T> T visitAll(StatisticsAggregator<T> visitor) {
      visitor.accept(rootData);
      for (StatisticsDataReference ref: allData) {
        StatisticsData data = ref.getData();
        visitor.accept(data);
      }
      return visitor.aggregate();
    }

在寫多讀少的環(huán)境下,這種方案可以有效的解決傳統(tǒng)“加鎖”方案帶來的多線程間的競爭。Brilliant idea!

2.2 解決數(shù)據(jù)存儲問題

2.2.1 Struts2的ActionContext設計原理

Struts2是使用較為廣泛的MVC框架,其關于請求響應流程的設計思路也是很新穎的。當?shù)谝淮谓佑|Struts2的時候,曾一直困惑于一個問題:Action中的每個方法的請求參數(shù)怎么獲得的?處理結(jié)果又是如何返回的?在傳統(tǒng)的Servlet中,我們可以通過函數(shù)入?yún)ttpServletRequest對象獲取請求參數(shù),可以通過入?yún)ttpServletResponse對象向輸出流寫入響應數(shù)據(jù)。而Struts2中自定義的Action的每個方法都沒有入?yún)ⅲ姨幚砗蟮捻憫獢?shù)據(jù)也不是當作返回值返回的。

Struts2的最大亮點也許就是對數(shù)據(jù)流和控制流的解耦。數(shù)據(jù)不再需要作為方法參數(shù)傳入或作為返回值返回。Struts2的返回值僅僅作為控制流的標識(比如:選擇哪個視圖)。Struts2中數(shù)據(jù)載體就是ActionContext。不管是請求參數(shù)亦或是處理后的響應數(shù)據(jù)都被封裝在ActionContext內(nèi)部。開發(fā)者一般常接觸的是ActionContext的子類ServletActionContext。

首先看下Struts2中幾個主要組件的示意圖:

Struts2組件示意圖

ActionContext作為數(shù)據(jù)載體,與每個組件都會有數(shù)據(jù)交互,如:ActionInvocation、Interceptor、Action、Result等。這幾乎涵蓋了一個請求的整個生命周期。這里說的請求的生命周期可以泛指處理請求的線程的生命周期。ThreadLocal不正適合這種情況嗎?下面看下com.opensymphony.xwork2.ActionContext類的部分結(jié)構(gòu):

public class ActionContext implements Serializable {

    //。。。
    
    static ThreadLocal<ActionContext> actionContext = new ThreadLocal<ActionContext>();
    
    private Map<String, Object> context;

    public ActionContext(Map<String, Object> context) {
        this.context = context;
    }
    
    public static ActionContext getContext() {
        return actionContext.get();
    }
    public Map<String, Object> getContextMap() {
        return context;
    }
    //。。。

ActionContext是典型的ThreadLocal使用案例,通過將請求處理過程中涉及到的所有參數(shù)封裝進ActionContext中,從而實現(xiàn)了數(shù)據(jù)流和控制流的分離,這一解耦思路值得好好學習。Another brilliant idea!

2.2.1 Spring中thread scope Bean

在Spring中,如果按照Bean的生命周期對其進行劃分,那么大致可以分為這么幾類:Singleton、Prototype、Request、Session、Thread Scope等。這一節(jié)主要介紹ThreadScope的Bean如何實現(xiàn)。經(jīng)過上面的各種案例分析,這個問題就灰常容
易解決了,只需要將Bean的生命周期與Thread同步就行。ThreadLocal正合適。下面是Spring內(nèi)部已經(jīng)實現(xiàn)的方案SimpleThreadScope:

public class SimpleThreadScope implements Scope {

    private final ThreadLocal<Map<String, Object>> threadScope =
            new NamedThreadLocal<Map<String, Object>>("SimpleThreadScope") {
                @Override
                protected Map<String, Object> initialValue() {
                    return new HashMap<String, Object>();
                }
            };

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Map<String, Object> scope = this.threadScope.get();
        Object object = scope.get(name);
        if (object == null) {
            object = objectFactory.getObject();
            scope.put(name, object);
        }
        return object;
    }

    //。。。
}

3. 總結(jié)

上面小節(jié)中分別分析了ThreadLocal的兩個主要的應用領域:1.解決并發(fā)問題。2.解決數(shù)據(jù)存儲問題。其中解決并發(fā)問題的本質(zhì)是一種以空間換時間的思路,時間效率提升了,但是也存在著內(nèi)存使用時的潛在溢出風險。數(shù)據(jù)存儲問題主要指的是:系統(tǒng)中多個組件如何實現(xiàn)數(shù)據(jù)的交互和共享,而作為執(zhí)行者的線程作為數(shù)據(jù)載體再適合不過了。雖然各種組件可以實現(xiàn)數(shù)據(jù)共享,但是數(shù)據(jù)在線程間是隔離的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容