點擊上方 "程序員小樂"關注, 星標或置頂一塊兒成長程序員
天天凌晨00點00分, 第一時間與你相約面試
每日英文安全
Sometimes,you are not happy if you see through everything.It's better to be naive and inattentive.bash
不少時候,看的太透反而不快樂,倒不如幼稚的沒心沒肺。
多線程
每日掏心話併發
人生,何須扭扭捏捏。生活,何苦畏首畏尾。勞累,聽聽音樂;傷心,侃侃心情;失敗,從頭再來;一生,須要的就是隨性。
app
來自:Oo魯毅oO | 責編:樂樂框架
連接:juejin.im/post/5e0d8765f265da5d332cde44dom
程序員小樂(ID:study_tech)第 746 次推文 圖片來自 Pexelside
往日回顧:互聯網形勢不容樂觀,360董事長周鴻禕年會送「免裁卡」安定軍心
每一個線程須要一個獨享對象(一般是工具類,典型須要使用的類有SimpleDateFormat和Random)
每一個Thread內有本身的實例副本,不共享
比喻:教材只有一本,一塊兒作筆記有線程安全問題。複印後沒有問題,使用ThradLocal至關於複印了教材。
每一個線程內須要保存全局變量(例如在攔截器中獲取用戶信息),可讓不一樣方法直接使用,避免參數傳遞的麻煩
/**
* 兩個線程打印日期
*/
public class ThreadLocalNormalUsage00 {
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage00().date(10);
System.out.println(date);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage00().date(104707);
System.out.println(date);
}
}).start();
}
public String date(int seconds) {
//參數的單位是毫秒,從1970.1.1 00:00:00 GMT 開始計時
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
}
複製代碼
運行結果
由於中國位於東八區,因此時間從1970年1月1日的8點開始計算的
/**
* 三十個線程打印日期
*/
public class ThreadLocalNormalUsage01 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 30; i++) {
int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage01().date(finalI);
System.out.println(date);
}
}).start();
//線程啓動後,休眠100ms
Thread.sleep(100);
}
}
public String date(int seconds) {
//參數的單位是毫秒,從1970.1.1 00:00:00 GMT 開始計時
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
}
複製代碼
運行結果
多個線程打印本身的時間(若是線程超級多就會產生性能問題),因此要使用線程池。
/**
* 1000個線程打印日期,用線程池來執行
*/
public class ThreadLocalNormalUsage02 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
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 ThreadLocalNormalUsage02().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
//參數的單位是毫秒,從1970.1.1 00:00:00 GMT 開始計時
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
}
複製代碼
運行結果
可是使用線程池時就會發現每一個線程都有一個本身的SimpleDateFormat對象,沒有必要,因此將SimpleDateFormat聲明爲靜態,保證只有一個
/**
* 1000個線程打印日期,用線程池來執行,出現線程安全問題
*/
public class ThreadLocalNormalUsage03 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
//只建立一次 SimpleDateFormat 對象,避免沒必要要的資源消耗
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh: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 ThreadLocalNormalUsage03().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
//參數的單位是毫秒,從1970.1.1 00:00:00 GMT 開始計時
Date date = new Date(1000 * seconds);
return dateFormat.format(date);
}
}
複製代碼
運行結果
出現了秒數相同的打印結果,這顯然是不正確的。
多個線程的task指向了同一個SimpleDateFormat對象,SimpleDateFormat是非線程安全的。
方案1:加鎖
格式化代碼是在最後一句return dateFormat.format(date);
,因此能夠爲最後一句代碼添加synchronized鎖
public String date(int seconds) {
//參數的單位是毫秒,從1970.1.1 00:00:00 GMT 開始計時
Date date = new Date(1000 * seconds);
String s;
synchronized (ThreadLocalNormalUsage04.class) {
s = dateFormat.format(date);
}
return s;
}
複製代碼
運行結果
運行結果中沒有發現相同的時間,達到了線程安全的目的
缺點:由於添加了synchronized,因此會保證同一時間只有一條線程能夠執行,這在高併發場景下確定不是一個好的選擇,因此看看其餘方案吧。
方案2:使用ThreadLocal
/**
* 利用 ThreadLocal 給每一個線程分配本身的 dateFormat 對象
* 不但保證了線程安全,還高效的利用了內存
*/
public class ThreadLocalNormalUsage05 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
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 ThreadLocalNormalUsage05().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
//參數的單位是毫秒,從1970.1.1 00:00:00 GMT 開始計時
Date date = new Date(1000 * seconds);
//獲取 SimpleDateFormat 對象
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new
ThreadLocal<SimpleDateFormat>(){
//建立一份 SimpleDateFormat 對象
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
}
複製代碼
運行結果
使用了ThreadLocal後不一樣的線程不會有共享的 SimpleDateFormat 對象,因此也就不會有線程安全問題
當前用戶信息須要被線程內的全部方法共享
方案1:傳遞參數
能夠將user做爲參數在每一個方法中進行傳遞,
缺點:可是這樣作會產生代碼冗餘問題,而且可維護性差。
方案2:使用Map
對此進行改進的方案是使用一個Map,在第一個方法中存儲信息,後續須要使用直接get()便可,
缺點:若是在單線程環境下能夠保證安全,可是在多線程環境下是不能夠的。若是使用加鎖和ConcurrentHashMap都會產生性能問題。
方案3:使用ThreadLocal,實現不一樣方法間的資源共享
使用 ThreadLocal 能夠避免加鎖產生的性能問題,也能夠避免層層傳遞參數來實現業務需求,就能夠實現不一樣線程中存儲不一樣信息的要求。
/**
* 演示 ThreadLocal 的用法2:避免參數傳遞的麻煩
*/
public class ThreadLocalNormalUsage06 {
public static void main(String[] args) {
new Service1().process();
}
}
class Service1 {
public void process() {
User user = new User("魯毅");
//將User對象存儲到 holder 中
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service2拿到用戶名: " + user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用戶名: " + user.name);
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
複製代碼
運行結果
讓某個須要用到的對象實現線程之間的隔離(每一個線程都有本身獨立的對象)
能夠在任何方法中輕鬆的獲取到該對象
根據共享對象生成的時機選擇使用initialValue方法仍是set方法
對象初始化的時機由咱們控制的時候使用initialValue 方式
若是對象生成的時機不禁咱們控制的時候使用 set 方式
達到線程安全的目的
不須要加鎖,執行效率高
更加節省內存,節省開銷
免去傳參的繁瑣,下降代碼耦合度
Thread
ThreadLocal
ThreadLocalMap
在Thread類內部有有ThreadLocal.ThreadLocalMap threadLocals = null;這個變量,它用於存儲ThreadLocal,由於在同一個線程當中能夠有多個ThreadLocal,而且屢次調用get()因此須要在內部維護一個ThreadLocalMap用來存儲多個ThreadLocal
T initialValue()
該方法用於設置初始值,而且在調用get()方法時纔會被觸發,因此是懶加載。
可是若是在get()以前進行了set()操做,這樣就不會調用initialValue()。
一般每一個線程只能調用一次本方法,可是調用了remove()後就能再次調用
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//獲取到了值直接返回resule
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生成的值,並在後續操做中進行set,最後將值返回
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
複製代碼
void set(T t)
爲這個線程設置一個新值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
複製代碼
T get()
獲取線程對應的value
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
複製代碼
void remove()
刪除對應這個線程的值
內存泄露;某個對象不會再被使用,可是該對象的內存卻沒法被收回
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
//調用父類,父類是一個弱引用
super(k);
//強引用
value = v;
}
}
複製代碼
強引用:當內存不足時觸發GC,寧願拋出OOM也不會回收強引用的內存
弱引用:觸發GC後便會回收弱引用的內存
正常狀況
當Thread運行結束後,ThreadLocal中的value會被回收,由於沒有任何強引用了
非正常狀況
當Thread一直在運行始終不結束,強引用就不會被回收,存在如下調用鏈 Thread-->ThreadLocalMap-->Entry(key爲null)-->value
由於調用鏈中的 value 和 Thread 存在強引用,因此value沒法被回收,就有可能出現OOM。
JDK的設計已經考慮到了這個問題,因此在set()、remove()、resize()方法中會掃描到key爲null的Entry,而且把對應的value設置爲null,這樣value對象就能夠被回收。
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
//當ThreadLocal爲空時,將ThreadLocal對應的value也設置爲null
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
複製代碼
可是隻有在調用set()、remove()、resize()這些方法時纔會進行這些操做,若是沒有調用這些方法而且線程不中止,那麼調用鏈就會一直存在,因此可能會發生內存泄漏。
調用remove()方法,就會刪除對應的Entry對象,能夠避免內存泄漏,因此使用完ThreadLocal後,要調用remove()方法。
class Service1 {
public void process() {
User user = new User("魯毅");
//將User對象存儲到 holder 中
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service2拿到用戶名: " + user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用戶名: " + user.name);
//手動釋放內存,從而避免內存泄漏
UserContextHolder.holder.remove();
}
}
複製代碼
/**
* ThreadLocal的空指針異常問題
*/
public class ThreadLocalNPE {
ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
public void set() {
longThreadLocal.set(Thread.currentThread().getId());
}
public Long get() {
return longThreadLocal.get();
}
public static void main(String[] args) {
ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
//若是get方法返回值爲基本類型,則會報空指針異常,若是是包裝類型就不會出錯
System.out.println(threadLocalNPE.get());
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
threadLocalNPE.set();
System.out.println(threadLocalNPE.get());
}
});
thread1.start();
}
}
複製代碼
若是get方法返回值爲基本類型,則會報空指針異常,若是是包裝類型就不會出錯。這是由於基本類型和包裝類型存在裝箱和拆箱的關係,形成空指針問題的緣由在於使用者。
若是在每一個線程中ThreadLocal.set()進去的東西原本就是多個線程共享的同一對象,好比static對象,那麼多個線程調用ThreadLocal.get()獲取的內容仍是同一個對象,仍是會發生線程安全問題。
若是在任務數不多的時候,在局部方法中建立對象就能夠解決問題,這樣就不須要使用ThreadLocal。
例如在Spring框架中,若是可使用RequestContextHolder,那麼就不須要本身維護ThreadLocal,由於本身可能會忘記調用remove()方法等,形成內存泄漏。
歡迎在留言區留下你的觀點,一塊兒討論提升。若是今天的文章讓你有新的啓發,學習能力的提高上有新的認識,歡迎轉發分享給更多人。
歡迎各位讀者加入程序員小樂技術羣,在公衆號後臺回覆「加羣」或者「學習」便可。
猜你還想看
關注「程序員小樂」,收看更多精彩內容
複製代碼
嘿,你在看嗎?