1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        SimpleDateFormat線程不安全的5種解決方案!

        共 10785字,需瀏覽 22分鐘

         ·

        2021-05-20 09:52

        作者 | 王磊

        來源 | Java中文社群(ID:javacn666)

        轉(zhuǎn)載請(qǐng)聯(lián)系授權(quán)(微信ID:GG_Stone)

        1.什么是線程不安全?

        線程不安全也叫非線程安全,是指多線程執(zhí)行中,程序的執(zhí)行結(jié)果和預(yù)期的結(jié)果不符的情況就叫著線程不安全。

        線程不安全的代碼

        SimpleDateFormat 就是一個(gè)典型的線程不安全事例,接下來我們動(dòng)手來實(shí)現(xiàn)一下。首先我們先創(chuàng)建 10 個(gè)線程來格式化時(shí)間,時(shí)間格式化每次傳遞的待格式化時(shí)間都是不同的,所以程序如果正確執(zhí)行將會(huì)打印 10 個(gè)不同的值,接下來我們來看具體的代碼實(shí)現(xiàn):

        import java.text.SimpleDateFormat;
        import java.util.Date;
        import java.util.concurrent.ExecutorService;
        import java.util.concurrent.Executors;

        public class SimpleDateFormatExample {
        // 創(chuàng)建 SimpleDateFormat 對(duì)象
        private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");

        public static void main(String[] args) {
        // 創(chuàng)建線程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        // 執(zhí)行 10 次時(shí)間格式化
        for (int i = 0; i < 10; i++) {
        int finalI = i;
        // 線程池執(zhí)行任務(wù)
        threadPool.execute(new Runnable() {
        @Override
        public void run() {
        // 創(chuàng)建時(shí)間對(duì)象
        Date date = new Date(finalI * 1000);
        // 執(zhí)行時(shí)間格式化并打印結(jié)果
        System.out.println(simpleDateFormat.format(date));
        }
        });
        }
        }
        }

        我們預(yù)期的正確結(jié)果是這樣的(10 次打印的值都不同):

        然而,以上程序的運(yùn)行結(jié)果卻是這樣的:

        從上述結(jié)果可以看出,當(dāng)在多線程中使用 SimpleDateFormat 進(jìn)行時(shí)間格式化是線程不安全的。

        2.解決方案

        SimpleDateFormat 線程不安全的解決方案總共包含以下 5 種:

        1. SimpleDateFormat 定義為局部變量;
        2. 使用 synchronized 加鎖執(zhí)行;
        3. 使用 Lock 加鎖執(zhí)行(和解決方案 2 類似);
        4. 使用 ThreadLocal;
        5. 使用 JDK 8 中提供的 DateTimeFormat。

        接下來我們分別來看每種解決方案的具體實(shí)現(xiàn)。

        ① SimpleDateFormat改為局部變量

        SimpleDateFormat 定義為局部變量時(shí),因?yàn)槊總€(gè)線程都是獨(dú)享 SimpleDateFormat 對(duì)象的,相當(dāng)于將多線程程序變成“單線程”程序了,所以不會(huì)有線程不安全的問題,具體實(shí)現(xiàn)代碼如下:

        import java.text.SimpleDateFormat;
        import java.util.Date;
        import java.util.concurrent.ExecutorService;
        import java.util.concurrent.Executors;

        public class SimpleDateFormatExample {
        public static void main(String[] args) {
        // 創(chuàng)建線程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        // 執(zhí)行 10 次時(shí)間格式化
        for (int i = 0; i < 10; i++) {
        int finalI = i;
        // 線程池執(zhí)行任務(wù)
        threadPool.execute(new Runnable() {
        @Override
        public void run() {
        // 創(chuàng)建 SimpleDateFormat 對(duì)象
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
        // 創(chuàng)建時(shí)間對(duì)象
        Date date = new Date(finalI * 1000);
        // 執(zhí)行時(shí)間格式化并打印結(jié)果
        System.out.println(simpleDateFormat.format(date));
        }
        });
        }
        // 任務(wù)執(zhí)行完之后關(guān)閉線程池
        threadPool.shutdown();
        }
        }

        以上程序的執(zhí)行結(jié)果為:

        當(dāng)打印的結(jié)果都不相同時(shí),表示程序的執(zhí)行是正確的,從上述結(jié)果可以看出,將 SimpleDateFormat 定義為局部變量之后,就可以成功的解決線程不安全問題了。

        ② 使用synchronized加鎖

        鎖是解決線程不安全問題最常用的手段,接下來我們先用 synchronized 來加鎖進(jìn)行時(shí)間格式化,實(shí)現(xiàn)代碼如下:

        import java.text.SimpleDateFormat;
        import java.util.Date;
        import java.util.concurrent.ExecutorService;
        import java.util.concurrent.Executors;

        public class SimpleDateFormatExample2 {
        // 創(chuàng)建 SimpleDateFormat 對(duì)象
        private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");

        public static void main(String[] args) {
        // 創(chuàng)建線程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        // 執(zhí)行 10 次時(shí)間格式化
        for (int i = 0; i < 10; i++) {
        int finalI = i;
        // 線程池執(zhí)行任務(wù)
        threadPool.execute(new Runnable() {
        @Override
        public void run() {
        // 創(chuàng)建時(shí)間對(duì)象
        Date date = new Date(finalI * 1000);
        // 定義格式化的結(jié)果
        String result = null;
        synchronized (simpleDateFormat) {
        // 時(shí)間格式化
        result = simpleDateFormat.format(date);
        }
        // 打印結(jié)果
        System.out.println(result);
        }
        });
        }
        // 任務(wù)執(zhí)行完之后關(guān)閉線程池
        threadPool.shutdown();
        }
        }

        以上程序的執(zhí)行結(jié)果為:

        ③ 使用Lock加鎖

        在 Java 語(yǔ)言中,鎖的常用實(shí)現(xiàn)方式有兩種,除了 synchronized 之外,還可以使用手動(dòng)鎖 Lock,接下來我們使用 Lock 來對(duì)線程不安全的代碼進(jìn)行改造,實(shí)現(xiàn)代碼如下:

        import java.text.SimpleDateFormat;
        import java.util.Date;
        import java.util.concurrent.ExecutorService;
        import java.util.concurrent.Executors;
        import java.util.concurrent.locks.Lock;
        import java.util.concurrent.locks.ReentrantLock;

        /**
        * Lock 解決線程不安全問題
        */

        public class SimpleDateFormatExample3 {
        // 創(chuàng)建 SimpleDateFormat 對(duì)象
        private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");

        public static void main(String[] args) {
        // 創(chuàng)建線程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        // 創(chuàng)建 Lock 鎖
        Lock lock = new ReentrantLock();
        // 執(zhí)行 10 次時(shí)間格式化
        for (int i = 0; i < 10; i++) {
        int finalI = i;
        // 線程池執(zhí)行任務(wù)
        threadPool.execute(new Runnable() {
        @Override
        public void run() {
        // 創(chuàng)建時(shí)間對(duì)象
        Date date = new Date(finalI * 1000);
        // 定義格式化的結(jié)果
        String result = null;
        // 加鎖
        lock.lock();
        try {
        // 時(shí)間格式化
        result = simpleDateFormat.format(date);
        } finally {
        // 釋放鎖
        lock.unlock();
        }
        // 打印結(jié)果
        System.out.println(result);
        }
        });
        }
        // 任務(wù)執(zhí)行完之后關(guān)閉線程池
        threadPool.shutdown();
        }
        }

        以上程序的執(zhí)行結(jié)果為:

        從上述代碼可以看出,手動(dòng)鎖的寫法相比于 synchronized 要繁瑣一些。

        ④ 使用ThreadLocal

        加鎖方案雖然可以正確的解決線程不安全的問題,但同時(shí)也引入了新的問題,加鎖會(huì)讓程序進(jìn)入排隊(duì)執(zhí)行的流程,從而一定程度的降低了程序的執(zhí)行效率,如下圖所示:

        那有沒有一種方案既能解決線程不安全的問題,同時(shí)還可以避免排隊(duì)執(zhí)行呢?

        答案是有的,可以考慮使用 ThreadLocal。ThreadLocal 翻譯為中文是線程本地變量的意思,字如其人 ThreadLocal 就是用來創(chuàng)建線程的私有(本地)變量的,每個(gè)線程擁有自己的私有對(duì)象,這樣就可以避免線程不安全的問題了,實(shí)現(xiàn)如下:

        知道了實(shí)現(xiàn)方案之后,接下來我們使用具體的代碼來演示一下 ThreadLocal 的使用,實(shí)現(xiàn)代碼如下:

        import java.text.SimpleDateFormat;
        import java.util.Date;
        import java.util.concurrent.ExecutorService;
        import java.util.concurrent.Executors;

        /**
        * ThreadLocal 解決線程不安全問題
        */

        public class SimpleDateFormatExample4 {
        // 創(chuàng)建 ThreadLocal 對(duì)象,并設(shè)置默認(rèn)值(new SimpleDateFormat)
        private static ThreadLocal<SimpleDateFormat> threadLocal =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));

        public static void main(String[] args) {
        // 創(chuàng)建線程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        // 執(zhí)行 10 次時(shí)間格式化
        for (int i = 0; i < 10; i++) {
        int finalI = i;
        // 線程池執(zhí)行任務(wù)
        threadPool.execute(new Runnable() {
        @Override
        public void run() {
        // 創(chuàng)建時(shí)間對(duì)象
        Date date = new Date(finalI * 1000);
        // 格式化時(shí)間
        String result = threadLocal.get().format(date);
        // 打印結(jié)果
        System.out.println(result);
        }
        });
        }
        // 任務(wù)執(zhí)行完之后關(guān)閉線程池
        threadPool.shutdown();
        }
        }

        以上程序的執(zhí)行結(jié)果為:

        ThreadLocal和局部變量的區(qū)別

        首先來說 ThreadLocal 不等于局部變量,這里的“局部變量”指的是像 2.1 示例代碼中的局部變量, ThreadLocal 和局部變量最大的區(qū)別在于:ThreadLocal 屬于線程的私有變量,如果使用的是線程池,那么 ThreadLocal 中的變量是可以重復(fù)使用的,而代碼級(jí)別的局部變量,每次執(zhí)行時(shí)都會(huì)創(chuàng)建新的局部變量,二者區(qū)別如下圖所示:

        更多關(guān)于 ThreadLocal 的內(nèi)容,可以訪問磊哥前面的文章《ThreadLocal不好用?那是你沒用對(duì)!》。

        ⑤ 使用DateTimeFormatter

        以上 4 種解決方案都是因?yàn)?SimpleDateFormat 是線程不安全的,所以我們需要加鎖或者使用 ThreadLocal 來處理,然而,JDK 8 之后我們就有了新的選擇,如果使用的是 JDK 8+  版本,就可以直接使用 JDK 8 中新增的、安全的時(shí)間格式化工具類 DateTimeFormatter 來格式化時(shí)間了,接下來我們來具體實(shí)現(xiàn)一下。

        使用 DateTimeFormatter 必須要配合 JDK 8 中新增的時(shí)間對(duì)象 LocalDateTime 來使用,因此在操作之前,我們可以先將 Date 對(duì)象轉(zhuǎn)換成  LocalDateTime,然后再通過 DateTimeFormatter 來格式化時(shí)間,具體實(shí)現(xiàn)代碼如下:

        import java.time.LocalDateTime;
        import java.time.ZoneId;
        import java.time.format.DateTimeFormatter;
        import java.util.Date;
        import java.util.concurrent.ExecutorService;
        import java.util.concurrent.Executors;

        /**
        * DateTimeFormatter 解決線程不安全問題
        */

        public class SimpleDateFormatExample5 {
        // 創(chuàng)建 DateTimeFormatter 對(duì)象
        private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("mm:ss");

        public static void main(String[] args) {
        // 創(chuàng)建線程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        // 執(zhí)行 10 次時(shí)間格式化
        for (int i = 0; i < 10; i++) {
        int finalI = i;
        // 線程池執(zhí)行任務(wù)
        threadPool.execute(new Runnable() {
        @Override
        public void run() {
        // 創(chuàng)建時(shí)間對(duì)象
        Date date = new Date(finalI * 1000);
        // 將 Date 轉(zhuǎn)換成 JDK 8 中的時(shí)間類型 LocalDateTime
        LocalDateTime localDateTime =
        LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
        // 時(shí)間格式化
        String result = dateTimeFormatter.format(localDateTime);
        // 打印結(jié)果
        System.out.println(result);
        }
        });
        }
        // 任務(wù)執(zhí)行完之后關(guān)閉線程池
        threadPool.shutdown();
        }
        }

        以上程序的執(zhí)行結(jié)果為:

        3.線程不安全原因分析

        要了解 SimpleDateFormat 為什么是線程不安全的?我們需要查看并分析 SimpleDateFormat 的源碼才行,那我們先從使用的方法 format 入手,源碼如下:

        private StringBuffer format(Date date, StringBuffer toAppendTo,
        FieldDelegate delegate)
        {
        // 注意此行代碼
        calendar.setTime(date);

        boolean useDateFormatSymbols = useDateFormatSymbols();

        for (int i = 0; i < compiledPattern.length; ) {
        int tag = compiledPattern[i] >>> 8;
        int count = compiledPattern[i++] & 0xff;
        if (count == 255) {
        count = compiledPattern[i++] << 16;
        count |= compiledPattern[i++];
        }

        switch (tag) {
        case TAG_QUOTE_ASCII_CHAR:
        toAppendTo.append((char)count);
        break;

        case TAG_QUOTE_CHARS:
        toAppendTo.append(compiledPattern, i, count);
        i += count;
        break;

        default:
        subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
        break;
        }
        }
        return toAppendTo;
        }

        也許是好運(yùn)使然,沒想到剛開始分析第一個(gè)方法就找到了線程不安全的問題所在。

        從上述源碼可以看出,在執(zhí)行 SimpleDateFormat.format 方法時(shí),會(huì)使用 calendar.setTime 方法將輸入的時(shí)間進(jìn)行轉(zhuǎn)換,那么我們想象一下這樣的場(chǎng)景:

        1. 線程 1 執(zhí)行了 calendar.setTime(date) 方法,將用戶輸入的時(shí)間轉(zhuǎn)換成了后面格式化時(shí)所需要的時(shí)間;
        2. 線程 1 暫停執(zhí)行,線程 2 得到 CPU 時(shí)間片開始執(zhí)行;
        3. 線程 2 執(zhí)行了 calendar.setTime(date) 方法,對(duì)時(shí)間進(jìn)行了修改;
        4. 線程 2 暫停執(zhí)行,線程 1 得出 CPU 時(shí)間片繼續(xù)執(zhí)行,因?yàn)榫€程 1 和線程 2 使用的是同一對(duì)象,而時(shí)間已經(jīng)被線程 2 修改了,所以此時(shí)當(dāng)線程 1 繼續(xù)執(zhí)行的時(shí)候就會(huì)出現(xiàn)線程安全的問題了。

        正常的情況下,程序的執(zhí)行是這樣的:

        非線程安全的執(zhí)行流程是這樣的:

        在多線程執(zhí)行的情況下,線程 1 的 date1 和線程 2 的 date2,因?yàn)閳?zhí)行順序的問題,最終都被格式化成 date2 formatted,而非線程 1 date1 formatted 和線程 2 date2 formatted,這樣就會(huì)導(dǎo)致線程不安全的問題。

        4.各方案優(yōu)缺點(diǎn)總結(jié)

        如果使用的是 JDK 8+ 版本,可以直接使用線程安全的 DateTimeFormatter 來進(jìn)行時(shí)間格式化,如果使用的 JDK 8 以下版本或者改造老的 SimpleDateFormat 代碼,可以考慮使用 synchronizedThreadLocal 來解決線程不安全的問題。因?yàn)閷?shí)現(xiàn)方案 1 局部變量的解決方案,每次執(zhí)行的時(shí)候都會(huì)創(chuàng)建新的對(duì)象,因此不推薦使用。synchronized 的實(shí)現(xiàn)比較簡(jiǎn)單,而使用 ThreadLocal 可以避免加鎖排隊(duì)執(zhí)行的問題。


        推薦閱讀:

        一文讀懂微內(nèi)核架構(gòu)

        架構(gòu)師之路一-架構(gòu)師入門指引

        Linux 文件搜索神器 find 實(shí)戰(zhàn)詳解,建議收藏!

        貓撲,涼了!

        搞清楚這 10 幾個(gè)后端面試問題,工作穩(wěn)了!


        關(guān)號(hào)互聯(lián)網(wǎng)全棧架構(gòu)價(jià)

        瀏覽 49
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            囯产精品久久77777免费影视 | DHDHDH18-19XXXX | 免费xxxx视频 | 成人观看| 国产精品久久久久久久影歌 | 三上悠亚被淫辱の教室 | se.av | 熟女逼逼 | 六月婷色| 中国操逼网站 |