精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

并發編程 ThreadLocal 必知必會

開發
ThreadLocal? 是 Java 提供的一種簡單而強大的機制,用于實現線程局部變量,即每個線程都有自己的獨立副本,互不干擾。

在多線程編程中,共享資源的管理和同步一直是開發人員面臨的挑戰之一。ThreadLocal 是 Java 提供的一種簡單而強大的機制,用于實現線程局部變量,即每個線程都有自己的獨立副本,互不干擾。這種機制不僅簡化了并發編程中的數據管理,還提高了代碼的可讀性和可維護性。

一、詳解ThreadLocal

1. 什么是ThreadLocal?它有什么用?

為了保證特定變量對當前線程可見,我們就可以使用ThreadLocal關鍵字,ThreadLocal可以為每個線程創建這個變量的副本并存到每個線程的存儲空間中(關于這個存儲空間后文會展開講述),從而確保共享變量對每個線程隔離:

2. ThreadLocal基礎使用示例

如上文所說ThreadLocal最典型的用法就是維護各個線程各自需要獨享變量,基于ThreadLocal為每個將每個線程的id存到線程內部,彼此之間互不影響。

ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

        Thread t1 = new Thread(() -> {
            THREAD_LOCAL.set("thread-0");
            Console.log("thread-0獲取線程內部緩存值:{}", THREAD_LOCAL.get());
        }, "t0");


        Thread t2 = new Thread(() -> {
            THREAD_LOCAL.set("thread-1");
            Console.log("thread-1獲取線程內部緩存值:{}", THREAD_LOCAL.get());
        }, "t1");

        t1.start();
        t2.start();

從輸出結果可以看出,兩個線程都用THREAD_LOCAL 在自己的內存空間中存儲了變量的副本,彼此互相隔離的使用。

thread-1獲取線程內部緩存值:thread-1
thread-0獲取線程內部緩存值:thread-0

二、ThreadLocal兩種應用場景

1. 日期格式化

我們經常會使用SimpleDateFormat執行日期格式化輸出,為了避免頻繁的創建SimpleDateFormat的繁瑣和開銷,我們可能會編寫出下面這樣一段代碼:

private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS");

    /**
     * 將傳入的秒轉為時間字符串
     * @param second
     * @return
     */
    public static String convert2DateStr(int second) {
        Date date = new Date(second);
        return dateFormat.format(date);
    }

對應我們也給出相應的使用示例:

//跑兩次傳入1970-01-01 08:00:00:00和1970-01-01 08:00:01:00的數據
        for (int i = 0; i < 2; i++) {
            int finalI = i;
            new Thread(() -> {
                String str = convert2DateStr(1000 * finalI);
                Console.log("轉換結果:{}", str);
            }).start();
        }

輸出結果如下,可以發現不同的時間卻輸出相同的結果:

原因也很簡單,SimpleDateFormat進行format時會通過calendar存儲當前轉換的日期,并發情況下,很可能其他線程會將當前calendar的值覆蓋,這也就是為什么我們線程0輸出了和線程1一樣的結果:

對應我們也給出SimpleDateFormat的format函數底層實現,可以看到其底層在轉換前會通過calendar存儲當前需要轉換的日期的值:

@Override
    public StringBuffer format(Date date, StringBuffer toAppendTo,
                               FieldPosition pos)
    {
        //......
      //調用format完成日期格式化字符串轉換
        return format(date, toAppendTo, pos.getFieldDelegate());
    }

    // Called from Format after creating a FieldDelegate
    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // 將calendar設置為需要格式化的日期
        calendar.setTime(date);
      
     //...... 
    }

基于該問題我們使用ThreadLocal為線程分配SimpleDateFormat,本質上就是針對每個線程分配一個專門的SimpleDateFormat:

private static ThreadLocal<SimpleDateFormat> THREAD_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS"));

    /**
     * 將傳入的秒轉為時間字符串
     *
     * @param second
     * @return
     */
    public static String convert2DateStr(int second) {
        Date date = new Date(second);
        return THREAD_LOCAL.get().format(date);
    }

2. 服務間調用的線程變量共享

我們日常web開發都會涉及到各種service的調用,例如某個controller需要調用完service1之后再調用service2。因為我們的controller和service都是單例的,所以如果我們希望多線程調用這些controller和service保證共享變量的隔離,也可以用到ThreadLocal。

為了實現這個示例,我們編寫了線程獲取共享變量的工具類:

public class MyUserContextHolder {
    private static ThreadLocal<User> holder = new ThreadLocal<>();

    public static ThreadLocal<User> getHolder() {
        return holder;
    }
}

service調用鏈示例如下,筆者創建service1之后,所有線程復用這個service完成了調用,并且在服務間調用直接通過ThreadLocal完成了線程副本共享:

public class MyThreadLocalGetUserId {

    private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);

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

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            MyService1 service1 = new MyService1();
            threadPool.submit(() -> {

                service1.doWork1("username" + (finalI+1));
            });

        }


    }
}


class MyService1 {

    private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);

    public void doWork1(String name) {

        logger.info("service1 存儲userName:" + name);
        ThreadLocal<String> holder = MyUserContextHolder.getHolder();
        holder.set(name);
        MyService2 service2 = new MyService2();
        service2.doWork2();
    }

}

class MyService2 {
    private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);

    public void doWork2() {
        ThreadLocal<String> holder = MyUserContextHolder.getHolder();

        logger.info("service2 獲取userName:" + holder.get());
        MyService3 service3 = new MyService3();
        service3.doWork3();
    }
}


class MyService3 {
    private static Logger logger = LoggerFactory.getLogger(MyThreadLocalGetUserId.class);

    public void doWork3() {
        ThreadLocal<String> holder = MyUserContextHolder.getHolder();

        logger.info("service3獲取 userName:" + holder.get());

// 避免oom問題
        holder.remove();
    }
}

從輸出結果來看,在單例對象情況下,既保證了同一個線程間變量共享。

也保證了不同線程之間變量的隔離。

三、ThreadLocal使用注意事項

1. 內存泄漏問題

我們有下面這樣一段web代碼,每次請求test0就會像線程池中的線程存一個4M的byte數組:

RestController
public class TestController {
    final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(100, 100, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());// 創建線程池,通過線程池,保證創建的線程存活

    final static ThreadLocal<Byte[]> localVariable = new ThreadLocal<Byte[]>();// 聲明本地變量

    @RequestMapping(value = "/test0")
    public String test0(HttpServletRequest request) {
        poolExecutor.execute(() -> {
            Byte[] c = new Byte[4* 1024* 1024];
            localVariable.set(c);// 為線程添加變量

        });
        return "success";
    }

   
}

我們將這個代碼打成jar包部署到服務器上并啟動。

java -jar -Xms100m -Xmx100m # 調整堆內存大小
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof  # 表示發生OOM時輸出日志文件,指定path為/tmp/heapdump.hprof
-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/tmp/heapTest.log # 打印日志、gc時間以及指定gc日志的路徑
demo-0.0.1-SNAPSHOT.jar

只需頻繁調用幾次,就會輸出OutOfMemoryError。

Exception in thread "pool-1-thread-5" java.lang.OutOfMemoryError: Java heap space
        at com.example.jstackTest.TestController.lambda$test0$0(TestController.java:25)
        at com.example.jstackTest.TestController$$Lambda$582/394910033.run(Unknown Source)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

問題的根本原因是我們沒有及時回收Thread從ThreadLocal中得到的變量副本。因為我們的使用的線程是來自線程池中,所以線程使用結束后并不會被銷毀,這就使得ThreadLocal中的變量副本會一直存儲與線程池中的線程中,導致OOM。

可能你會問了,不是說Java有GC回收機制嘛?為什么還會出現Thread中的ThreadLocalMap的value不會被回收呢?

我們上文提到ThreadLocal得到值,都會以ThreadLocal為key,ThreadLocal的initialValue方法得到的value作為值生成一個entry對象,存到當前線程的ThreadLocalMap中。 而我們的Entry的key是一個弱引用,一旦我們使用的threadLocal臨時變量用完被垃圾回收之后,這個key就會因為弱引用(只要垃圾回收器啟動就會被回收)的原因被回收,而我們這個key所對應的value仍然被線程池中的線程的強引用引用著,所以就遲遲無法回收,隨著時間推移每個線程都出現這種情況導致OOM。

所以我們每個線程使用完ThreadLocal之后,一定要使用remove方法清楚ThreadLocalMap中的value:

localVariable.remove()

從源碼中可以看到remove方法會遍歷當前線程map然后將強引用之間的聯系切斷,確保下次GC可以回收掉可以無用對象。

private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            //定位,并將entry清除
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

2. 空指針問題

使用ThreadLocal存放包裝類的時候也需要注意添加初始化方法,否則在拆箱時可能會出現空指針問題。

private  static ThreadLocal<Long> threadLocal = new ThreadLocal<>();


    public static void main(String[] args) {
        Long num = threadLocal.get();
        long sum=1+num;

    }

輸出錯誤:

Exception in thread "main" java.lang.NullPointerException
 at com.guide.base.MyThreadLocalNpe.main(MyThreadLocalNpe.java:11)

解決方式:

private  static ThreadLocal<Long> threadLocal = ThreadLocal.withInitial(()->new Long(0));

3. 線程重用問題

這個問題和OOM問題類似,在線程池中服用同一個線程未及時清理,導致下一次HTTP請求時得到上一次ThreadLocal存儲的結果。

ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> null);


 * 線程池中使用threadLocal示例
     *
     * @param accountCode
     * @return
     */
    @GetMapping("/account/getAccountByCode/{accountCode}")
    @SentinelResource(value = "getAccountByCode")
    ResultData<Map<String, Object>> getAccountByCode(@PathVariable(value = "accountCode") String accountCode) throws InterruptedException {
        Map<String, Object> result = new HashMap<>();
        
        CountDownLatch countDownLatch = new CountDownLatch(1);
        
        
        threadPool.submit(() -> {

            String before = Thread.currentThread().getName() + ":" + threadLocal.get();
            log.info("before:" + before);
            result.put("before", before);

            log.info("調用getByCode,請求參數:{}", accountCode);
            QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("account_code", accountCode);
            Account account = accountService.getOne(queryWrapper);

            String after = Thread.currentThread().getName() + ":" + account.getAccountName();
            result.put("after", account.getAccountName());
            log.info("after:" + after);

            threadLocal.set(account.getAccountName());
            
            //完成計算后,使用countDown按下倒計時門閂,通知主線程可以執行后續步驟
            countDownLatch.countDown();

        });

        //等待上述線程池完成
        countDownLatch.await();

        return ResultData.success(result);
    }

從輸出結果可以看出,我們第二次進行HTTP請求時,threadLocal第一get獲得了上一次請求的值,出現臟數據。

C:\Users\xxx>curl http://localhost:9000/account/getAccountByCode/demoData
{"status":100,"message":"操作成功","data":{"before":"pool-2-thread-1:null","after":"pool-2-thread-1:demoData"},"success":true,"timestamp":1678410699943}
C:\Users\xxx>curl http://localhost:9000/account/getAccountByCode/Zsy
{"status":100,"message":"操作成功","data":{"before":"pool-2-thread-1:demoData","after":"pool-2-thread-1:zsy"},"success":true,"timestamp":1678410707473}

解決方法也很簡單,手動添加一個threadLocal的remove方法即可:

@GetMapping("/account/getAccountByCode/{accountCode}")
    @SentinelResource(value = "getAccountByCode")
    ResultData<Map<String, Object>> getAccountByCode(@PathVariable(value = "accountCode") String accountCode) throws InterruptedException {
        Map<String, Object> result = new HashMap<>();

        CountDownLatch countDownLatch = new CountDownLatch(1);

        try {
            threadPool.submit(() -> {

                String before = Thread.currentThread().getName() + ":" + threadLocal.get();
                log.info("before:" + before);
                result.put("before", before);

                log.info("調用getByCode,請求參數:{}", accountCode);
                QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
                queryWrapper.eq("account_code", accountCode);
                Account account = accountService.getOne(queryWrapper);

                String after = Thread.currentThread().getName() + ":" + account.getAccountName();
                result.put("after", after);
                log.info("after:" + after);

                threadLocal.set(account.getAccountName());

                //完成計算后,使用countDown按下倒計時門閂,通知主線程可以執行后續步驟
                countDownLatch.countDown();

            });
        } finally {
            threadLocal.remove();
        }


        //等待上述線程池完成
        countDownLatch.await();

        return ResultData.success(result);
    }

四、基于源碼了解ThreadlLocal工作原理

1. ThreadlLocal如何做到線程隔離的?

我們下面這段代碼為例進行分析,通過withInitial聲明每個線程創建后續創建線程私有變量的SimpleDateFormat模板:

ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS"));

當我們執行get操作時,threadLocal 就會為當前線程完成內部map的初始化,然后通過initialValue獲取上一步聲明的SimpleDateFormat實例,由此保證每個線程內部都有一個獨有的SimpleDateFormat:

對應的我們給出ThreadlLocal的get的源碼,整體邏輯與上述差不多,即初始化線程內部的map,然后通過setInitialValue調用initialValue創建初始值存到線程的map中:

public T get() {
    //獲取當前線程
        Thread t = Thread.currentThread();
        //拿到當前線程中的map
        ThreadLocalMap map = getMap(t);
        //如果map不為空則取用當前這個ThreadLocal作為key取出值,否則通過setInitialValue完成ThreadLocal初始化
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }


private T setInitialValue() {
    //執行initialValue為當前線程創建變量value,在這里也就是我們要用的SimpleDateFormat 
        T value = initialValue();
        //獲取當前線程map,有則直接以ThreadLocal為key將SimpleDateFormat 設置進去,若沒有先創建再設置
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
//返回SimpleDateFormat 
        return value;
    }

2. ThreadLocalMap有什么特點?和HashMap有什么區別

我們通過源碼查看到這個map為ThreadLocalMap,它是由一個個Entry 構成的數組:

private Entry[] table;

并且每個Entry 的key是弱引用,這就意味著當觸發GC時,Entry 的key也就是ThreadLocal就會被回收。

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

除上面所說,thread中的map和hashmap還有一個不同點就是數據結構,因為threadLocal的適用場景特殊,所以大部分情況下其內部存儲空間不會存儲太多元素,所以出于簡單的考慮,線程中的map本質上就是一個數組,一旦發生沖突則直接通過線性探測法找到數組中空閑的位置將值存入:

private void set(ThreadLocal<?> key, Object value) {

           //......

            Entry[] tab = table;
            int len = tab.length;
            //定位鍵值對存儲的索引位置
            int i = key.threadLocalHashCode & (len-1);
      //通過線性探測法循環找到空閑位置存入元素
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

               //......
            }
      //找到合適的位置將元素存入
            tab[i] = new Entry(key, value);
            //更新一下容量信息
            int sz = ++size;
            //......
        }

3. ThreadLocal探測式清理和啟發式清理

上文中我們介紹了ThreadLocal使用不當所導致的內存泄漏問題,實際上Josh Bloch and Doug Lea也考慮過該問題,并在實現中也有對這些問題做一定的考慮,即探測式清理和啟發式清理。

我們先來說說探測式清理,它一般會在如下幾個時機觸發:

  • 調用get方法未找到元素時,調用getEntryAfterMiss觸發探測式清理
  • 調用remove方法時,完成對應的元素清理后
  • 底層map容量達到閾值時,觸發rehash
private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
       //遍歷threadLocal 底層的map
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
              //如果ket為空,就說明該key對應value沒有被使用,可直接設置為null等待gc
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                   //......
                }
            }
            return i;
        }

另一種則是探測式清理,它一般時跟隨著threadLocal寫入新元素或者覆蓋新元素時觸發的set和replaceStaleEntry,它會從操作的索引i開始遍歷并清理value:

private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
               //調用e的get方法為空,說明這個key即對應的線程的threadLocal被回收
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

需要強調的是,這兩種清理方式都有一定的局限性,使得thread map中key為空只會在觸發如下3個條件:

  • threadLocal被垃圾回收
  • key作為弱引用被回收
  • 觸發探測式或者啟動式清理

這也就意味著如果我們的threadLocal采用static修飾后,對應的元空間threadLocal引用就會強引用著堆區的threadlocal實例,也就說theadlocal就會跟隨類對象存在于GC堆中,即與類對象生命周期相同,除非threadLocal對應類被卸載,否則這個threadLocal變量就不可能被GC即每個線程內部的value都無法被探測式或者啟發式清理掉,如果沒有顯示remove每個線程中threadLocal的map的話,還是存在內存溢出的風險:

五、ThreadLocal的不可繼承性

1. 通過代碼證明ThreadLocal的不可繼承性

如下代碼所示,ThreadLocal子線程無法拿到主線程維護的內部變量:

/**
 * ThreadLocal 不具備可繼承性
 */
public class ThreadLocalInheritTest {
    private static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    private static Logger logger = LoggerFactory.getLogger(ThreadLocalInheritTest.class);

    public static void main(String[] args) {
        THREAD_LOCAL.set("mainVal");
        logger.info("主線程的值為: " + THREAD_LOCAL.get());

        new Thread(() -> {
            try {
                //睡眠3s確保上述邏輯運行
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            logger.info("子線程獲取THREAD_LOCAL的值為:[{}]", THREAD_LOCAL.get());
        }).start();
    }

}

2. 使用InheritableThreadLocal實現主線程內部變量繼承

如下所示,我們將THREAD_LOCAL 改為InheritableThreadLocal類即可解決問題。

/**
 * ThreadLocal 不具備可繼承性
 */
public class ThreadLocalInheritTest {

    private static ThreadLocal<String> THREAD_LOCAL = new InheritableThreadLocal<>();

    private static Logger logger = LoggerFactory.getLogger(ThreadLocalInheritTest.class);

    public static void main(String[] args) {
        THREAD_LOCAL.set("mainVal");
        logger.info("主線程的值為: " + THREAD_LOCAL.get());

        new Thread(() -> {
            try {
                //睡眠3s確保上述邏輯運行
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            logger.info("子線程獲取THREAD_LOCAL的值為:[{}]", THREAD_LOCAL.get());
        }).start();
    }

}

3. 基于源碼剖析原因

因為 ThreadLocal會將變量存儲在線程的 ThreadLocalMap中,所以我們先看看InheritableThreadLocal的getMap方法,從而定位到了inheritableThreadLocals:

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

然后我們到Thread類去定位這個變量的使用之處,所以我們在創建線程的地方打了個斷點:

從而定位到這段初始化,它會獲取主線程的ThreadLocalMap并將主線程ThreadLocalMap中的值存到子線程的ThreadLocalMap中。

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

  //獲取當前線程的主線程
        Thread parent = currentThread();
       
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
            //將主線程的map的值存到子線程中
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        //......
    }

createInheritedMap內部就會調用ThreadLocalMap方法將主線程的ThreadLocalMap的值存到子線程的ThreadLocalMap中。

private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];
   //遍歷父線程數據復制到子線程map中
            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                     //......
                     //定位當前子線程bucket位置將value存入
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

4. ThreadLocal在Spring中的運用

其實針對日期格式化問題,Spring已經為我們內置好了相應的工具類即DateTimeContextHolder:

private static final ThreadLocal<DateTimeContext> dateTimeContextHolder =
   new NamedThreadLocal<>("DateTimeContext");

該工具類和simpledateformate差不多,使用示例如下所示,是spring封裝的,使用起來也很方便:

public class DateTimeContextHolderTest {


    protected static final Logger logger = LoggerFactory.getLogger(DateTimeContextHolderTest.class);

    private final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    private Set<String> set = new ConcurrentHashSet<String>();

    @Test
    public void test_withLocale_same() throws Exception {
        ExecutorService threadPool = Executors.newFixedThreadPool(30);

        for (int i = 0; i < 30; i++) {
            int finalI = i;
            threadPool.execute(() -> {
                LocalDate currentdate = LocalDate.now();
                int year = currentdate.getYear();
                int month = currentdate.getMonthValue();
                int day = 1 + finalI;
                LocalDate date = LocalDate.of(year, month, day);

                DateTimeFormatter fmt = DateTimeContextHolder.getFormatter(formatter, null);
                String text = date.format(fmt);
                set.add(text);
                logger.info("轉換后的時間為" + text);
            });
        }

        threadPool.shutdown();
        while (!threadPool.isTerminated()) {

        }

        logger.info("查看去重后的數量"+set.size());


    }
}

5. 為什么JDK建議將ThreadLocal設置為static

我們都知道使用static是屬于類,存在于方法區中,即修飾的變量是全局共享的,這意味著當前ThreadLocal在通過static之后,即所有的實例對象都共享一個ThreadLocal。從而避免重復創建TSO(Thread Specific Object)即ThreadLocal所關聯的對象的創建的開銷, 以及這種方案使得即使出現內存泄漏也是O(1)級別的內存泄露。對應的實例變量的ThreadLocal的O(n)內存泄漏,這就不必多說。

六、小結

  • ThreadLocal通過在將共享變量拷貝一份到每個線程內部的ThreadLocalMap保證線程安全。
  • ThreadLocal使用完成后記得使用remove方法手動清理線程中的ThreadLocalMap過期對象,避免OOM和一些業務上的錯誤。
  • ThreadLocal是不可被繼承了,如果想使用主線的的ThreadLocal,就必須使用InheritableThreadLocal。
責任編輯:趙寧寧 來源: 寫代碼的SharkChili
相關推薦

2024-06-19 10:08:34

GoChannel工具

2015-08-17 16:05:35

javascript對象編程

2020-07-10 07:58:14

Linux

2024-11-15 11:11:48

2024-01-03 07:56:50

2022-05-18 09:01:19

JSONJavaScript

2023-10-09 18:52:14

SOLIDJava

2022-08-19 10:31:32

Kafka大數據

2024-09-02 09:00:59

2024-01-10 18:01:22

編程技巧Java 12

2019-01-30 14:14:16

LinuxUNIX操作系統

2015-10-20 09:46:33

HTTP網絡協議

2018-10-26 14:10:21

2023-05-08 15:25:19

Python編程語言編碼技巧

2023-04-20 14:31:20

Python開發教程

2024-06-13 09:10:22

2024-01-09 13:58:22

PandasPython數據分析

2023-12-26 12:10:13

2022-08-26 14:46:31

機器學習算法線性回歸

2019-11-06 10:56:59

Python數據分析TGI
點贊
收藏

51CTO技術棧公眾號

91亚洲视频在线观看| 精品无码人妻一区| xxxx在线视频| 久久精品亚洲麻豆av一区二区 | 亚洲人成电影网站| 久久久久久久久久久久久久久国产 | 久久伊人精品天天| 给我看免费高清在线观看| 成人精品视频在线观看| 欧美性猛交视频| 黄色污污在线观看| 浮生影视网在线观看免费| 成人免费视频国产在线观看| 国产剧情久久久久久| 自拍偷拍欧美亚洲| 欧美成人tv| 中文字幕在线精品| 亚洲AV无码片久久精品| 77成人影视| 8x福利精品第一导航| 久久婷婷国产精品| 爱啪视频在线观看视频免费| 综合欧美一区二区三区| 色姑娘综合av| 青青色在线视频| 成人免费高清在线观看| 成人欧美一区二区三区黑人| www.久久视频| 久久精品中文| 57pao成人永久免费视频| 久久久www成人免费毛片| 国产精品传媒精东影业在线| 国产一区av在线| 三叶草欧洲码在线| 国产suv精品一区二区四区视频| 欧美精品在线一区二区| 香蕉视频禁止18| 第四色男人最爱上成人网| 精品福利免费观看| 欧美亚洲日本一区二区三区| 国产高清在线a视频大全| 一区二区三区欧美在线观看| 国产福利片一区二区| 亚洲成人三级| 中文字幕一区二区三区乱码在线 | 欧美~级网站不卡| 精品国产一区二区三区久久久狼 | 亚洲激情 欧美| 亚洲国产aⅴ精品一区二区| 欧美一卡二卡在线观看| 国内av免费观看| 欧美午夜在线播放| 日韩亚洲欧美在线| 一级黄色片毛片| 精品人人人人| 精品香蕉一区二区三区| 久久国产精品影院| 免费毛片在线不卡| 尤物精品国产第一福利三区| 欧美午夜激情影院| 久久激情电影| 久久综合九色九九| 国产亚洲精品久久久久久无几年桃 | 99精品国产99久久久久久白柏| 久久66热这里只有精品| 九色视频在线观看免费播放 | 日韩精品免费观看| 扒开jk护士狂揉免费| 成人免费在线观看av| 日韩在线观看免费高清完整版| 中文字幕亚洲欧美日韩| 亚洲天堂偷拍| 日产精品99久久久久久| 在线观看中文字幕码| 国产伦精品一区二区三区免费| 懂色av一区二区三区在线播放| 偷拍自拍在线| 国产精品三级久久久久三级| 青青草综合视频| 国产伦理精品| 欧美日韩国产影片| 少妇熟女视频一区二区三区| 网曝91综合精品门事件在线| 日韩中文字幕第一页| 国产一级片视频| 美女被久久久| 97在线资源站| 国产日产精品久久久久久婷婷| 中文字幕日韩精品一区| 欧美黑人经典片免费观看| 免费污视频在线一区| 日韩三级.com| 免费看污片的网站| 欧美日韩亚洲一区三区| 国产成人高潮免费观看精品| 国产高清视频免费观看| 久久先锋影音av鲁色资源网| 国产免费xxx| 色8久久影院午夜场| 日韩一区二区在线观看| 亚洲国产av一区| 亚洲一级一区| 成人黄色在线观看| 国产在线你懂得| 亚洲一二三专区| 青青草原国产在线视频| 日韩美女精品| 欧美黑人xxx| 亚洲天堂2021av| 99re成人在线| 中文字幕人妻熟女人妻洋洋| 草莓视频成人appios| 亚洲精品福利在线| 久久国产精品波多野结衣av| 麻豆成人91精品二区三区| 久久精品第九区免费观看 | 亚洲国产一区二区视频| 狠狠干狠狠操视频| 午夜欧洲一区| 久久免费国产视频| 精品人妻无码一区二区色欲产成人 | 亚洲一区二区三区精品在线观看| 免费看男女www网站入口在线| 日韩欧美中文字幕一区| 欧美性x x x| 蜜桃视频在线一区| 欧洲亚洲一区二区三区四区五区| 国产色播av在线| 欧美成人高清电影在线| 免费中文字幕日韩| 美女网站色91| 亚洲欧美日韩不卡一区二区三区| 日韩久久一区二区三区| 亚洲另类欧美自拍| 亚洲午夜18毛片在线看| a美女胸又www黄视频久久| 99热亚洲精品| 欧美有码在线| 国产91精品久久久久| 日韩一区免费视频| 天天操天天色综合| 亚洲一区二区观看| 久久久久久久欧美精品| 日本高清久久一区二区三区| 日本综合久久| 伊人伊成久久人综合网小说| 久久久999久久久| 中文字幕 久热精品 视频在线| 欧美性猛交久久久乱大交小说 | 国产又粗又爽又黄的视频| 2019中文亚洲字幕| 美女精品视频一区| 丰满大乳国产精品| 精品国产乱码久久久久酒店| 精品人妻一区二区三区日产乱码卜| 国产欧美成人| 日韩av在线电影观看| 99精品在免费线偷拍| 日韩视频第一页| 亚洲精品一区二区三区新线路| 一区二区免费在线播放| 国产黑丝一区二区| 丝袜诱惑亚洲看片 | 亚洲免费高清| 奇米视频888战线精品播放| 成人国产精品一区二区免费麻豆| 久久中文字幕一区| 人妻无码中文字幕| 色综合久久中文字幕| 久久一级免费视频| 国产成人精品免费在线| 岳毛多又紧做起爽| 日韩一区二区中文| 国产精华一区二区三区| 日韩精品一区二区三区| 精品国偷自产在线| 天堂在线资源库| 欧美性受xxxx黑人xyx| 久久久久成人网站| 国产日本亚洲高清| 国产精品无码自拍| 久久一区视频| 日韩久久久久久久久久久久| 奇米狠狠一区二区三区| 91精品啪aⅴ在线观看国产| segui88久久综合9999| 国产一区二区三区欧美| 亚洲精品第五页| 欧美伊人久久久久久久久影院| 免费在线观看黄视频| 久久久久九九视频| 成人啪啪18免费游戏链接| 日本不卡一区二区| 国产av麻豆mag剧集| 欧美疯狂party性派对| 久精品国产欧美| 亚洲国产aⅴ精品一区二区| 国产国语videosex另类| 青青在线视频| 久久精品久久久久电影| 欧洲毛片在线| 精品日韩成人av| 一区二区视频网| 欧美日韩亚洲天堂| 精品午夜福利视频| 国产精品久久久久久久久图文区 | 亚洲免费看av| 亚洲专区一区二区三区| www国产无套内射com| 欧美gay男男猛男无套| 免费观看成人在线| 999久久久精品一区二区| 成人黄色av网| 99欧美精品| 97涩涩爰在线观看亚洲| 中中文字幕av在线| 久久精品视频在线播放| aⅴ在线视频男人的天堂| 日韩国产高清视频在线| 六月丁香综合网| 欧美哺乳videos| a级片在线视频| 欧美蜜桃一区二区三区| 黄色一区二区视频| 在线免费不卡视频| 黄色一级片免费在线观看| 亚洲成av人在线观看| 校园春色 亚洲| 亚洲精品视频免费观看| 91狠狠综合久久久| 中文字幕一区二区三中文字幕| 99久久99久久精品免费看小说.| 久久日一线二线三线suv| 日韩精品卡通动漫网站| aaa国产一区| 黄色短视频在线观看| 99热在这里有精品免费| 午夜av免费看| 91社区在线播放| 丰满少妇在线观看资源站| 96av麻豆蜜桃一区二区| 亚洲男女在线观看| 久久综合av免费| 波多野结衣一本| 国产午夜精品在线观看| 神马久久久久久久久久久 | 国产无精乱码一区二区三区| 亚洲在线中文字幕| 久久精品视频久久| 五月天激情小说综合| 日本三级一区二区| 欧美性猛交xxxx乱大交蜜桃| av一级在线观看| 欧美亚洲一区三区| 国产又黄又大又粗的视频| 欧美丰满美乳xxx高潮www| 精品国产九九九| 亚洲激情 国产| www.亚洲.com| 超碰精品一区二区三区乱码| 日本片在线看| 欧美亚洲国产成人精品| 麻豆精品蜜桃| 91夜夜未满十八勿入爽爽影院 | 韩国一区二区视频| 久久久久亚洲av无码网站| 99精品欧美一区二区三区小说| 久久久无码人妻精品一区| 中文字幕免费一区| av成人免费网站| 婷婷久久综合九色综合绿巨人| 亚洲欧美偷拍视频| 欧美精品777| 日本激情视频网站| 亚洲最大中文字幕| 伊人春色在线观看| 欧美在线免费观看| 96视频在线观看欧美| 久久av一区二区三区漫画| 欧美一区二区三区激情视频| 超级碰在线观看| 久久一二三区| 亚洲视频在线不卡| 91免费视频网址| 国产一区二区精彩视频| 欧美网站在线观看| 国产乱淫a∨片免费观看| 亚洲精品动漫100p| av片在线观看| 国产精品jizz在线观看麻豆| 午夜视频在线观看精品中文| 日本精品视频一区| 亚洲午夜精品久久久久久app| 成人性做爰aaa片免费看不忠| 国产精品91一区二区| 真实乱视频国产免费观看| 亚洲永久精品大片| 一二三四区在线| 亚洲老司机av| 波多野结衣中文字幕久久| 国产精品稀缺呦系列在线| 久久综合另类图片小说| 午夜久久久久久久久久久| 日韩国产高清在线| 7788色淫网站小说| 一区二区三区在线免费观看| 伊人亚洲综合网| 亚洲天堂成人在线| 天堂av在线| 国产精品视频免费观看| 久久精品国内一区二区三区水蜜桃 | 精品伦精品一区二区三区视频密桃| 性做久久久久久| 亚洲国产视频一区二区三区| 久久精品成人动漫| 深夜视频一区二区| 蜜桃狠狠色伊人亚洲综合网站| 欧美日韩精品| 中文字幕avav| 成人免费在线视频观看| 在线观看国产精品入口男同| 亚洲人成人99网站| 国产欧美一区二区三区精品酒店| 国产精品一区二区三区免费| 中国精品18videos性欧美| 九一精品久久久| 国产精品久久久久四虎| 依依成人在线视频| 日韩在线国产精品| 久久69成人| 懂色av一区二区三区四区五区| 七七婷婷婷婷精品国产| 一级在线观看视频| 欧美日韩精品一区二区三区| av中文天堂在线| 国产精品入口免费视| 日韩情爱电影在线观看| 国产视频1区2区3区| 国产精品久久久久国产精品日日| 性色av一区二区三区四区| 尤物九九久久国产精品的特点| 深夜视频一区二区| 亚洲午夜精品一区二区| 看电视剧不卡顿的网站| 日本裸体美女视频| 日韩一区二区三区免费看| 丝袜美腿av在线| 俄罗斯精品一区二区| 99热在线精品观看| 在线观看国产精品一区| 欧美三级日韩三级国产三级| 免费a级人成a大片在线观看| 亚洲一区亚洲二区| 国产精品v日韩精品v欧美精品网站| 在线播放av网址| 精品久久久久久久久中文字幕| 日本中文字幕一区二区有码在线 | 女性女同性aⅴ免费观女性恋| 91视视频在线观看入口直接观看www| 亚洲第一网站在线观看| 一区二区亚洲欧洲国产日韩| 亚洲狼人在线| 久久99久久99精品| 国产三级欧美三级| 国产精品伦理一区| 久久久久久久97| 免费国产自久久久久三四区久久| 中文字幕av专区| 亚洲一区二区在线免费观看视频| 婷婷在线免费视频| 国产成人综合精品在线| 仙踪林久久久久久久999| 国产精品成人99一区无码| 色综合激情久久| 成人免费观看视频大全| 国产一区二区免费电影| 日韩精品亚洲专区| 1024手机在线视频| 亚洲免费一级电影| 韩国三级大全久久网站| 国产午夜福利视频在线观看| 国产精品欧美极品| 日本韩国在线观看| 国产日本欧美视频| 日韩一级大片| 亚洲精品电影院| 日韩国产精品一区| 国产精品亚洲一区二区在线观看| 欧美日韩黄色一级片| 亚洲三级在线免费| 青青草在线免费视频| 亚洲999一在线观看www| 久久一区二区三区四区五区 | 激情久久久久久| 欧美一区二区三区粗大| 亚洲国产成人一区| 在线观看亚洲精品福利片| av免费中文字幕| 亚洲一二三四在线观看| 麻豆网站在线免费观看|