乾貨!ThreadLocal 使用場景02

本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!前端

接下來將承接上文,介紹ThreadLocal的另外一個經典使用場景後端

上一遍文章連接:ThreadLocal經典使用場景安全

經典場景

ThreadLocal 用做保存每一個線程獨享的對象,爲每一個線程都建立一個副本,這樣每一個線程均可以修改本身所擁有的副本, 而不會影響其餘線程的副本,確保了線程安全。markdown

image.png

前幾天我在網上看到一篇介紹,寫的確實不錯,咱們一塊兒來學習下。多線程

拿SimpleDateFormat作實驗

1 拿10個線程測試下

假設咱們有 10 個線程同時對應 10 個 SimpleDateFormat 對象。看輸出什麼,咱們來看下面這種寫法:併發

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 10; i++) {

            int finalI = i;

            new Thread(() -> {

                String date = new ThreadLocalDemo02().date(finalI);

                System.out.println(date);

            }).start();

            Thread.sleep(100);

        }

    }



    public String date(int seconds) {

        Date date = new Date(1000 * seconds);

        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");

        return simpleDateFormat.format(date);

    }

}

複製代碼

上面的代碼利用了一個 for 循環來完成這個需求。for 循環一共循環 10 次,每一次都會新建一個線程,而且每個線程都會在 date 方法中建立一個 SimpleDateFormat 對象,示意圖以下:ide

image.png

能夠看出一共有 10 個線程,對應 10 個 SimpleDateFormat 對象。post

代碼的運行結果:學習

image.png

2 需求變成了 1000 個線程都要用到 SimpleDateFormat

可是線程不能無休地建立下去,由於線程越多,所佔用的資源也會越多。假設咱們須要 1000 個任務,那就不能再用 for 循環的方法了,而是應該使用線程池來實現線程的複用,不然會消耗過多的內存等資源。測試

利用線程池:

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



    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 1000; i++) {

            int finalI = i;

            threadPool.submit(new Runnable() {

                @Override

                public void run() {

                    String date = new ThreadLocalDemo03().date(finalI);

                    System.out.println(date);

                }

            });

        }

        threadPool.shutdown();

    }



    public String date(int seconds) {

        Date date = new Date(1000 * seconds);

        SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");

        return dateFormat.format(date);

    }

}

複製代碼

能夠看出,咱們用了一個 16 線程的線程池,而且給這個線程池提交了 1000 次任務。每一個任務中它作的事情和以前是同樣的,仍是去執行 date 方法,而且在這個方法中建立一個 simpleDateFormat 對象。程序的一種運行結果是(多線程下,運行結果不惟一):

00:00
00:07
00:04
00:02
...
16:29
16:28
16:27
16:26
16:39
複製代碼

程序運行結果正確,把從 00:00 到 16:39 這 1000 個時間給打印了出來,而且沒有重複的時間。咱們把這段代碼用圖形化給表示出來,如圖所示:

image.png

圖的左側是一個線程池,右側是 1000 個任務。咱們剛纔所作的就是每一個任務都建立了一個 simpleDateFormat 對象,也就是說,1000 個任務對應 1000 個 simpleDateFormat 對象。

可是這樣作是沒有必要的,由於這麼多對象的建立是有開銷的,而且在使用完以後的銷燬一樣是有開銷的,並且這麼多對象同時存在在內存中也是一種內存的浪費。

如今咱們就來優化一下。既然不想要這麼多的 simpleDateFormat 對象,最簡單的就是隻用一個就能夠了

3 全部的線程都共用一個 simpleDateFormat 對象

咱們用下面的代碼來演示只用一個 simpleDateFormat 對象的狀況:

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

    static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");



    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 1000; i++) {

            int finalI = i;

            threadPool.submit(new Runnable() {

                @Override

                public void run() {

                    String date = new ThreadLocalDemo04().date(finalI);

                    System.out.println(date);

                }

            });

        }

        threadPool.shutdown();

    }



    public String date(int seconds) {

        Date date = new Date(1000 * seconds);

        return dateFormat.format(date);

    }

}

複製代碼

在代碼中能夠看出,其餘的沒有變化,變化之處就在於,咱們把這個 simpleDateFormat 對象給提取了出來,變成 static 靜態變量,須要用的時候直接去獲取這個靜態對象就能夠了。看上去省略掉了建立 1000 個 simpleDateFormat 對象的開銷,看上去沒有問題,咱們用圖形的方式把這件事情給表示出來:

image.png

從圖中能夠看出,咱們有不一樣的線程,而且線程會執行它們的任務。可是不一樣的任務所調用的 simpleDateFormat 對象都是同一個,因此它們所指向的那個對象都是同一個,可是這樣一來就會有線程不安全的問題。

4 線程不安全,出現了併發安全問題

00:04
00:04
00:05
00:04
...
16:15
16:14
16:13
複製代碼

執行上面的代碼就會發現,控制檯所打印出來的和咱們所期待的是不一致的。咱們所期待的是打印出來的時間是不重複的,可是能夠看出在這裏出現了重複,好比第一行和第二行都是 04 秒,這就表明它內部已經出錯了。

加鎖

出錯的緣由就在於,simpleDateFormat 這個對象自己不是一個線程安全的對象,不該該被多個線程同時訪問。因此咱們就想到了一個解決方案,用 synchronized 來加鎖。因而代碼就修改爲下面的樣子:

public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalDemo05().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        String s = null;
        synchronized (ThreadLocalDemo05.class) {
            s = dateFormat.format(date);
        }
        return s;
    }
}
複製代碼

能夠看出在 date 方法中加入了 synchronized 關鍵字,把 simpleDateFormat 的調用給上了鎖。

00:00
00:01
00:06
...
15:56
16:37
16:36
複製代碼

這樣的結果是正常的,沒有出現重複的時間。可是因爲咱們使用了 synchronized 關鍵字,就會陷入一種排隊的狀態,多個線程不能同時工做,這樣一來,總體的效率就被大大下降了。有沒有更好的解決方案呢?

咱們但願達到的效果是,既不浪費過多的內存,同時又想保證線程安全。通過思考得出,可讓每一個線程都擁有一個本身的 simpleDateFormat 對象來達到這個目的,這樣就能一箭雙鵰了。

請出主角,ThreadLocal

那麼,要想達到上面所說目的,咱們就可使用 ThreadLocal。示例代碼以下所示:

public class ThreadLocalDemo06 {

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

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalDemo06().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }
}

class ThreadSafeFormatter {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("mm:ss");
        }
    };
}
複製代碼

在這段代碼中,咱們使用了 ThreadLocal 幫每一個線程去生成它本身的 simpleDateFormat 對象,對於每一個線程而言,這個對象是獨享的。但與此同時,這個對象就不會創造過多,一共只有 16 個,由於線程只有 16 個。

00:05
00:04
00:01
...
16:37
16:36
16:32
複製代碼

這個結果是正確的,不會出現重複的時間。

咱們用圖來看一下當前的這種狀態:

image.png

在圖中的左側能夠看到,這個線程池一共有 16 個線程,對應 16 個 simpleDateFormat 對象。而在這個圖畫的右側是 1000 個任務,任務是很是多的,和原來同樣有 1000 個任務。可是這裏最大的變化就是,雖然任務有 1000 個,可是咱們再也不須要去建立 1000 個 simpleDateFormat 對象了。即使任務再多,最終也只會有和線程數相同的 simpleDateFormat 對象。這樣既高效地使用了內存,又同時保證了線程安全。

以上就是第一種很是典型的適合使用 ThreadLocal 的場景。

感謝您的閱讀,歡迎點贊、評論、分享

弦外之音

學無止境、天天積累一點點。分享多一點點!

相關文章
相關標籤/搜索