學會這5種JS函式繼承方式,前端面試你至少成功50%
關於
執行緒池為學習 JUC 的第二章,持續更新,直到完結 。如果您感興趣,可以前往本人的 JUC 標籤中。如果您有問題或者本人這裡寫的不對、不足,歡迎評論留言,感謝!!
第二章 ThreadLocal
1、兩大使用場景
小夥伴們看完 兩大使用場景 後或許有些疑惑,請閱讀下面的“ 3 、重要方法”內容,可能會對您有所幫助。
1、執行緒需要一個獨享的物件(例如工具類,典型需要使用的類有 SimpleDateFormat 和 Random)。
1)併發使用靜態工具類是有很大風險的,此時可以使用 ThreadLocal 為每個執行緒都製作一個獨享的物件;
2)使用執行緒池加上 ThreadLocal 可以節省資源,不需要建立更多的物件浪費資源。
示例:
public class ThreadLocalNormal { public static ExecutorService service = Executors.newFixedThreadPool(5); public String dateFormat(int seconds){
//每個執行緒只會觸發一次下面的 initialValue() SimpleDateFormat format = ThreadSafeFormatter.threadLocal.get(); return format.format(newDate(seconds* 1000)); } public static void main(String[] args) { //使用此方法就不需要建立 1000 個 SimpleDateFormat 物件了,有多少個執行緒,就建立多少個 SimpleDateFormat 就夠了 for (int i = 0; i < 1000; i++) { int finalI = i; service.submit(() -> { System.out.println(new ThreadLocalNormal().dateFormat(finalI)); });try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } service.shutdown(); } } class ThreadSafeFormatter { public static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(){ @Override //這個函式是初始化作用 protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yy-mm-dd hh:mm:ss"); } }; }
2、執行緒內需要儲存全域性變數(例如在攔截器中獲取使用者資訊),可以讓不同方法直接使用,避免參數傳遞的麻煩。
1)例如用 ThreadLocal 儲存一些業務內容(使用者許可權資訊、從使用者系統獲取到的使用者名稱、user ID 等),就不需要把一個資訊從 service1 傳遞到 service2 在傳遞 service3 .......
2)每個執行緒使用 ThreadLocal 儲存的內容是不共享的,自己獨有的。
示例1:
/** * 避免傳遞引數的麻煩 */ public class ThreadLocalNormal3 { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2); for (int i = 0; i < 2; i++) { int j = i; executorService.execute(() -> { Service1 service1 = new Service1(); service1.process("張三"+j,"test"+j); }); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } executorService.shutdown(); } } class Service1 { public void process(String name,String password){ User user = new User(name,password); UserContextHolder.holder.set(user); new Service2(); } } class Service2 { public Service2(){ System.out.println(Thread.currentThread().getName()+"---我是Service2:" + UserContextHolder.holder.get()); new Service3(); } } class Service3 { public Service3(){ System.out.println(Thread.currentThread().getName()+"---我是Service3:" + UserContextHolder.holder.get()); } } class UserContextHolder { public static ThreadLocal<User> holder = new ThreadLocal<>(); } class User{ public String name; public String password; public User(String name, String password) { this.name = name; this.password = password; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", password='" + password + '\'' + '}'; } }
執行結果:
示例二:
public class ThreadLocalNormal3 { public static ThreadLocal local = new ThreadLocal(); public static void main(String[] args) throws InterruptedException { local.set("我是主執行緒"); new Thread(() -> { local.set("我是執行緒一"); System.out.println(local.get()); },"執行緒一").start(); Thread.sleep(1000); System.out.println(local.get()); } }
執行結果:
2、使用 ThreadLocal 的好處
1、達到執行緒安全
1)例如上面的場景一,我們可以把共享的靜態物件變成每個執行緒自己獨享的物件。
2、不需要加鎖,提高執行效率
1)既然是每個執行緒自己所獨享的資源,執行緒就是安全的,不需要加鎖。
3、更高效地利用記憶體、節省開銷
1)如上面場景一程式碼以及內容所示。
4、免去傳參的繁瑣:無論是場景一的工具類,還是場景二的使用者資訊,都可以在任何地方直接通過 ThreadLocal 拿到,再也不需要每次都傳遞同樣的引數。ThreadLocal 使得程式碼耦合度耕地,更優雅
3、重要方法
1、搞清楚 Thread、ThreadLocal、ThreadLocalMap 三者之間的關係:
如上圖所示:
1)每個 Thread 物件中持有一個 ThreadLocalMap 成員變數;
2)TreadLocalMap 是以 ThreadLocal 為 key 以獨享的資源為 value;
3)因為一個 Thread 可以有多個不同的獨享資源 ,所以一個 ThreadLocalMap 中是以ThreadLocal 為 key 來標識每一個資源;
4)線上程中,可以使用 ThreadLocal.get() 來獲取相應的資源。
2、T initialValue() :初始化
1)該方法會返回當前執行緒對應的“初始值”(如果沒有重寫該方法,則初始值為 null),這是一個延遲載入的方法,只有在第一次呼叫 get 的時候,才會觸發(除非.......在 3)敘述.....);
2)當前執行緒第一次使用 get 方法時,就會呼叫此方法,除非執行緒先前呼叫了 set 方法,在這種情況下,就不會呼叫該執行緒所對應自己的 initialValue 方法;
(以上兩點正是對應了以上寫的兩種場景示例程式碼)
3)通常,每個執行緒最多呼叫一次此方法,但如果已經呼叫了 remove() 後,在呼叫 get() ,則可以再次呼叫此方法;
4)如果不重寫本方法,這個方法會返回 null。一般使用匿名內部類的方法來重寫 initialValue() ,以便在後續使用中可以初始化副本物件。
3、void set(T t) :為這個執行緒設定一個新值,與setinitialValue() 很類似
原始碼:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //如果 map 不為空(該 map 可以儲存多個不同的 ThreadLocal),則更改目標 ThreadLocal 所對應的 value(this 為當前的 ThreadLocal) //注意:這個 map 以及 map 中的 key 和 value 都是儲存線上程中的 if (map != null) map.set(this, value); //如果為空,則存入 else createMap(t, value); }
4、T get() :得到ThreadLocalMap 中對應的 ThreadLocal 的值 。如果是首次呼叫 get(),則會呼叫 initialize 來得到這個值
1)get 方法是先取出當前執行緒的 ThreadLocalMap,然後呼叫 map.getEntry 方法,把本 ThreadLocal 的引用作為引數傳入,取出 map 中屬於本 ThreadLocal 的 value。
原始碼:
5、void remove() :刪除ThreadLocalMap 裡面所對應的 key 以及 value(下面程式碼中,刪除所對應的 key 為 local 物件)
1)remove() 在上面沒有做演示,在這裡簡單的做兩個示例:
示例一:
public class ThreadLocalNormal3 { public static ThreadLocal local = new ThreadLocal(); public static void main(String[] args) throws InterruptedException { IsNull(); } public static void IsNull() throws InterruptedException { local.set("我是主執行緒"); local.remove(); Thread.sleep(1000); System.out.println("IsNull 方法結果:"+local.get()); } }
示例二:
public class ThreadLocalNormal3 { public static ThreadLocal local = new ThreadLocal(); public static void main(String[] args) throws InterruptedException { IsNull(); } public static void IsNull() throws InterruptedException { local.set("我是主執行緒"); local.remove(); local.set("重新 set"); Thread.sleep(1000); System.out.println("IsNull 方法結果:"+local.get()); } }
原始碼:
6、兩種場景殊途同歸
- setInitialValue 和直接 set 最後都是利用 map.set() 來設定;
- 也就是說,最後都會對應到 ThreadLocalMap 的一個 Entry,只不過是起點和入口不一樣
7、演示:一個執行緒,一個 ThreadLocalMap,兩個 ThreadLocal
public class ThreadLocalDemo { //一個主執行緒 public static void main(String[] args) throws InterruptedException { //設定兩個ThreadLocal ThreadLocal local1 = new ThreadLocal(); ThreadLocal local2 = new ThreadLocal(); //分別為兩個 ThreadLocal 設定 value local1.set("我是 local1"); local2.set("我是 local2"); Thread.sleep(1000); //底層會自動的將這兩個 ThreadLocal 以及相應的 value 放入主執行緒的 ThreadLocalMap 中 //輸出 ThreadLocalMap 中的這兩個 ThreadLocal 對應的 value System.out.println(local1.get()); //我是 local1 System.out.println(local2.get()); //我是 local2 } }
4、ThreadLocalMap 類
1、ThreadLocalMap 類是每個執行緒 Thread 類裡面的變數,裡面最重要的是一個鍵值對陣列 Entry[] table。可以認為是一個 Map,鍵值對:
- 鍵:這個 ThreadLocal;
- 值:實際需要的成員變數,比如 user 或者 simpleDateFormat 物件。
2、ThreadLocalMap 這裡採用的是線性探測法,也就是說,如果發生衝突,就繼續找下一個空位置,而不是用連結串列拉鍊
5、其他
1、注意空指標異常,原理如程式碼中的註釋:
在進行 get 之前,必須先 set,否則可能報空指標異常,一下程式碼報空指標異常
public class ThreadLocalNPE { ThreadLocal<Long> longThreadLocal = new ThreadLocal<>(); public void set() { longThreadLocal.set(Thread.currentThread().getId()); } //上面定義的是 ThreadLocal<Long>,這裡如果返回值是 long,那麼 get 到的值要做拆箱,如果拿 null 去拆箱,會報空指標異常 public long get(){ return longThreadLocal.get(); } public static void main(String[] args) { ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE(); System.out.println(threadLocalNPE.get()); } }
2、共享物件問題
1)如果在每個執行緒中 ThreadLocal.set() 進去的東西本來就是多執行緒共享的同一個物件,比如 static 物件,那麼多個執行緒的 ThreadLocal.get() 取得的還是這個共享物件本身,還是有併發訪問問題;
2)如果可以不適用 ThreadLocal 就解決問題,那麼不要強行使用;
-
- 例如在任務數很少的時候,在區域性變數中可以新建物件就可以解決問題,那麼就不需要使用到 ThreadLocal。
3)優先使用框架的支援,而不是自己創造。
-
- 例如在 Spring 中,如果可以使用 RequestContextHolder,那麼就不需要自己維護 ThreadLocal,因為自己可能會忘記呼叫 remove() 等,造成記憶體洩漏。