<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          1.5w字,20 圖帶你徹底掌握 ThreadLocal!

          共 20028字,需瀏覽 41分鐘

           ·

          2021-05-18 06:10

          在 Java 中,如果要問哪個類使用簡單,但用好最不簡單?我想你的腦海中一定會浮現(xiàn)出一次詞——“ThreadLocal”。

          確實如此,ThreadLocal 原本設(shè)計是為了解決并發(fā)時,線程共享變量的問題,但由于過度設(shè)計,如弱引用和哈希碰撞,從而導(dǎo)致它的理解難度大和使用成本高等問題。當然,如果稍有不慎還是導(dǎo)致臟數(shù)據(jù)、內(nèi)存溢出、共享變量更新等問題,但即便如此,ThreadLocal 依舊有適合自己的使用場景,以及無可取代的價值,比如本文要介紹了這兩種使用場景,除了 ThreadLocal 之外,還真沒有合適的替代方案。

          使用場景1:本地變量

          我們以多線程格式化時間為例,來演示 ThreadLocal 的價值和作用,當我們在多個線程中格式化時間時,通常會這樣操作。

          ① 2個線程格式化

          當有 2 個線程進行時間格式化時,我們可以這樣寫:

          import java.text.SimpleDateFormat;
          import java.util.Date;

          public class Test {
          public static void main(String[] args) throws InterruptedException {
          // 創(chuàng)建并啟動線程1
          Thread t1 = new Thread(new Runnable() {
          @Override
          public void run() {
          // 得到時間對象
          Date date = new Date(1 * 1000);
          // 執(zhí)行時間格式化
          formatAndPrint(date);
          }
          });
          t1.start();
          // 創(chuàng)建并啟動線程2
          Thread t2 = new Thread(new Runnable() {
          @Override
          public void run() {
          // 得到時間對象
          Date date = new Date(2 * 1000);
          // 執(zhí)行時間格式化
          formatAndPrint(date);
          }
          });
          t2.start();
          }

          /**
          * 格式化并打印結(jié)果
          * @param date 時間對象
          */

          private static void formatAndPrint(Date date) {
          // 格式化時間對象
          SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
          // 執(zhí)行格式化
          String result = simpleDateFormat.format(date);
          // 打印最終結(jié)果
          System.out.println("時間:" + result);
          }
          }
          以上程序的執(zhí)行結(jié)果為:

          上面的代碼因為創(chuàng)建的線程數(shù)量并不多,所以我們可以給每個線程創(chuàng)建一個私有對象 SimpleDateFormat 來進行時間格式化。

          ② 10個線程格式化

          當線程的數(shù)量從 2 個升級為 10 個時,我們可以使用 for 循環(huán)來創(chuàng)建多個線程執(zhí)行時間格式化,具體實現(xiàn)代碼如下:

          import java.text.SimpleDateFormat;
          import java.util.Date;

          public class Test {
          public static void main(String[] args) throws InterruptedException {
          for (int i = 0; i < 10; i++) {
          int finalI = i;
          // 創(chuàng)建線程
          Thread thread = new Thread(new Runnable() {
          @Override
          public void run() {
          // 得到時間對象
          Date date = new Date(finalI * 1000);
          // 執(zhí)行時間格式化
          formatAndPrint(date);
          }
          });
          // 啟動線程
          thread.start();
          }
          }
          /**
          * 格式化并打印時間
          * @param date 時間對象
          */

          private static void formatAndPrint(Date date) {
          // 格式化時間對象
          SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
          // 執(zhí)行格式化
          String result = simpleDateFormat.format(date);
          // 打印最終結(jié)果
          System.out.println("時間:" + result);
          }
          }

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


          從上述結(jié)果可以看出,雖然此時創(chuàng)建的線程數(shù)和 SimpleDateFormat 的數(shù)量不算少,但程序還是可以正常運行的。

          ③ 1000個線程格式化

          然而當我們將線程的數(shù)量從 10 個變成 1000 個的時候,我們就不能單純的使用 for 循環(huán)來創(chuàng)建 1000 個線程的方式來解決問題了,因為這樣頻繁的新建和銷毀線程會造成大量的系統(tǒng)開銷和線程過度爭搶 CPU 資源的問題。

          所以經(jīng)過一番思考后,我們決定使用線程池來執(zhí)行這 1000 次的任務(wù),因為線程池可以復(fù)用線程資源,無需頻繁的新建和銷毀線程,也可以通過控制線程池中線程的數(shù)量來避免過多線程所導(dǎo)致的 CPU 資源過度爭搶和線程頻繁切換所造成的性能問題,而且我們可以將 SimpleDateFormat 提升為全局變量,從而避免每次執(zhí)行都要新建 SimpleDateFormat 的問題,于是我們寫下了這樣的代碼:

          import java.text.SimpleDateFormat;
          import java.util.Date;
          import java.util.concurrent.LinkedBlockingQueue;
          import java.util.concurrent.ThreadPoolExecutor;
          import java.util.concurrent.TimeUnit;

          public class App {
          // 時間格式化對象
          private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");

          public static void main(String[] args) throws InterruptedException {
          // 創(chuàng)建線程池執(zhí)行任務(wù)
          ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
          TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
          for (int i = 0; i < 1000; i++) {
          int finalI = i;
          // 執(zhí)行任務(wù)
          threadPool.execute(new Runnable() {
          @Override
          public void run() {
          // 得到時間對象
          Date date = new Date(finalI * 1000);
          // 執(zhí)行時間格式化
          formatAndPrint(date);
          }
          });
          }
          // 線程池執(zhí)行完任務(wù)之后關(guān)閉
          threadPool.shutdown();
          }

          /**
          * 格式化并打印時間
          * @param date 時間對象
          */

          private static void formatAndPrint(Date date) {
          // 執(zhí)行格式化
          String result = simpleDateFormat.format(date);
          // 打印最終結(jié)果
          System.out.println("時間:" + result);
          }
          }
          以上程序的執(zhí)行結(jié)果為:


          當我們懷著無比喜悅的心情去運行程序的時候,卻發(fā)現(xiàn)意外發(fā)生了,這樣寫代碼竟然會出現(xiàn)線程安全的問題。從上述結(jié)果可以看出,程序的打印結(jié)果竟然有重復(fù)內(nèi)容的,正確的情況應(yīng)該是沒有重復(fù)的時間才對。

          PS:所謂的線程安全問題是指:在多線程的執(zhí)行中,程序的執(zhí)行結(jié)果與預(yù)期結(jié)果不相符的情況

          a) 線程安全問題分析

          為了找到問題所在,我們嘗試查看 SimpleDateFormatformat 方法的源碼來排查一下問題,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;
          }

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

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

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

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

          b) 解決線程安全問題:加鎖

          當出現(xiàn)線程安全問題時,我們想到的第一解決方案就是加鎖,具體的實現(xiàn)代碼如下:

          import java.text.SimpleDateFormat;
          import java.util.Date;
          import java.util.concurrent.LinkedBlockingQueue;
          import java.util.concurrent.ThreadPoolExecutor;
          import java.util.concurrent.TimeUnit;

          public class App {
          // 時間格式化對象
          private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");

          public static void main(String[] args) throws InterruptedException {
          // 創(chuàng)建線程池執(zhí)行任務(wù)
          ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
          TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
          for (int i = 0; i < 1000; i++) {
          int finalI = i;
          // 執(zhí)行任務(wù)
          threadPool.execute(new Runnable() {
          @Override
          public void run() {
          // 得到時間對象
          Date date = new Date(finalI * 1000);
          // 執(zhí)行時間格式化
          formatAndPrint(date);
          }
          });
          }
          // 線程池執(zhí)行完任務(wù)之后關(guān)閉
          threadPool.shutdown();
          }

          /**
          * 格式化并打印時間
          * @param date 時間對象
          */

          private static void formatAndPrint(Date date) {
          // 執(zhí)行格式化
          String result = null;
          // 加鎖
          synchronized (App.class) {
          result = simpleDateFormat.format(date);
          }
          // 打印最終結(jié)果
          System.out.println("時間:" + result);
          }
          }
          以上程序的執(zhí)行結(jié)果為:

          從上述結(jié)果可以看出,使用了 synchronized 加鎖之后程序就可以正常的執(zhí)行了。

          加鎖的缺點

          加鎖的方式雖然可以解決線程安全的問題,但同時也帶來了新的問題,當程序加鎖之后,所有的線程必須排隊執(zhí)行某些業(yè)務(wù)才行,這樣無形中就降低了程序的運行效率了

          有沒有既能解決線程安全問題,又能提高程序的執(zhí)行速度的解決方案呢?

          答案是:有的,這個時候 ThreadLocal就要上場了。

          c) 解決線程安全問題:ThreadLocal

          1.ThreadLocal 介紹

          ThreadLocal 從字面的意思來理解是線程本地變量的意思,也就是說它是線程中的私有變量,每個線程只能使用自己的變量。

          以上面線程池格式化時間為例,當線程池中有 10 個線程時,SimpleDateFormat 會存入 ThreadLocal 中,它也只會創(chuàng)建 10 個對象,即使要執(zhí)行 1000 次時間格式化任務(wù),依然只會新建 10 個 SimpleDateFormat 對象,每個線程調(diào)用自己的 ThreadLocal 變量。

          2.ThreadLocal 基礎(chǔ)使用

          ThreadLocal 常用的核心方法有三個:

          1. set 方法:用于設(shè)置線程獨立變量副本。沒有 set 操作的 ThreadLocal 容易引起臟數(shù)據(jù)。
          2. get 方法:用于獲取線程獨立變量副本。沒有 get 操作的 ThreadLocal 對象沒有意義。
          3. remove 方法:用于移除線程獨立變量副本。沒有 remove 操作容易引起內(nèi)存泄漏。

          ThreadLocal 所有方法如下圖所示:



          官方說明文檔:https://docs.oracle.com/javase/8/docs/api/

          ThreadLocal 基礎(chǔ)用法如下:

          /**
          * @公眾號:Java中文社群
          */

          public class ThreadLocalExample {
          // 創(chuàng)建一個 ThreadLocal 對象
          private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

          public static void main(String[] args) {
          // 線程執(zhí)行任務(wù)
          Runnable runnable = new Runnable() {
          @Override
          public void run() {
          String threadName = Thread.currentThread().getName();
          System.out.println(threadName + " 存入值:" + threadName);
          // 在 ThreadLocal 中設(shè)置值
          threadLocal.set(threadName);
          // 執(zhí)行方法,打印線程中設(shè)置的值
          print(threadName);
          }
          };
          // 創(chuàng)建并啟動線程 1
          new Thread(runnable, "MyThread-1").start();
          // 創(chuàng)建并啟動線程 2
          new Thread(runnable, "MyThread-2").start();
          }

          /**
          * 打印線程中的 ThreadLocal 值
          * @param threadName 線程名稱
          */

          private static void print(String threadName) {
          try {
          // 得到 ThreadLocal 中的值
          String result = threadLocal.get();
          // 打印結(jié)果
          System.out.println(threadName + " 取出值:" + result);
          } finally {
          // 移除 ThreadLocal 中的值(防止內(nèi)存溢出)
          threadLocal.remove();
          }
          }
          }
          以上程序的執(zhí)行結(jié)果為:


          從上述結(jié)果可以看出,每個線程只會讀取到屬于自己的 ThreadLocal 值。

          3.ThreadLocal 高級用法

          ① 初始化:initialValue
          public class ThreadLocalByInitExample {
          // 定義 ThreadLocal
          private static ThreadLocal<String> threadLocal = new ThreadLocal(){
          @Override
          protected String initialValue() {
          System.out.println("執(zhí)行 initialValue() 方法");
          return "默認值";
          }
          };

          public static void main(String[] args) {
          // 線程執(zhí)行任務(wù)
          Runnable runnable = new Runnable() {
          @Override
          public void run() {
          // 執(zhí)行方法,打印線程中數(shù)據(jù)(未設(shè)置值打印)
          print(threadName);
          }
          };
          // 創(chuàng)建并啟動線程 1
          new Thread(runnable, "MyThread-1").start();
          // 創(chuàng)建并啟動線程 2
          new Thread(runnable, "MyThread-2").start();
          }

          /**
          * 打印線程中的 ThreadLocal 值
          * @param threadName 線程名稱
          */

          private static void print(String threadName) {
          // 得到 ThreadLocal 中的值
          String result = threadLocal.get();
          // 打印結(jié)果
          System.out.println(threadName + " 得到值:" + result);
          }
          }
          以上程序的執(zhí)行結(jié)果為:


          當使用了 #threadLocal.set 方法之后,initialValue 方法就不會被執(zhí)行了,如下代碼所示:

          public class ThreadLocalByInitExample {
          // 定義 ThreadLocal
          private static ThreadLocal<String> threadLocal = new ThreadLocal() {
          @Override
          protected String initialValue() {
          System.out.println("執(zhí)行 initialValue() 方法");
          return "默認值";
          }
          };

          public static void main(String[] args) {
          // 線程執(zhí)行任務(wù)
          Runnable runnable = new Runnable() {
          @Override
          public void run() {
          String threadName = Thread.currentThread().getName();
          System.out.println(threadName + " 存入值:" + threadName);
          // 在 ThreadLocal 中設(shè)置值
          threadLocal.set(threadName);
          // 執(zhí)行方法,打印線程中設(shè)置的值
          print(threadName);
          }
          };
          // 創(chuàng)建并啟動線程 1
          new Thread(runnable, "MyThread-1").start();
          // 創(chuàng)建并啟動線程 2
          new Thread(runnable, "MyThread-2").start();
          }

          /**
          * 打印線程中的 ThreadLocal 值
          * @param threadName 線程名稱
          */

          private static void print(String threadName) {
          try {
          // 得到 ThreadLocal 中的值
          String result = threadLocal.get();
          // 打印結(jié)果
          System.out.println(threadName + "取出值:" + result);
          } finally {
          // 移除 ThreadLocal 中的值(防止內(nèi)存溢出)
          threadLocal.remove();
          }
          }
          }
          以上程序的執(zhí)行結(jié)果為:


          為什么 set 之后,初始化代碼就不執(zhí)行了?

          要理解這個問題,需要從 ThreadLocal.get() 方法的源碼中得到答案,因為初始化方法 initialValueThreadLocal 創(chuàng)建時并不會立即執(zhí)行,而是在調(diào)用了 get 方法只會才會執(zhí)行,測試代碼如下:

          import java.util.Date;

          public class ThreadLocalByInitExample {
          // 定義 ThreadLocal
          private static ThreadLocal<String> threadLocal = new ThreadLocal() {
          @Override
          protected String initialValue() {
          System.out.println("執(zhí)行 initialValue() 方法 " + new Date());
          return "默認值";
          }
          };
          public static void main(String[] args) {
          // 線程執(zhí)行任務(wù)
          Runnable runnable = new Runnable() {
          @Override
          public void run() {
          // 得到當前線程名稱
          String threadName = Thread.currentThread().getName();
          // 執(zhí)行方法,打印線程中設(shè)置的值
          print(threadName);
          }
          };
          // 創(chuàng)建并啟動線程 1
          new Thread(runnable, "MyThread-1").start();
          // 創(chuàng)建并啟動線程 2
          new Thread(runnable, "MyThread-2").start();
          }

          /**
          * 打印線程中的 ThreadLocal 值
          * @param threadName 線程名稱
          */

          private static void print(String threadName) {
          System.out.println("進入 print() 方法 " + new Date());
          try {
          // 休眠 1s
          Thread.sleep(1000);
          } catch (InterruptedException e) {
          e.printStackTrace();
          }
          // 得到 ThreadLocal 中的值
          String result = threadLocal.get();
          // 打印結(jié)果
          System.out.println(String.format("%s 取得值:%s %s",
          threadName, result, new Date()));
          }
          }
          以上程序的執(zhí)行結(jié)果為:

          從上述打印的時間可以看出:initialValue 方法并不是在 ThreadLocal 創(chuàng)建時執(zhí)行的,而是在調(diào)用 Thread.get 方法時才執(zhí)行的。

          接下來來看 Threadlocal.get 源碼的實現(xiàn):

          public T get() {
          // 得到當前的線程
          Thread t = Thread.currentThread();
          ThreadLocalMap map = getMap(t);
          // 判斷 ThreadLocal 中是否有數(shù)據(jù)
          if (map != null) {
          ThreadLocalMap.Entry e = map.getEntry(this);
          if (e != null) {
          @SuppressWarnings("unchecked")
          T result = (T)e.value;
          // 有 set 值,直接返回數(shù)據(jù)
          return result;
          }
          }
          // 執(zhí)行初始化方法【重點關(guān)注】
          return setInitialValue();
          }
          private T setInitialValue() {
          // 執(zhí)行初始化方法【重點關(guān)注】
          T value = initialValue();
          Thread t = Thread.currentThread();
          ThreadLocalMap map = getMap(t);
          if (map != null)
          map.set(this, value);
          else
          createMap(t, value);
          return value;
          }

          從上述源碼可以看出,當 ThreadLocal 中有值時會直接返回值 e.value,只有 Threadlocal 中沒有任何值時才會執(zhí)行初始化方法 initialValue

          注意事項—類型必須保持一致
          注意在使用 initialValue 時,返回值的類型要和 ThreadLoca 定義的數(shù)據(jù)類型保持一致,如下圖所示:

          如果數(shù)據(jù)不一致就會造成 ClassCaseException 類型轉(zhuǎn)換異常,如下圖所示:


          ② 初始化2:withInitial
          import java.util.function.Supplier;

          public class ThreadLocalByInitExample {
          // 定義 ThreadLocal
          private static ThreadLocal<String> threadLocal =
          ThreadLocal.withInitial(new Supplier<String>() {
          @Override
          public String get() {
          System.out.println("執(zhí)行 withInitial() 方法");
          return "默認值";
          }
          });
          public static void main(String[] args) {
          // 線程執(zhí)行任務(wù)
          Runnable runnable = new Runnable() {
          @Override
          public void run() {
          String threadName = Thread.currentThread().getName();
          // 執(zhí)行方法,打印線程中設(shè)置的值
          print(threadName);
          }
          };
          // 創(chuàng)建并啟動線程 1
          new Thread(runnable, "MyThread-1").start();
          // 創(chuàng)建并啟動線程 2
          new Thread(runnable, "MyThread-2").start();
          }

          /**
          * 打印線程中的 ThreadLocal 值
          * @param threadName 線程名稱
          */

          private static void print(String threadName) {
          // 得到 ThreadLocal 中的值
          String result = threadLocal.get();
          // 打印結(jié)果
          System.out.println(threadName + " 得到值:" + result);
          }
          }
          以上程序的執(zhí)行結(jié)果為:


          通過上述的代碼發(fā)現(xiàn),withInitial 方法的使用好和 initialValue 好像沒啥區(qū)別,那為啥還要造出兩個類似的方法呢?客官莫著急,繼續(xù)往下看。

          ③ 更簡潔的 withInitial 使用

          withInitial 方法的優(yōu)勢在于可以更簡單的實現(xiàn)變量初始化,如下代碼所示:

          public class ThreadLocalByInitExample {
          // 定義 ThreadLocal
          private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "默認值");
          public static void main(String[] args) {
          // 線程執(zhí)行任務(wù)
          Runnable runnable = new Runnable() {
          @Override
          public void run() {
          String threadName = Thread.currentThread().getName();
          // 執(zhí)行方法,打印線程中設(shè)置的值
          print(threadName);
          }
          };
          // 創(chuàng)建并啟動線程 1
          new Thread(runnable, "MyThread-1").start();
          // 創(chuàng)建并啟動線程 2
          new Thread(runnable, "MyThread-2").start();
          }

          /**
          * 打印線程中的 ThreadLocal 值
          * @param threadName 線程名稱
          */

          private static void print(String threadName) {
          // 得到 ThreadLocal 中的值
          String result = threadLocal.get();
          // 打印結(jié)果
          System.out.println(threadName + " 得到值:" + result);
          }
          }
          以上程序的執(zhí)行結(jié)果為:


          4.ThreadLocal 版時間格式化

          了解了 ThreadLocal 的使用之后,我們回到本文的主題,接下來我們將使用 ThreadLocal 來實現(xiàn) 1000 個時間的格式化,具體實現(xiàn)代碼如下:

          import java.text.SimpleDateFormat;
          import java.util.Date;
          import java.util.concurrent.LinkedBlockingQueue;
          import java.util.concurrent.ThreadPoolExecutor;
          import java.util.concurrent.TimeUnit;

          public class MyThreadLocalByDateFormat {
          // 創(chuàng)建 ThreadLocal 并設(shè)置默認值
          private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
          ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));

          public static void main(String[] args) {
          // 創(chuàng)建線程池執(zhí)行任務(wù)
          ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
          TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
          // 執(zhí)行任務(wù)
          for (int i = 0; i < 1000; i++) {
          int finalI = i;
          // 執(zhí)行任務(wù)
          threadPool.execute(new Runnable() {
          @Override
          public void run() {
          // 得到時間對象
          Date date = new Date(finalI * 1000);
          // 執(zhí)行時間格式化
          formatAndPrint(date);
          }
          });
          }
          // 線程池執(zhí)行完任務(wù)之后關(guān)閉
          threadPool.shutdown();
          // 線程池執(zhí)行完任務(wù)之后關(guān)閉
          threadPool.shutdown();
          }
          /**
          * 格式化并打印時間
          * @param date 時間對象
          */

          private static void formatAndPrint(Date date) {
          // 執(zhí)行格式化
          String result = dateFormatThreadLocal.get().format(date);
          // 打印最終結(jié)果
          System.out.println("時間:" + result);
          }
          }
          以上程序的執(zhí)行結(jié)果為:


          從上述結(jié)果可以看出,使用 ThreadLocal 也可以解決線程并發(fā)問題,并且避免了代碼加鎖排隊執(zhí)行的問題。

          使用場景2:跨類傳遞數(shù)據(jù)

          除了上面的使用場景之外,我們還可以使用 ThreadLocal 來實現(xiàn)線程中跨類、跨方法的數(shù)據(jù)傳遞。比如登錄用戶的 User 對象信息,我們需要在不同的子系統(tǒng)中多次使用,如果使用傳統(tǒng)的方式,我們需要使用方法傳參和返回值的方式來傳遞 User 對象,然而這樣就無形中造成了類和類之間,甚至是系統(tǒng)和系統(tǒng)之間的相互耦合了,所以此時我們可以使用 ThreadLocal 來實現(xiàn) User 對象的傳遞。

          確定了方案之后,接下來我們來實現(xiàn)具體的業(yè)務(wù)代碼。我們可以先在主線程中構(gòu)造并初始化一個 User 對象,并將此 User 對象存儲在 ThreadLocal 中,存儲完成之后,我們就可以在同一個線程的其他類中,如倉儲類或訂單類中直接獲取并使用 User 對象了,具體實現(xiàn)代碼如下。

          主線程中的業(yè)務(wù)代碼:

          public class ThreadLocalByUser {
          public static void main(String[] args) {
          // 初始化用戶信息
          User user = new User("Java");
          // 將 User 對象存儲在 ThreadLocal 中
          UserStorage.setUser(user);
          // 調(diào)用訂單系統(tǒng)
          OrderSystem orderSystem = new OrderSystem();
          // 添加訂單(方法內(nèi)獲取用戶信息)
          orderSystem.add();
          // 調(diào)用倉儲系統(tǒng)
          RepertorySystem repertory = new RepertorySystem();
          // 減庫存(方法內(nèi)獲取用戶信息)
          repertory.decrement();
          }
          }

          User 實體類:

          /**
          * 用戶實體類
          */

          class User {
          public User(String name) {
          this.name = name;
          }
          private String name;
          public String getName() {
          return name;
          }
          public void setName(String name) {
          this.name = name;
          }
          }

          ThreadLocal 操作類:

          /**
          * 用戶信息存儲類
          */

          class UserStorage {
          // 用戶信息
          public static ThreadLocal<User> USER = new ThreadLocal();

          /**
          * 存儲用戶信息
          * @param user 用戶數(shù)據(jù)
          */

          public static void setUser(User user) {
          USER.set(user);
          }
          }

          訂單類:

          /**
          * 訂單類
          */

          class OrderSystem {
          /**
          * 訂單添加方法
          */

          public void add() {
          // 得到用戶信息
          User user = UserStorage.USER.get();
          // 業(yè)務(wù)處理代碼(忽略)...
          System.out.println(String.format("訂單系統(tǒng)收到用戶:%s 的請求。",
          user.getName()));
          }
          }

          倉儲類:

          /**
          * 倉儲類
          */

          class RepertorySystem {
          /**
          * 減庫存方法
          */

          public void decrement() {
          // 得到用戶信息
          User user = UserStorage.USER.get();
          // 業(yè)務(wù)處理代碼(忽略)...
          System.out.println(String.format("倉儲系統(tǒng)收到用戶:%s 的請求。",
          user.getName()));
          }
          }
          以上程序的最終執(zhí)行結(jié)果:


          從上述結(jié)果可以看出,當我們在主線程中先初始化了 User 對象之后,訂單類和倉儲類無需進行任何的參數(shù)傳遞也可以正常獲得 User 對象了,從而實現(xiàn)了一個線程中,跨類和跨方法的數(shù)據(jù)傳遞

          總結(jié)

          使用 ThreadLocal 可以創(chuàng)建線程私有變量,所以不會導(dǎo)致線程安全問題,同時使用 ThreadLocal 還可以避免因為引入鎖而造成線程排隊執(zhí)行所帶來的性能消耗;再者使用 ThreadLocal 還可以實現(xiàn)一個線程內(nèi)跨類、跨方法的數(shù)據(jù)傳遞。


          參考 & 鳴謝

          《碼出高效:Java開發(fā)手冊》

          《Java 并發(fā)編程 78 講》


          推薦閱讀

          《徹底掌握分布式事務(wù)2PC、3PC模型》

          《線上問題復(fù)盤,JVM Fast Throw 的故事》

          《看 Guava、Spring 如何抽象觀察者模式?》


          我是龍臺,帝都 Java 后端開發(fā),專注 中間件、分布式、框架源碼 等知識分享,歡迎關(guān)注哈

          瀏覽 86
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久黄色网片老女人做爱 | 韩国美女操逼 | 78成人视频 | 色吊丝永久性观看网站在线观看 | 极品在线视频 |