本文轉載自互聯網,侵刪html
什麼是併發java
在過去單CPU時代,單任務在一個時間點只能執行單一程序。以後發展到多任務階段,計算機能在同一時間點並行執行多任務或多進程。雖然並非真正意義上的「同一時間點」,而是多個任務或進程共享一個CPU,並交由操做系統來完成多任務間對CPU的運行切換,以使得每一個任務都有機會得到必定的時間片運行。git
隨着多任務對軟件開發者帶來的新挑戰,程序不在能假設獨佔全部的CPU時間、全部的內存和其餘計算機資源。一個好的程序榜樣是在其再也不使用這些資源時對其進行釋放,以使得其餘程序能有機會使用這些資源。程序員
再後來發展到多線程技術,使得在一個程序內部能擁有多個線程並行執行。一個線程的執行能夠被認爲是一個CPU在執行該程序。當一個程序運行在多線程下,就好像有多個CPU在同時執行該程序。github
多線程比多任務更加有挑戰。多線程是在同一個程序內部並行執行,所以會對相同的內存空間進行併發讀寫操做。這多是在單線程程序中歷來不會遇到的問題。其中的一些錯誤也未必會在單CPU機器上出現,由於兩個線程歷來不會獲得真正的並行執行。然而,更現代的計算機伴隨着多核CPU的出現,也就意味着不一樣的線程能被不一樣的CPU核獲得真正意義的並行執行。web
若是一個線程在讀一個內存時,另外一個線程正向該內存進行寫操做,那進行讀操做的那個線程將得到什麼結果呢?是寫操做以前舊的值?仍是寫操做成功以後的新值?或是一半新一半舊的值?或者,若是是兩個線程同時寫同一個內存,在操做完成後將會是什麼結果呢?是第一個線程寫入的值?仍是第二個線程寫入的值?仍是兩個線程寫入的一個混合值?所以如沒有合適的預防措施,任何結果都是可能的。並且這種行爲的發生甚至不能預測,因此結果也是不肯定性的。面試
Java是最早支持多線程的開發的語言之一,Java從一開始就支持了多線程能力,所以Java開發者能常遇到上面描述的問題場景。這也是我想爲Java併發技術而寫這篇系列的緣由。做爲對本身的筆記,和對其餘Java開發的追隨者均可獲益的。算法
該系列主要關注Java多線程,但有些在多線程中出現的問題會和多任務以及分佈式系統中出現的存在相似,所以該系列會將多任務和分佈式系統方面做爲參考,因此叫法上稱爲「併發性」,而不是「多線程」。數據庫
儘管面臨不少挑戰,多線程有一些優勢使得它一直被使用。這些優勢是:編程
想象一下,一個應用程序須要從本地文件系統中讀取和處理文件的情景。比方說,從磁盤讀取一個文件須要5秒,處理一個文件須要2秒。處理兩個文件則須要:
1 |
5 秒讀取文件A |
2 |
2 秒處理文件A |
3 |
5 秒讀取文件B |
4 |
2 秒處理文件B |
5 |
--------------------- |
6 |
總共須要 14 秒 |
從磁盤中讀取文件的時候,大部分的CPU時間用於等待磁盤去讀取數據。在這段時間裏,CPU很是的空閒。它能夠作一些別的事情。經過改變操做的順序,就可以更好的使用CPU資源。看下面的順序:
1 |
5 秒讀取文件A |
2 |
5 秒讀取文件B + 2 秒處理文件A |
3 |
2 秒處理文件B |
4 |
--------------------- |
5 |
總共須要 12 秒 |
CPU等待第一個文件被讀取完。而後開始讀取第二個文件。當第二文件在被讀取的時候,CPU會去處理第一個文件。記住,在等待磁盤讀取文件的時候,CPU大部分時間是空閒的。
總的說來,CPU可以在等待IO的時候作一些其餘的事情。這個不必定就是磁盤IO。它也能夠是網絡的IO,或者用戶輸入。一般狀況下,網絡和磁盤的IO比CPU和內存的IO慢的多。
在單線程應用程序中,若是你想編寫程序手動處理上面所提到的讀取和處理的順序,你必須記錄每一個文件讀取和處理的狀態。相反,你能夠啓動兩個線程,每一個線程處理一個文件的讀取和操做。線程會在等待磁盤讀取文件的過程當中被阻塞。在等待的時候,其餘的線程可以使用CPU去處理已經讀取完的文件。其結果就是,磁盤老是在繁忙地讀取不一樣的文件到內存中。這會帶來磁盤和CPU利用率的提高。並且每一個線程只須要記錄一個文件,所以這種方式也很容易編程實現。
服務器的流程以下所述:
1 |
while (server is active){ |
2 |
listen for request |
3 |
process request |
4 |
} |
若是一個請求須要佔用大量的時間來處理,在這段時間內新的客戶端就沒法發送請求給服務端。只有服務器在監聽的時候,請求才能被接收。另外一種設計是,監聽線程把請求傳遞給工做者線程(worker thread),而後馬上返回去監聽。而工做者線程則可以處理這個請求併發送一個回覆給客戶端。這種設計以下所述:
1 |
while (server is active){ |
2 |
listen for request |
3 |
hand request to worker thread |
4 |
} |
這種方式,服務端線程迅速地返回去監聽。所以,更多的客戶端可以發送請求給服務端。這個服務也變得響應更快。
桌面應用也是一樣如此。若是你點擊一個按鈕開始運行一個耗時的任務,這個線程既要執行任務又要更新窗口和按鈕,那麼在任務執行的過程當中,這個應用程序看起來好像沒有反應同樣。相反,任務能夠傳遞給工做者線程(word thread)。當工做者線程在繁忙地處理任務的時候,窗口線程能夠自由地響應其餘用戶的請求。當工做者線程完成任務的時候,它發送信號給窗口線程。窗口線程即可以更新應用程序窗口,並顯示任務的結果。對用戶而言,這種具備工做者線程設計的程序顯得響應速度更快。
從一個單線程的應用到一個多線程的應用並不只僅帶來好處,它也會有一些代價。不要僅僅爲了使用多線程而使用多線程。而應該明確在使用多線程時能多來的好處比所付出的代價大的時候,才使用多線程。若是存在疑問,應該嘗試測量一下應用程序的性能和響應能力,而不僅是猜想。
雖然有一些多線程應用程序比單線程的應用程序要簡單,但其餘的通常都更復雜。在多線程訪問共享數據的時候,這部分代碼須要特別的注意。線程之間的交互每每很是複雜。不正確的線程同步產生的錯誤很是難以被發現,而且重現以修復。
當CPU從執行一個線程切換到執行另一個線程的時候,它須要先存儲當前線程的本地的數據,程序指針等,而後載入另外一個線程的本地數據,程序指針等,最後纔開始執行。這種切換稱爲「上下文切換」(「context switch」)。CPU會在一個上下文中執行一個線程,而後切換到另一個上下文中執行另一個線程。
上下文切換並不廉價。若是沒有必要,應該減小上下文切換的發生。
你能夠經過維基百科閱讀更多的關於上下文切換相關的內容:
http://en.wikipedia.org/wiki/Context_switch
線程在運行的時候須要從計算機裏面獲得一些資源。除了CPU,線程還須要一些內存來維持它本地的堆棧。它也須要佔用操做系統中一些資源來管理線程。咱們能夠嘗試編寫一個程序,讓它建立100個線程,這些線程什麼事情都不作,只是在等待,而後看看這個程序在運行的時候佔用了多少內存。
在同一程序中運行多個線程自己不會致使問題,問題在於多個線程訪問了相同的資源。如,同一內存區(變量,數組,或對象)、系統(數據庫,web services等)或文件。實際上,這些問題只有在一或多個線程向這些資源作了寫操做時纔有可能發生,只要資源沒有發生變化,多個線程讀取相同的資源就是安全的。
多線程同時執行下面的代碼可能會出錯:
1 |
public class Counter { |
2 |
protected long count = 0 ; |
3 |
public void add( long value){ |
4 |
this .count = this .count + value; |
5 |
} |
6 |
} |
想象下線程A和B同時執行同一個Counter對象的add()方法,咱們沒法知道操做系統什麼時候會在兩個線程之間切換。JVM並非將這段代碼視爲單條指令來執行的,而是按照下面的順序:
觀察線程A和B交錯執行會發生什麼:
兩個線程分別加了2和3到count變量上,兩個線程執行結束後count變量的值應該等於5。然而因爲兩個線程是交叉執行的,兩個線程從內存中讀出的初始值都是0。而後各自加了2和3,並分別寫回內存。最終的值並非指望的5,而是最後寫回內存的那個線程的值,上面例子中最後寫回內存的是線程A,但實際中也多是線程B。若是沒有采用合適的同步機制,線程間的交叉執行狀況就沒法預料。
競態條件 & 臨界區
當兩個線程競爭同一資源時,若是對資源的訪問順序敏感,就稱存在競態條件。致使競態條件發生的代碼區稱做臨界區。上例中add()方法就是一個臨界區,它會產生競態條件。在臨界區中使用適當的同步就能夠避免競態條件。
容許被多個線程同時執行的代碼稱做線程安全的代碼。線程安全的代碼不包含競態條件。當多個線程同時更新共享資源時會引起競態條件。所以,瞭解Java線程執行時共享了什麼資源很重要。
局部變量
局部變量存儲在線程本身的棧中。也就是說,局部變量永遠也不會被多個線程共享。因此,基礎類型的局部變量是線程安全的。下面是基礎類型的局部變量的一個例子:
1 |
public void someMethod(){ |
2 |
|
3 |
long threadSafeInt = 0 ; |
4 |
5 |
threadSafeInt++; |
6 |
} |
局部的對象引用
對象的局部引用和基礎類型的局部變量不太同樣。儘管引用自己沒有被共享,但引用所指的對象並無存儲在線程的棧內。全部的對象都存在共享堆中。若是在某個方法中建立的對象不會逃逸出(譯者注:即該對象不會被其它方法得到,也不會被非局部變量引用到)該方法,那麼它就是線程安全的。實際上,哪怕將這個對象做爲參數傳給其它方法,只要別的線程獲取不到這個對象,那它還是線程安全的。下面是一個線程安全的局部引用樣例:
01 |
public void someMethod(){ |
02 |
|
03 |
LocalObject localObject = new LocalObject(); |
04 |
05 |
localObject.callMethod(); |
06 |
method2(localObject); |
07 |
} |
08 |
09 |
public void method2(LocalObject localObject){ |
10 |
localObject.setValue( "value" ); |
11 |
} |
樣例中LocalObject對象沒有被方法返回,也沒有被傳遞給someMethod()方法外的對象。每一個執行someMethod()的線程都會建立本身的LocalObject對象,並賦值給localObject引用。所以,這裏的LocalObject是線程安全的。事實上,整個someMethod()都是線程安全的。即便將LocalObject做爲參數傳給同一個類的其它方法或其它類的方法時,它仍然是線程安全的。固然,若是LocalObject經過某些方法被傳給了別的線程,那它就再也不是線程安全的了。
對象成員
對象成員存儲在堆上。若是兩個線程同時更新同一個對象的同一個成員,那這個代碼就不是線程安全的。下面是一個樣例:
1 |
public class NotThreadSafe{ |
2 |
StringBuilder builder = new StringBuilder(); |
3 |
|
4 |
public add(String text){ |
5 |
this .builder.append(text); |
6 |
} |
7 |
} |
若是兩個線程同時調用同一個NotThreadSafe
實例上的add()方法,就會有競態條件問題。例如:
01 |
NotThreadSafe sharedInstance = new NotThreadSafe(); |
02 |
03 |
new Thread( new MyRunnable(sharedInstance)).start(); |
04 |
new Thread( new MyRunnable(sharedInstance)).start(); |
05 |
06 |
public class MyRunnable implements Runnable{ |
07 |
NotThreadSafe instance = null ; |
08 |
|
09 |
public MyRunnable(NotThreadSafe instance){ |
10 |
this .instance = instance; |
11 |
} |
12 |
13 |
public void run(){ |
14 |
this .instance.add( "some text" ); |
15 |
} |
16 |
} |
注意兩個MyRunnable共享了同一個NotThreadSafe對象。所以,當它們調用add()方法時會形成競態條件。
固然,若是這兩個線程在不一樣的NotThreadSafe實例上調用call()方法,就不會致使競態條件。下面是稍微修改後的例子:
1 |
new Thread( new MyRunnable( new NotThreadSafe())).start(); |
2 |
new Thread( new MyRunnable( new NotThreadSafe())).start(); |
如今兩個線程都有本身單獨的NotThreadSafe對象,調用add()方法時就會互不干擾,不再會有競態條件問題了。因此非線程安全的對象仍能夠經過某種方式來消除競態條件。
線程控制逃逸規則
線程控制逃逸規則能夠幫助你判斷代碼中對某些資源的訪問是不是線程安全的。
資源能夠是對象,數組,文件,數據庫鏈接,套接字等等。Java中你無需主動銷燬對象,因此「銷燬」指再也不有引用指向對象。
即便對象自己線程安全,但若是該對象中包含其餘資源(文件,數據庫鏈接),整個應用也許就再也不是線程安全的了。好比2個線程都建立了各自的數據庫鏈接,每一個鏈接自身是線程安全的,但它們所鏈接到的同一個數據庫也許不是線程安全的。好比,2個線程執行以下代碼:
若是兩個線程同時執行,並且碰巧檢查的是同一個記錄,那麼兩個線程最終可能都插入了記錄:
一樣的問題也會發生在文件或其餘共享資源上。所以,區分某個線程控制的對象是資源自己,仍是僅僅到某個資源的引用很重要。
當多個線程同時訪問同一個資源,而且其中的一個或者多個線程對這個資源進行了寫操做,纔會產生競態條件。多個線程同時讀同一個資源不會產生競態條件。
咱們能夠經過建立不可變的共享對象來保證對象在線程間共享時不會被修改,從而實現線程安全。以下示例:
01 |
public class ImmutableValue{ |
02 |
private int value = 0 ; |
03 |
04 |
public ImmutableValue( int value){ |
05 |
this .value = value; |
06 |
} |
07 |
08 |
public int getValue(){ |
09 |
return this .value; |
10 |
} |
11 |
} |
請注意ImmutableValue類的成員變量value
是經過構造函數賦值的,而且在類中沒有set方法。這意味着一旦ImmutableValue實例被建立,value
變量就不能再被修改,這就是不可變性。但你能夠經過getValue()方法讀取這個變量的值。
(譯者注:注意,「不變」(Immutable)和「只讀」(Read Only)是不一樣的。當一個變量是「只讀」時,變量的值不能直接改變,可是能夠在其它變量發生改變的時候發生改變。好比,一我的的出生年月日是「不變」屬性,而一我的的年齡即是「只讀」屬性,可是不是「不變」屬性。隨着時間的變化,一我的的年齡會隨之發生變化,而一我的的出生年月日則不會變化。這就是「不變」和「只讀」的區別。(摘自《Java與模式》第34章))
若是你須要對ImmutableValue類的實例進行操做,能夠經過獲得value變量後建立一個新的實例來實現,下面是一個對value變量進行加法操做的示例:
01 |
public class ImmutableValue{ |
02 |
private int value = 0 ; |
03 |
04 |
public ImmutableValue( int value){ |
05 |
this .value = value; |
06 |
} |
07 |
08 |
public int getValue(){ |
09 |
return this .value; |
10 |
} |
11 |
12 |
public ImmutableValue add( int valueToAdd){ |
13 |
return new ImmutableValue( this .value + valueToAdd); |
14 |
} |
15 |
} |
請注意add()方法以加法操做的結果做爲一個新的ImmutableValue類實例返回,而不是直接對它本身的value變量進行操做。
引用不是線程安全的!
重要的是要記住,即便一個對象是線程安全的不可變對象,指向這個對象的引用也可能不是線程安全的。看這個例子:
01 |
public void Calculator{ |
02 |
private ImmutableValue currentValue = null ; |
03 |
04 |
public ImmutableValue getValue(){ |
05 |
return currentValue; |
06 |
} |
07 |
08 |
public void setValue(ImmutableValue newValue){ |
09 |
this .currentValue = newValue; |
10 |
} |
11 |
12 |
public void add( int newValue){ |
13 |
this .currentValue = this .currentValue.add(newValue); |
14 |
} |
15 |
} |
Calculator類持有一個指向ImmutableValue實例的引用。注意,經過setValue()方法和add()方法可能會改變這個引用。所以,即便Calculator類內部使用了一個不可變對象,但Calculator類自己仍是可變的,所以Calculator類不是線程安全的。換句話說:ImmutableValue類是線程安全的,但使用它的類不是。當嘗試經過不可變性去得到線程安全時,這點是須要牢記的。
要使Calculator類實現線程安全,將getValue()、setValue()和add()方法都聲明爲同步方法便可。
線程是什麼?
線程(Thread)是一個對象(Object)。用來幹什麼?Java 線程(也稱 JVM 線程)是 Java 進程內容許多個同時進行的任務。該進程內併發的任務成爲線程(Thread),一個進程裏至少一個線程。
Java 程序採用多線程方式來支持大量的併發請求處理,程序若是在多線程方式執行下,其複雜度遠高於單線程串行執行。那麼多線程:指的是這個程序(一個進程)運行時產生了不止一個線程。
爲啥使用多線程?
聊到多線程,多半會聊併發與並行,咋理解並區分這兩個的區別呢?
Java 建立線程對象有兩種方法:
若是一個類繼承Thread,則不適合資源共享。可是若是實現了Runable接口的話,則很容易的實現資源共享。
實現Runnable接口比繼承Thread類所具備的優點:
1):適合多個相同的程序代碼的線程去處理同一個資源
2):能夠避免java中的單繼承的限制
3):增長程序的健壯性,代碼能夠被多個線程共享,代碼和數據獨立
直接看代碼:
一、繼承Thread的demo
提醒一下你們:main方法其實也是一個線程。在java中全部的線程都是同時啓動的,至於何時,哪一個先執行,徹底看誰先獲得CPU的資源。
在java中,每次程序運行至少啓動2個線程。一個是main線程,一個是垃圾收集線程。由於每當使用java命令執行一個類的時候,實際上都會啓動一個jvm,每個jvm實際上就是在操做系統中啓動了一個進程。
新建 MyThread 對象,代碼以下:
/** * 繼承 Thread 類建立線程對象 * @author Jeff Lee @ bysocket.com * @since 2018年01月27日21:03:02 */ public class MyThread extends Thread { @Override // 能夠省略 public void run() { System.out.println("MyThread 的線程對象正在執行任務"); } public static void main(String[] args) { for (int i = 0; i < 10; i++) { MyThread thread = new MyThread(); thread.start(); System.out.println("MyThread 的線程對象 " + thread.getId()); } } }
MyThread 類繼承了 Thread 對象,並重寫(Override)了 run 方法,實現線程裏面的邏輯。main 函數是使用 for 語句,循環建立了 10 個線程,調用 start 方法啓動線程,最後打印當前線程對象的 ID。
run 方法和 start 方法的區別是什麼呢?
run 方法就是跑的意思,線程啓動後,會調用 run 方法。
start 方法就是啓動的意思,就是啓動新線程實例。啓動線程後,纔會調線程的 run 方法。
執行 main 方法後,控制檯打印以下:
可見,線程的 ID 是線程惟一標識符,每一個線程 ID 都是不同的。
start 方法和 run 方法的關係如圖所示:
轉存失敗從新上傳取消
同理,實現 Runnable 接口類建立線程對象也很簡單,只是不一樣的形式。新建 MyThreadBrother 代碼以下:
/** * 實現 Runnable 接口類建立線程對象 * @author Jeff Lee @ bysocket.com * @since 2018年01月27日21:22:57 */ public class MyThreadBrother implements Runnable { @Override // 能夠省略 public void run() { System.out.println("MyThreadBrother 的線程對象正在執行任務"); } public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Thread(new MyThreadBrother()); thread.start(); System.out.println("MyThreadBrother 的線程對象 " + thread.getId()); } } }
具體代碼:「java-concurrency-core-learning」
https://github.com/JeffLi1993/java-concurrency-core-learning
在運行上面兩個小 demo 後,JVM 執行了 main 函數線程,而後在主線程中執行建立了新的線程。正常狀況下,全部線程執行到運行結束爲止。除非某個線程中調用了 System.exit(1) 則被終止。
在實際開發中,一個請求到響應式是一個線程。但在這個線程中可使用線程池建立新的線程,去執行任務。
新建 MyThreadInfo 類,打印線程對象屬性,代碼以下:
/** * 線程實例對象的屬性值 * @author Jeff Lee @ bysocket.com * @since 2018年01月27日21:24:40 */ public class MyThreadInfo extends Thread { @Override // 能夠省略 public void run() { System.out.println("MyThreadInfo 的線程實例正在執行任務"); // System.exit(1); } public static void main(String[] args) { MyThreadInfo thread = new MyThreadInfo(); thread.start(); System.out.print("MyThreadInfo 的線程對象 \n" + "線程惟一標識符:" + thread.getId() + "\n" + "線程名稱:" + thread.getName() + "\n" + "線程狀態:" + thread.getState() + "\n" + "線程優先級:" + thread.getPriority()); } }
執行代碼打印以下:
線程是一個對象,它有惟一標識符 ID、名稱、狀態、優先級等屬性。線程只能修改其優先級和名稱等屬性 ,沒法修改 ID 、狀態。ID 是 JVM 分配的,名字默認也爲 Thread-XX,XX是一組數字。線程初始狀態爲 NEW。
線程優先級的範圍是 1 到 10 ,其中 1 是最低優先級,10 是最高優先級。不推薦改變線程的優先級,若是業務須要,天然能夠修改線程優先級到最高,或者最低。
線程的狀態實現經過 Thread.State 常量類實現,有 6 種線程狀態:new(新建)、runnnable(可運行)、blocked(阻塞)、waiting(等待)、time waiting (定時等待)和 terminated(終止)。狀態轉換圖以下:
線程狀態流程大體以下:
本文介紹了線程與多線程的基礎篇,包括了線程啓動及線程狀態等。下一篇咱們聊下線程的具體操做。包括中斷、終止等
1.1 什麼是線程中斷?
線程中斷是線程的標誌位屬性。而不是真正終止線程,和線程的狀態無關。線程中斷過程表示一個運行中的線程,經過其餘線程調用了該線程的 interrupt()
方法,使得該線程中斷標誌位屬性改變。
深刻思考下,線程中斷不是去中斷了線程,偏偏是用來通知該線程應該被中斷了。具體是一個標誌位屬性,到底該線程生命週期是去終止,仍是繼續運行,由線程根據標誌位屬性自行處理。
1.2 線程中斷操做
調用線程的 interrupt()
方法,根據線程不一樣的狀態會有不一樣的結果。
下面新建 InterruptedThread 對象,代碼以下:
/** * 一直運行的線程,中斷狀態爲 true * * @author Jeff Lee @ bysocket.com * @since 2018年02月23日19:03:02 */ public class InterruptedThread implements Runnable { @Override // 能夠省略 public void run() { // 一直 run while (true) { } } public static void main(String[] args) throws Exception { Thread interruptedThread = new Thread(new InterruptedThread(), "InterruptedThread"); interruptedThread.start(); TimeUnit.SECONDS.sleep(2); interruptedThread.interrupt(); System.out.println("InterruptedThread interrupted is " + interruptedThread.isInterrupted()); TimeUnit.SECONDS.sleep(2); } }
運行 main 函數,結果以下:
代碼詳解:
interrupt()
方法,中斷狀態置爲 true,但不會影響線程的繼續運行另外一種狀況,新建 InterruptedException 對象,代碼以下:
/** * 拋出 InterruptedException 的線程,中斷狀態被重置爲默認狀態 false * * @author Jeff Lee @ bysocket.com * @since 2018年02月23日19:03:02 */ public class InterruptedException implements Runnable { @Override // 能夠省略 public void run() { // 一直 sleep try { TimeUnit.SECONDS.sleep(10); } catch (java.lang.InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) throws Exception { Thread interruptedThread = new Thread(new InterruptedException(), "InterruptedThread"); interruptedThread.start(); TimeUnit.SECONDS.sleep(2); // 中斷被阻塞狀態(sleep、wait、join 等狀態)的線程,會拋出異常 InterruptedException // 在拋出異常 InterruptedException 前,JVM 會先將中斷狀態重置爲默認狀態 false interruptedThread.interrupt(); System.out.println("InterruptedThread interrupted is " + interruptedThread.isInterrupted()); TimeUnit.SECONDS.sleep(2); } }
運行 main 函數,結果以下:
代碼詳解:
小結下線程中斷:
代碼:https://github.com/JeffLi1993/java-concurrency-core-learning
好比在 IDEA 中強制關閉程序,當即中止程序,不給程序釋放資源等操做,確定是不正確的。線程終止也存在相似的問題,因此須要考慮如何終止線程?
上面聊到了線程中斷,能夠利用線程中斷標誌位屬性來安全終止線程。同理也可使用 boolean 變量來控制是否須要終止線程。
新建 ,代碼以下:
/** * 安全終止線程 * * @author Jeff Lee @ bysocket.com * @since 2018年02月23日19:03:02 */ public class ThreadSafeStop { public static void main(String[] args) throws Exception { Runner one = new Runner(); Thread countThread = new Thread(one, "CountThread"); countThread.start(); // 睡眠 1 秒,通知 CountThread 中斷,並終止線程 TimeUnit.SECONDS.sleep(1); countThread.interrupt(); Runner two = new Runner(); countThread = new Thread(two,"CountThread"); countThread.start(); // 睡眠 1 秒,而後設置線程中止狀態,並終止線程 TimeUnit.SECONDS.sleep(1); two.stopSafely(); } private static class Runner implements Runnable { private long i; // 終止狀態 private volatile boolean on = true; @Override public void run() { while (on && !Thread.currentThread().isInterrupted()) { // 線程執行具體邏輯 i++; } System.out.println("Count i = " + i); } public void stopSafely() { on = false; } } }
從上面代碼能夠看出,經過 while (on && !Thread.currentThread().isInterrupted())
代碼來實現線程是否跳出執行邏輯,並終止。可是疑問點就來了,爲啥須要 on
和 isInterrupted()
兩項一塊兒呢?用其中一個方式不就好了嗎?答案在下面
on
經過 volatile 關鍵字修飾,達到線程之間可見,從而實現線程的終止。但當線程狀態爲被阻塞狀態(sleep、wait、join 等狀態)時,對成員變量操做也阻塞,進而沒法執行安全終止線程isInterrupted();
只去解決阻塞狀態下的線程安全終止。不少好友介紹,若是用 Spring 棧開發到使用線程或者線程池,那麼儘可能使用框架這塊提供的線程操做及框架提供的終止等
原文出處http://cmsblogs.com/ 『chenssy』
ThreadLocal是啥?之前面試別人時就喜歡問這個,有些夥伴喜歡把它和線程同步機制混爲一談,事實上ThreadLocal與線程同步無關。ThreadLocal雖然提供了一種解決多線程環境下成員變量的問題,可是它並非解決多線程共享變量的問題。那麼ThreadLocal究竟是什麼呢?
API是這樣介紹它的:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).
該類提供了線程局部 (thread-local) 變量。這些變量不一樣於它們的普通對應物,由於訪問某個變量(經過其
get
或set
方法)的每一個線程都有本身的局部變量,它獨立於變量的初始化副本。ThreadLocal
實例一般是類中的 private static 字段,它們但願將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。
因此ThreadLocal與線程同步機制不一樣,線程同步機制是多個線程共享同一個變量,而ThreadLocal是爲每個線程建立一個單獨的變量副本,故而每一個線程均可以獨立地改變本身所擁有的變量副本,而不會影響其餘線程所對應的副本。能夠說ThreadLocal爲多線程環境下變量問題提供了另一種解決思路。
ThreadLocal定義了四個方法:
除了這四個方法,ThreadLocal內部還有一個靜態內部類ThreadLocalMap,該內部類纔是實現線程隔離機制的關鍵,get()、set()、remove()都是基於該內部類操做。ThreadLocalMap提供了一種用鍵值對方式存儲每個線程的變量副本的方法,key爲當前ThreadLocal對象,value則是對應線程的變量副本。
對於ThreadLocal須要注意的有兩點:
1. ThreadLocal實例自己是不存儲值,它只是提供了一個在當前線程中找到副本值得key。
2. 是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小夥伴會弄錯他們的關係。
下圖是Thread、ThreadLocal、ThreadLocalMap的關係(http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/)
示例以下:
運行結果:
從運行結果能夠看出,ThreadLocal確實是能夠達到線程隔離機制,確保變量的安全性。這裏咱們想一個問題,在上面的代碼中ThreadLocal的initialValue()方法返回的是0,加入該方法返回得是一個對象呢,會產生什麼後果呢?例如:
具體過程請參考:對ThreadLocal實現原理的一點思考
ThreadLocal雖然解決了這個多線程變量的複雜問題,可是它的源碼實現倒是比較簡單的。ThreadLocalMap是實現ThreadLocal的關鍵,咱們先從它入手。
ThreadLocalMap其內部利用Entry來實現key-value的存儲,以下:
從上面代碼中能夠看出Entry的key就是ThreadLocal,而value就是值。同時,Entry也繼承WeakReference,因此說Entry所對應key(ThreadLocal實例)的引用爲一個弱引用(關於弱引用這裏就很少說了,感興趣的能夠關注這篇博客:Java 理論與實踐: 用弱引用堵住內存泄漏)
ThreadLocalMap的源碼稍微多了點,咱們就看兩個最核心的方法getEntry()、set(ThreadLocal> key, Object value)方法。
**set(ThreadLocal> key, Object value)**
這個set()操做和咱們在集合瞭解的put()方式有點兒不同,雖然他們都是key-value結構,不一樣在於他們解決散列衝突的方式不一樣。集合Map的put()採用的是拉鍊法,而ThreadLocalMap的set()則是採用開放定址法(具體請參考散列衝突處理系列博客)。掌握了開放地址法該方法就一目瞭然了。
set()操做除了存儲元素外,還有一個很重要的做用,就是replaceStaleEntry()和cleanSomeSlots(),這兩個方法能夠清除掉key == null 的實例,防止內存泄漏。在set()方法中還有一個變量很重要:threadLocalHashCode,定義以下:
從名字上面咱們能夠看出threadLocalHashCode應該是ThreadLocal的散列值,定義爲final,表示ThreadLocal一旦建立其散列值就已經肯定了,生成過程則是調用nextHashCode():
nextHashCode表示分配下一個ThreadLocal實例的threadLocalHashCode的值,HASH_INCREMENT則表示分配兩個ThradLocal實例的threadLocalHashCode的增量,從nextHashCode就能夠看出他們的定義。
getEntry()
因爲採用了開放定址法,因此當前key的散列值和元素在數組的索引並非徹底對應的,首先取一個探測數(key的散列值),若是所對應的key就是咱們所要找的元素,則返回,不然調用getEntryAfterMiss(),以下:
這裏有一個重要的地方,當key == null時,調用了expungeStaleEntry()方法,該方法用於處理key == null,有利於GC回收,可以有效地避免內存泄漏。
返回當前線程所對應的線程變量
首先經過當前線程獲取所對應的成員變量ThreadLocalMap,而後經過ThreadLocalMap獲取當前ThreadLocal的Entry,最後經過所獲取的Entry獲取目標值result。
getMap()方法能夠獲取當前線程所對應的ThreadLocalMap,以下:
設置當前線程的線程局部變量的值。
獲取當前線程所對應的ThreadLocalMap,若是不爲空,則調用ThreadLocalMap的set()方法,key就是當前ThreadLocal,若是不存在,則調用createMap()方法新建一個,以下:
返回該線程局部變量的初始值。
該方法定義爲protected級別且返回爲null,很明顯是要子類實現它的,因此咱們在使用ThreadLocal的時候通常都應該覆蓋該方法。該方法不能顯示調用,只有在第一次調用get()或者set()方法時纔會被執行,而且僅執行1次。
將當前線程局部變量的值刪除。
該方法的目的是減小內存的佔用。固然,咱們不須要顯示調用該方法,由於一個線程結束後,它所對應的局部變量就會被垃圾回收。
前面提到每一個Thread都有一個ThreadLocal.ThreadLocalMap的map,該map的key爲ThreadLocal實例,它爲一個弱引用,咱們知道弱引用有利於GC回收。當ThreadLocal的key == null時,GC就會回收這部分空間,可是value卻不必定可以被回收,由於他還與Current Thread存在一個強引用關係,以下(圖片來自http://www.jianshu.com/p/ee8c9dccc953):
因爲存在這個強引用關係,會致使value沒法回收。若是這個線程對象不會銷燬那麼這個強引用關係則會一直存在,就會出現內存泄漏狀況。因此說只要這個線程對象可以及時被GC回收,就不會出現內存泄漏。若是碰到線程池,那就更坑了。
那麼要怎麼避免這個問題呢?
在前面提過,在ThreadLocalMap中的setEntry()、getEntry(),若是遇到key == null的狀況,會對value設置爲null。固然咱們也能夠顯示調用ThreadLocal的remove()方法進行處理。
下面再對ThreadLocal進行簡單的總結:
- ThreadLocal 不是用於解決共享變量的問題的,也不是爲了協調線程同步而存在,而是爲了方便每一個線程處理本身的狀態而引入的一個機制。這點相當重要。
- 每一個Thread內部都有一個ThreadLocal.ThreadLocalMap類型的成員變量,該成員變量用來存儲實際的ThreadLocal變量副本。
- ThreadLocal並非爲線程保存對象的副本,它僅僅只起到一個索引的做用。它的主要木得視爲每個線程隔離一個類的實例,這個實例的做用範圍僅限於線程內部。
有關於JMM內存模型的詳細介紹將在下一章講述。
本文轉載自併發編程網 – ifeve.com
更多內容請關注微信公衆號【Java技術江湖】
一位阿里 Java 工程師的技術小站。做者黃小斜,專一 Java 相關技術:SSM、SpringBoot、MySQL、分佈式、中間件、集羣、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!(關注公衆號後回覆」Java「便可領取 Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分佈式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送做者原創的Java學習指南、Java程序員面試指南等乾貨資源)