天天看點

java并發(一):線程基礎篇線程安全問題線程狀态ThreadLocal參考文獻

線程的建立很簡單,一般是內建Thread類或者實作Runnable接口,我就不細說了。然後,要牢記多線程的3大特性:

多線程的三個特性:原子性、可見性、有序性

原子性:是指一個操作是不可中斷的。即使是多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程幹擾。比如,對于一個靜态全局變量int i,兩個線程同時對它指派,線程A給他指派為1,線程B給他指派為-1。那麼不管這兩個線程以何種方式。何種步調工作,i的值要麼是1,要麼是-1.線程A和線程B之間是沒有幹擾的。這就是原子性的一個特點,不可被中斷。

可見性:是指當一個線程修改了某一個共享變量的值,其他線程是否能夠立即知道這個修改。顯然,對于串行來說,可見性問題是不存在的。

有序性:在并發時,程式的執行可能會出現亂序。給人的直覺感覺就是:寫在前面的代碼,會在後面執行。有序性問題的原因是因為程式在執行時,可能會進行指令重排,重排後的指令與原指令的順序未必一緻。

而共享變量的寫操作出錯,最重要的是原子性,一般多線程的問題主要抓住這個。

線程安全問題

一般多線程程式設計都會遇到線程安全的問題,線程安全總體來說是因為多個線程競争共享資源造成的。比如:

public class Test{

    private int num = 0;
    
    public void add(int value){
        this.num = this.num + value;
    }   
}
           

兩個線程分别加了2和3到count變量上,兩個線程執行結束後count變量的值應該等于5。如果兩個線程同時執行這個對象的add()方法,會造成這種現象:線程A先讀到num為0,此時恰好線程B也讀到num為0,然後A,B同時執行加2和加3的操作,如果A先指派num為2,然後B又指派num為3,會造成最後結果為3;或者反過來,造成num為2,使得最後的結果無法預料。

如果線程并沒有共享資源,那麼多線程執行的代碼是安全的,比如:

類方法中局部變量或者局部對象引用

public class Test{
    
    public void add(int value){
        int num = 0;
        String a = new String("aa");
        num = num + value;
    }   
}
           

還有一種安全的方法,就是每個線程都是執行同一個類不同對象的方法,雖然代碼相同,但是不同的對象空間,也不會出現問題,如servlet。

線程狀态

線程的狀态實作通過 Thread.State 常量類實作,有 6 種線程狀态:new(建立)、runnnable(可運作)、blocked(阻塞)、waiting(等待)、time waiting (定時等待)和 terminated(終止)。狀态轉換圖如下:

java并發(一):線程基礎篇線程安全問題線程狀态ThreadLocal參考文獻

線程狀态流程大緻如下:

  • 線程建立後,進入 new 狀态
  • 調用 start 或者 run 方法,進入 runnable 狀态
  • JVM 按照線程優先級及時間分片等執行 runnable 狀态的線程。開始執行時,進入 running 狀态
  • 如果線程執行 sleep、wait、join,或者進入 IO 阻塞等。進入 wait 或者 blocked 狀态
  • 線程執行完畢後,線程被線程隊列移除。最後為 terminated 狀态。

ThreadLocal

ThreadLocal與線程同步無關,它雖然提供了一種解決多線程環境下成員變量的問題,但是它并不是解決多線程共享變量的問題。

它的API介紹如下:

該類提供了線程局部 (thread-local) 變量。這些變量不同于它們的普通對應物,因為通路某個變量(通過其get 或 set 方法)的每個線程都有自己的局部變量,它獨立于變量的初始化副本。ThreadLocal執行個體通常是類中的 private static 字段,它們希望将狀态與某一個線程(例如,使用者 ID 或事務 ID)相關聯。

是以ThreadLocal與線程同步機制不同,線程同步機制是多個線程共享同一個變量,而ThreadLocal是為每一個線程建立一個單獨的變量副本,故而每個線程都可以獨立地改變自己所擁有的變量副本,而不會影響其他線程所對應的副本。可以說ThreadLocal為多線程環境下變量問題提供了另外一種解決思路。

ThreadLocal定義了四個方法:

  • get():傳回此線程局部變量的目前線程副本中的值。
  • initialValue():傳回此線程局部變量的目前線程的“初始值”。
  • remove():移除此線程局部變量目前線程的值。
  • set(T value):将此線程局部變量的目前線程副本中的值設定為指定值。

    除了這四個方法,ThreadLocal内部還有一個靜态内部類ThreadLocalMap,該内部類才是實作線程隔離機制的關鍵,get()、set()、remove()都是基于該内部類操作。ThreadLocalMap提供了一種用鍵值對方式存儲每一個線程的變量副本的方法,key為目前ThreadLocal對象,value則是對應線程的變量副本。

    對于ThreadLocal需要注意的有兩點:

  1. ThreadLocal執行個體本身是不存儲值,它隻是提供了一個在目前線程中找到副本值得key。
  2. 是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小夥伴會弄錯他們的關系。

    下圖是Thread、ThreadLocal、ThreadLocalMap的關系

    java并發(一):線程基礎篇線程安全問題線程狀态ThreadLocal參考文獻

ThreadLocal示例

package com.xushu.multi;

public class Test{
    
    private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){
        // 實作initialValue()
        @Override
        protected Integer initialValue() {
            return 0; //這裡傳回了一個0
        }
        
    };
    
    public int nextSeq(){
        count.set(count.get() + 1);
        
        return count.get();
    }
    
    private static class SeqThread implements Runnable{
        private Test te;

        SeqThread(Test te) {
            this.te = te;
        }
        
        @Override
        public void run() {
            for(int i = 0; i < 3; i++){
                System.out.println(Thread.currentThread().getName() + " seqCount :" + te.nextSeq());
            }
        }
    }
    
    public static void main(String[] args) {
        Test te = new Test();
        
        Thread t1 = new Thread(new SeqThread(te));
        Thread t2 = new Thread(new SeqThread(te));
        Thread t3 = new Thread(new SeqThread(te));
        Thread t4 = new Thread(new SeqThread(te));
        
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}
           

可以看出,每個線程都有自己的一個變量副本,是以從根本上避免了讀同一個變量。但是,如果在initialValue()方法中,如果return的是一個共有變量,那就是所有的線程都通路同一個變量了,是以ThreadLocal就失效了。

這篇文章有解析。

ThreadLocal源碼解析

ThreadLocal雖然解決了這個多線程變量的複雜問題,但是它的源碼實作卻是比較簡單的。ThreadLocalMap是實作ThreadLocal的關鍵,我們先從它入手。

ThreadLocalMap

ThreadLocalMap其内部利用Entry來實作key-value的存儲,如下:

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
           

從上面代碼中可以看出Entry的key就是ThreadLocal,而value就是值。同時,Entry也繼承WeakReference,是以說Entry所對應key(ThreadLocal執行個體)的引用為一個弱引用(關于弱引用這裡就不多說了,感興趣的可以關注這篇部落格

Java 理論與實踐: 用弱引用堵住記憶體洩漏

ThreadLocalMap的源碼稍微多了點,我們就看兩個最核心的方法getEntry()、set(ThreadLocal> key, Object value)方法。

set(ThreadLocal> key, Object value)

private void set(ThreadLocal<?> key, Object value) {
 
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
 
        // 根據 ThreadLocal 的散列值,查找對應元素在數組中的位置
        int i = key.threadLocalHashCode & (len-1);
 
        // 采用“線性探測法”,尋找合适位置
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
 
            ThreadLocal<?> k = e.get();
 
            // key 存在,直接覆寫
            if (k == key) {
                e.value = value;
                return;
            }
 
            // key == null,但是存在值(因為此處的e != null),說明之前的ThreadLocal對象已經被回收了
            if (k == null) {
                // 用新元素替換陳舊的元素
                replaceStaleEntry(key, value, i);
                return;
            }
        }
 
        // ThreadLocal對應的key執行個體不存在也沒有陳舊元素,new 一個
        tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
 
        int sz = ++size;
 
        // cleanSomeSlots 清楚陳舊的Entry(key == null)
        // 如果沒有清理陳舊的 Entry 并且數組中的元素大于了門檻值,則進行 rehash
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
           

這個set()操作和我們在集合了解的put()方式有點兒不一樣,雖然他們都是key-value結構,不同在于他們解決散列沖突的方式不同。集合Map的put()采用的是拉鍊法,而ThreadLocalMap的set()則是采用開放定址法(具體請參考

散列沖突處理系列部落格

)。掌握了開放位址法該方法就一目了然了。

set()操作除了存儲元素外,還有一個很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),這兩個方法可以清除掉key == null 的執行個體,防止記憶體洩漏。在set()方法中還有一個變量很重要:threadLocalHashCode,定義如下:

private final int threadLocalHashCode = nextHashCode();
           

從名字上面我們可以看出threadLocalHashCode應該是ThreadLocal的散列值,定義為final,表示ThreadLocal一旦建立其散列值就已經确定了,生成過程則是調用nextHashCode():

private static AtomicInteger nextHashCode = new AtomicInteger();
 
    private static final int HASH_INCREMENT = 0x61c88647;
 
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
           

nextHashCode表示配置設定下一個ThreadLocal執行個體的threadLocalHashCode的值,HASH_INCREMENT則表示配置設定兩個ThradLocal執行個體的threadLocalHashCode的增量,從nextHashCode就可以看出他們的定義。

private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
           

由于采用了開放定址法,是以目前key的散列值和元素在數組的索引并不是完全對應的,首先取一個探測數(key的散列值),如果所對應的key就是我們所要找的元素,則傳回,否則調用getEntryAfterMiss(),如下:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
 
            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
           

這裡有一個重要的地方,當key == null時,調用了expungeStaleEntry()方法,該方法用于處理key == null,有利于GC回收,能夠有效地避免記憶體洩漏。

get()

  • 傳回目前線程所對應的線程變量
public T get() {
        // 擷取目前線程
        Thread t = Thread.currentThread();
 
        // 擷取目前線程的成員變量 threadLocal
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 從目前線程的ThreadLocalMap擷取相對應的Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
 
                // 擷取目标值        
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
           

首先通過目前線程擷取所對應的成員變量ThreadLocalMap,然後通過ThreadLocalMap擷取目前ThreadLocal的Entry,最後通過所擷取的Entry擷取目标值result。

getMap()方法可以擷取目前線程所對應的ThreadLocalMap,如下:

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
           

set(T value)

  • 設定目前線程的線程局部變量的值。
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
           

擷取目前線程所對應的ThreadLocalMap,如果不為空,則調用ThreadLocalMap的set()方法,key就是目前ThreadLocal,如果不存在,則調用createMap()方法建立一個,如下:

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
           

initialValue()

  • 傳回該線程局部變量的初始值。
protected T initialValue() {
        return null;
    }
           

該方法定義為protected級别且傳回為null,很明顯是要子類實作它的,是以我們在使用ThreadLocal的時候一般都應該覆寫該方法。該方法不能顯示調用,隻有在第一次調用get()或者set()方法時才會被執行,并且僅執行1次。

remove()

  • 将目前線程局部變量的值删除。
public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }
           

該方法的目的是減少記憶體的占用。當然,我們不需要顯示調用該方法,因為一個線程結束後,它所對應的局部變量就會被垃圾回收。

參考文獻

1.并發程式設計網