時(shí)間轉(zhuǎn)化類SimpleDateFormat 的線程安全問(wèn)題

1. 場(chǎng)景

在項(xiàng)目中經(jīng)常用得到 SimpleDateFormat時(shí)間轉(zhuǎn)化類,但是其并非線程安全的??赏ㄟ^(guò)一個(gè)實(shí)例代碼來(lái)說(shuō)明。

import lombok.extern.slf4j.Slf4j;
@Slf4j
public class DateFormatExample1 {

    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    public static int threadTotal = 200;

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
    }

    private static void update() {
        try {
            simpleDateFormat.parse("20180208");
        } catch (Exception e) {
            log.error("parse exception", e);
        }
    }
}

2. 分析使用到的類與方法是否線程安全。

  • 發(fā)布的對(duì)象是可變類且線程間共享
  • 多個(gè)線程可以同時(shí)修改該對(duì)象

分析sdf的源碼可知SimpleDateFormat 類內(nèi)部有一個(gè) Calendar 對(duì)象引用,它用來(lái)儲(chǔ)存和這個(gè) SimpleDateFormat 相關(guān)的日期信息,例如sdf.parse(dateStr),sdf.format(date) 諸如此類的方法參數(shù)傳入的日期相關(guān)String, Date等等。而這個(gè)發(fā)布的對(duì)象又可以被多個(gè)線程同時(shí)修改,進(jìn)而導(dǎo)致了時(shí)間混亂的問(wèn)題。

3. 線程安全策略

通過(guò)線程安全策略的一般方法來(lái)研究這個(gè)問(wèn)題的解決辦法,
---> 首先,無(wú)法將Calendar對(duì)象定義成不可變對(duì)象。
---> 其次,我們來(lái)看看線程封閉的策略,如果將sdf對(duì)象封閉在線程里,也就是每個(gè)線程創(chuàng)建一個(gè) sdf對(duì)象,這樣雖然避免了線程安全的問(wèn)題,但是存在大量的資源消耗。這樣我們?cè)倏?ThreadLocal的封閉策略,這種方式確實(shí)不錯(cuò),我們通過(guò)控制最多的并發(fā)線程數(shù)可以限制實(shí)例化的sdf對(duì)象,并且封閉在各自的線程之中。
---> 同步容器,這里可以使用第三方 Joda-Time包

import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
@Slf4j
public class DateFormatExample3 {

    // 請(qǐng)求總數(shù)
    public static int clientTotal = 5000;

    // 同時(shí)并發(fā)執(zhí)行的線程數(shù)
    public static int threadTotal = 200;

    private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
    }

    private static void update(int i) {
        log.info("{}, {}", i, DateTime.parse("20180208", dateTimeFormatter).toDate());
    }
}

4. ThreadLocal 使用要注意的問(wèn)題

ThreadLocal<T>的出現(xiàn)是一種空間換時(shí)間的思想的運(yùn)用,是為了多線程環(huán)境下單線程內(nèi)變量共享的問(wèn)題。它的原理就是每個(gè)線程通過(guò)ThreadLocal.ThreadLocalMap,保存當(dāng)前線程中所有ThreadLocal變量引用的key和值。相當(dāng)于每個(gè)線程有各自的變量副本,線程內(nèi)共享這個(gè)變量數(shù)據(jù),線程間互不影響。

ThreadLocal<T>有它自己的使用場(chǎng)景,比如Spring中用它了解決Session、Connection等多線程并發(fā)訪問(wèn)問(wèn)題,但不能它不能用來(lái)代替為了解決多線程安全問(wèn)題的同步關(guān)鍵字,因?yàn)樗鼘?shí)際上沒(méi)有多線程間的變量共享,而線程安全問(wèn)題是指多線程間變量共享,且共享變量可修改,進(jìn)而可能會(huì)出現(xiàn)多線程并發(fā)修改共享變量的問(wèn)題,這種需要通過(guò)同步手段解決。

ThreadLocal<T>變量一般要聲名成static類型,即當(dāng)前線程中只有一個(gè)T類型變量的實(shí)例,線程內(nèi)可共享該實(shí)例數(shù)據(jù)且不會(huì)出問(wèn)題,如將其聲名成非static,則一個(gè)線程內(nèi)就存儲(chǔ)多個(gè)T類型變量的實(shí)例,有點(diǎn)存儲(chǔ)空間的浪費(fèi),一般很少有這樣的應(yīng)用場(chǎng)景。另外根據(jù)實(shí)際情況,ThreadLocal變量聲名時(shí)也多加上private final關(guān)鍵詞表明它時(shí)類內(nèi)私有、引用不可修改。

在線程池環(huán)境下,由于線程是一直運(yùn)行且復(fù)用的,使用ThreadLocal<T>時(shí)會(huì)出現(xiàn)這個(gè)任務(wù)看到上個(gè)任務(wù)ThreadLocal變量值以及內(nèi)存泄露等問(wèn)題,解決方法就是在當(dāng)前任務(wù)執(zhí)行完后將ThreadLocal變量remove。

5. DateTimeFormatter (Java 8支持)

更新下,Java 8內(nèi)置的DateTimeFormatter是線程安全的,放心使用。
https://www.liaoxuefeng.com/wiki/1252599548343744/1303985694703650

參考鏈接:
https://blog.csdn.net/zq602316498/article/details/40263083
https://www.cnblogs.com/zhuimengdeyuanyuan/archive/2017/10/25/7728009.html

Java面試必問(wèn),ThreadLocal終極篇

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容