1. 程式人生 > 實用技巧 >應用案例——多連結串列共享同一套連結串列操作

應用案例——多連結串列共享同一套連結串列操作

1.定義

執行緒本地的變數副本,屬於每個執行緒獨有。每個執行緒使用ThreadLocal設定自己的值,設定的值之間不受影響,但是使用同一個ThreadLocal物件。所以設定的每個變數,是給每個執行緒一個獨有的變數副本

2.簡單使用

public class HelloThreadLocal {
    private static ThreadLocal<Loan> threadLocal = new ThreadLocal<Loan>();

    public static void main(String[] args) {
        //執行緒1 使用threadLocal設定自己的變數副本
        new Thread(() -> {
            threadLocal.set(new Loan("zhangsan", "1000.00"));
            System.out.println("執行緒-1loan:" + threadLocal.get());
        }).start();

        //執行緒2 使用threadLocal設定自己的變數副本
        new Thread(() -> {
            try {
                Thread.sleep(1 * 1000);
            } catch (InterruptedException e) {
            }
            HelloThreadLocal.Loan loan = threadLocal.get();
            System.out.println("執行緒-2loan:" + loan);
            threadLocal.set(new Loan("lisi", "2000.00"));
            loan = threadLocal.get();
            System.out.println("執行緒-2loan:" + loan);

        }).start();

        try {
            Thread.sleep(5 * 1000);
        } catch (InterruptedException e) {
        }

        System.out.println("main-執行緒loan:" + threadLocal.get());
        threadLocal.set(new Loan("wangwu", "1000.00"));
        System.out.println("main-執行緒loan:" + threadLocal.get());

    }

    @Data
    @AllArgsConstructor
    public static class Loan {
        private String name;
        private String amount;
    }
}

輸出結果:

執行緒-1loan:HelloThreadLocal.Loan(name=zhangsan, amount=1000.00)
執行緒-2loan:null
執行緒-2loan:HelloThreadLocal.Loan(name=lisi, amount=2000.00)
main-執行緒loan:null
main-執行緒loan:HelloThreadLocal.Loan(name=wangwu, amount=1000.00)

可以看出,每個執行緒無法獲取到其它執行緒設定的loan物件

3.原始碼分析

1.get方法

首先獲取當前執行緒的ThreadLocalMap變數,如果map為空的話呼叫setInitialValue方法返回預設值,如果map不為空獲取entry中key對應的value值

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

getMap方法:

threadLocals變數用於儲存當前執行緒自身的ThreadLocal,所以雖然使用的是ThreadLocal的get方法,但是操作的實際是當前執行緒的threadLocals本地遍歷副本的Map

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

setInitialValue方法:

首先初始化value為null,然後當前執行緒獲取map,如果為null的話,建立map,否則直接set,這裡會返回null,所以get方法也會返回null的值

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

2.set方法

如果是第一次使用set方法,建立一個預設大小為16的ThreadLocalMap,並將key設為ThreadLocalMap物件,value用傳入的value,如果不是第一次直接map.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);
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

3.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;
    }
}

這裡繼承了弱引用,弱引用是隻要gc發現了它,就會回收掉,這裡的key使用弱引用是為了防止記憶體洩漏,因為如果key是強引用的話,當一個threadLocal物件為null後,還是會指向它,導致該物件不能被回收,造成記憶體洩漏,這裡雖然key使用了弱引用,但是還是存在value指向的強引用,所以需要用remove方法來刪除之前的key,不然還是會造成記憶體洩漏

4.應用場景

1.Spring的Transaction機制中,將一個執行緒中的事務放入ThreadLocal中,可以在整個方法呼叫棧中隨時取出事務的資訊進行操作,不會影響其它執行緒

2.Log4j2等日誌框架中的MDC

3.HDFS edits_log的txId自增後放入執行緒本地副本,HDFS的每條edit_log都有一個txId,會將這個txId記錄到當前執行緒方便在整個執行緒過程中隨時取用

小結:

ThreadLocal最常用的2個場景就是:

1.執行緒中,在各個方法需要共享變數時使用。除了方法之間傳遞入參,通過ThreadLocal可以很方便的做到這一點

2.多執行緒操作時,防止併發衝突,保證執行緒安全。比如一般會拷貝一份資料到執行緒本地,自己修改本地變數,是執行緒安全的