行到水窮處,坐看雲起時 - JUC(一)

併發不知Doug Lea,學盡工具也枉然。html

目錄

  1. JUC(一)
    • 線程池
    • ThreadLocal
  2. JUC(二)
    • CAS
  3. JUC(三)
    • 併發容器
    • 併發流程
  4. JUC(四)
    • AQS
    • 治理線程

線程池

1、相同的線程是如何執行不一樣的任務的?超過核心線程數的線程是如何被回收的?

先看一下線程池裏面很重要的兩個成員變量java

/** * The queue used for holding tasks and handing off to worker * threads. */
private final BlockingQueue<Runnable> workQueue;
/** * Set containing all worker threads in pool. Accessed only when * holding mainLock. */
private final HashSet<Worker> workers = new HashSet<Worker>();
複製代碼

這裏的Worker即持有具體thread的工做線程,workQueue(阻塞隊列)則是咱們傳入的一個個須要被執行的任務(run方法裏面的內容)。其餘的結構性的說明能夠瞅瞅這篇美團技術團隊的文章,寫的特別好: https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html安全

那相同的線程是如何執行不一樣的任務的?

來看看線程池的runWorker方法多線程

工做線程在執行完成一個任務之後,就會去阻塞隊列獲取新的任務,執行任務的run方法。

有一個挺有意思的地方:線程池在新任務進來時候,若是核心線程數沒有滿,則會去再開一個線程,而不是複用已存在的空閒的核心線程,由於runWork的方法,在getTask的地方會阻塞,可是他是阻塞在了獲取隊列中的task。併發

回收?保留?

上面說到,getTask會阻塞線程。這和回收保留有什麼關係呢?咱們來看看這段代碼dom

能夠很清晰的看到:核心線程永久阻塞,非核心線程則計時阻塞

ThreadLocal

2、ThreadLocal用在哪些場景?

  1. 每一個須要一個獨享的對象(一般是工具類,典型的有SimpleDateFormat和Random)
  2. 每一個線程內須要保存的全局變量,例如攔截器中獲取的用戶信息

ThreadLocal是什麼?

咱們先看看它的存在方式,在Thread類裏面有一個成員變量:ide

/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
複製代碼

它被一個線程所攜帶,存放在一個map裏面,這個map的key爲ThreadLocal對象,value爲你須要設置的值。工具

當咱們調用ThreadLocal的set方法時,實際上是把當前的threadlocal做爲key,加上你的value,放入了當前線程的那個Map裏面。性能

爲何須要使用ThreadLocal?

咱們來一步步看一下以下場景(模擬大量請求獲得服務的狀況,在這條請求鏈路中,咱們都須要使用相同的格式進行打印時間:從0-999):this

  1. 最暴力的方式,咱們直接在每個任務運行時候建立一個SimpleDateFormat對象,這樣雖然沒什麼問題,可是1000個「相同」對象的建立和銷燬,着實沒有必要
public class ThreadLocalNormalUsage {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(() -> {
                String date = new ThreadLocalNormalUsage().date(finalI);
                System.out.println(date);
                // 更多的service層任務
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        //參數的單位是毫秒,從1970.1.1 00:00:00 GMT計時
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return dateFormat.format(date);
    }
}
複製代碼
  1. 那既然你說是相同的,直接使用全局的靜態變量怎麼樣呢?是否是就解決問題了?
public class ThreadLocalNormalUsage {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(() -> {
                String date = new ThreadLocalNormalUsage().date(finalI);
                System.out.println(date);
                // 更多的service層任務
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        //參數的單位是毫秒,從1970.1.1 00:00:00 GMT計時
        Date date = new Date(1000 * seconds);
        return dateFormat.format(date);
    }
}
複製代碼

如今問題就來了,竟然出現了相同的時間打印,顯然這是不該該的呀。

由於SimpleDateFormat在多線程訪問下就會出現問題,由於他自己並非線程安全的類。

  1. 這樣的話,咱們使用synchronized把關鍵操做保護起來如何?
public class ThreadLocalNormalUsage {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(() -> {
                String date = new ThreadLocalNormalUsage().date(finalI);
                System.out.println(date);
                // 更多的service層任務
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        //參數的單位是毫秒,從1970.1.1 00:00:00 GMT計時
        Date date = new Date(1000 * seconds);
        String s;
        synchronized (ThreadLocalNormalUsage.class) {
            s = dateFormat.format(date);
        }
        return s;
    }
}
複製代碼

沒錯,此次結果正常了。

可是這樣不行呀,使用同步保護後,全部併發的線程都在這排隊,性能損耗豈不是很嚴重,這還得了。

  1. 好了,主角閃亮登場:對應線程池中的10個線程,咱們爲每個線程建立一個SimpleDateFormat對象,這樣既保證了併發安全,又節省了對象的開銷
public class ThreadLocalNormalUsage {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(() -> {
                String date = new ThreadLocalNormalUsage().date(finalI);
                System.out.println(date);
                // 更多的service層任務
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        //參數的單位是毫秒,從1970.1.1 00:00:00 GMT計時
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
        return dateFormat.format(date);
    }
}

/** * 兩種效果同樣的寫法,都是重寫initialValue方法 * initialValue方法會延遲加載,在使用get方法時候纔會觸發 */
class ThreadSafeFormatter {
    

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

    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
            .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
複製代碼

再一種場景就是須要在整條鏈路傳參(用戶信息)了,咱們雖然可使用方法參數的方式,可是並不優雅,ThreadLocal的set、get瞭解一下

public class ThreadLocalNormalUsage {

    public static void main(String[] args) {
        new Service1().process("mrhe");

    }
}

class Service1 {

    public void process(String name) {
        User user = new User(name);
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2拿到用戶名:" + user.name);
        new Service3().process();
    }
}

class Service3 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用戶名:" + user.name);
        UserContextHolder.holder.remove();
    }
}

class UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {

    String name;

    public User(String name) {
        this.name = name;
    }
}
複製代碼

相關知識點

ThreadLocal致使內存泄露

爲何會內存泄露呢?會發生在哪兒?

咱們先來分析一下ThreadLocal裏面的那個Map

/** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
複製代碼

顯然,這個key使用的是弱引用,既然是弱引用,那內存泄露應該不是發生在這裏(那就只有value咯~)。

正常狀況下,當線程終止,保存在ThreadLocal裏的value就會被垃圾回收,由於沒有強引用了。可是,若是線程不終止(好比線程池中反覆使用並保持的線程),那麼key對應的value就不能被回收,由於有以下的調用鏈:

Thread -> ThreadLocalMap -> Entry(key爲null) -> Value

由於這個強引用鏈路還存在,因此value就沒法被回收,就可能出現OOM。JDK已經考慮到這個問題,因此在set、remove和rehash方法中會掃描key爲null的Entry,進而把value置爲null:

/** * Double the capacity of the table. */
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}
複製代碼

可是若是一個ThreadLocal不被使用,那麼實際上set、rehash等方法也再也不被調用,這時線程又不中止的話,就會內存泄漏了。也就是說,須要咱們手動去remove。

話說回來,咱們通常使用的是static的ThreadLocal,那JDK這個機制也就無效了。並且咱們在使用線程池的時候,線程是會複用的,那這個時候爲了防止無用value不斷堆積又該怎麼辦呢?

那咱們最好在每一個任務執行完成的時候作一下必要的清理工做:

/** * 重寫線程池中的方法 */
protected void afterExecute(Runnable r, Throwable t) { 
    Thread.currentThread().threadLocals = null;
}
複製代碼
相關文章
相關標籤/搜索