本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!前端
接下來將承接上文,介紹ThreadLocal的另外一個經典使用場景後端
上一遍文章連接:ThreadLocal經典使用場景安全
ThreadLocal 用做保存每一個線程獨享的對象,爲每一個線程都建立一個副本,這樣每一個線程均可以修改本身所擁有的副本, 而不會影響其餘線程的副本,確保了線程安全。markdown
前幾天我在網上看到一篇介紹,寫的確實不錯,咱們一塊兒來學習下。多線程
假設咱們有 10 個線程同時對應 10 個 SimpleDateFormat 對象。看輸出什麼,咱們來看下面這種寫法:併發
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
String date = new ThreadLocalDemo02().date(finalI);
System.out.println(date);
}).start();
Thread.sleep(100);
}
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
return simpleDateFormat.format(date);
}
}
複製代碼
上面的代碼利用了一個 for 循環來完成這個需求。for 循環一共循環 10 次,每一次都會新建一個線程,而且每個線程都會在 date 方法中建立一個 SimpleDateFormat 對象,示意圖以下:ide
能夠看出一共有 10 個線程,對應 10 個 SimpleDateFormat 對象。post
代碼的運行結果:學習
可是線程不能無休地建立下去,由於線程越多,所佔用的資源也會越多。假設咱們須要 1000 個任務,那就不能再用 for 循環的方法了,而是應該使用線程池來實現線程的複用,不然會消耗過多的內存等資源。測試
利用線程池:
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDemo03().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
return dateFormat.format(date);
}
}
複製代碼
能夠看出,咱們用了一個 16 線程的線程池,而且給這個線程池提交了 1000 次任務。每一個任務中它作的事情和以前是同樣的,仍是去執行 date 方法,而且在這個方法中建立一個 simpleDateFormat 對象。程序的一種運行結果是(多線程下,運行結果不惟一):
00:00
00:07
00:04
00:02
...
16:29
16:28
16:27
16:26
16:39
複製代碼
程序運行結果正確,把從 00:00 到 16:39 這 1000 個時間給打印了出來,而且沒有重複的時間。咱們把這段代碼用圖形化給表示出來,如圖所示:
圖的左側是一個線程池,右側是 1000 個任務。咱們剛纔所作的就是每一個任務都建立了一個 simpleDateFormat 對象,也就是說,1000 個任務對應 1000 個 simpleDateFormat 對象。
可是這樣作是沒有必要的,由於這麼多對象的建立是有開銷的,而且在使用完以後的銷燬一樣是有開銷的,並且這麼多對象同時存在在內存中也是一種內存的浪費。
如今咱們就來優化一下。既然不想要這麼多的 simpleDateFormat 對象,最簡單的就是隻用一個就能夠了
咱們用下面的代碼來演示只用一個 simpleDateFormat 對象的狀況:
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDemo04().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
return dateFormat.format(date);
}
}
複製代碼
在代碼中能夠看出,其餘的沒有變化,變化之處就在於,咱們把這個 simpleDateFormat 對象給提取了出來,變成 static 靜態變量,須要用的時候直接去獲取這個靜態對象就能夠了。看上去省略掉了建立 1000 個 simpleDateFormat 對象的開銷,看上去沒有問題,咱們用圖形的方式把這件事情給表示出來:
從圖中能夠看出,咱們有不一樣的線程,而且線程會執行它們的任務。可是不一樣的任務所調用的 simpleDateFormat 對象都是同一個,因此它們所指向的那個對象都是同一個,可是這樣一來就會有線程不安全的問題。
00:04
00:04
00:05
00:04
...
16:15
16:14
16:13
複製代碼
執行上面的代碼就會發現,控制檯所打印出來的和咱們所期待的是不一致的。咱們所期待的是打印出來的時間是不重複的,可是能夠看出在這裏出現了重複,好比第一行和第二行都是 04 秒,這就表明它內部已經出錯了。
出錯的緣由就在於,simpleDateFormat 這個對象自己不是一個線程安全的對象,不該該被多個線程同時訪問。因此咱們就想到了一個解決方案,用 synchronized 來加鎖。因而代碼就修改爲下面的樣子:
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDemo05().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
String s = null;
synchronized (ThreadLocalDemo05.class) {
s = dateFormat.format(date);
}
return s;
}
}
複製代碼
能夠看出在 date 方法中加入了 synchronized 關鍵字,把 simpleDateFormat 的調用給上了鎖。
00:00
00:01
00:06
...
15:56
16:37
16:36
複製代碼
這樣的結果是正常的,沒有出現重複的時間。可是因爲咱們使用了 synchronized 關鍵字,就會陷入一種排隊的狀態,多個線程不能同時工做,這樣一來,總體的效率就被大大下降了。有沒有更好的解決方案呢?
咱們但願達到的效果是,既不浪費過多的內存,同時又想保證線程安全。通過思考得出,可讓每一個線程都擁有一個本身的 simpleDateFormat 對象來達到這個目的,這樣就能一箭雙鵰了。
那麼,要想達到上面所說目的,咱們就可使用 ThreadLocal。示例代碼以下所示:
public class ThreadLocalDemo06 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDemo06().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("mm:ss");
}
};
}
複製代碼
在這段代碼中,咱們使用了 ThreadLocal 幫每一個線程去生成它本身的 simpleDateFormat 對象,對於每一個線程而言,這個對象是獨享的。但與此同時,這個對象就不會創造過多,一共只有 16 個,由於線程只有 16 個。
00:05
00:04
00:01
...
16:37
16:36
16:32
複製代碼
這個結果是正確的,不會出現重複的時間。
咱們用圖來看一下當前的這種狀態:
在圖中的左側能夠看到,這個線程池一共有 16 個線程,對應 16 個 simpleDateFormat 對象。而在這個圖畫的右側是 1000 個任務,任務是很是多的,和原來同樣有 1000 個任務。可是這裏最大的變化就是,雖然任務有 1000 個,可是咱們再也不須要去建立 1000 個 simpleDateFormat 對象了。即使任務再多,最終也只會有和線程數相同的 simpleDateFormat 對象。這樣既高效地使用了內存,又同時保證了線程安全。
以上就是第一種很是典型的適合使用 ThreadLocal 的場景。
感謝您的閱讀,歡迎點贊、評論、分享
學無止境、天天積累一點點。分享多一點點!