想了想還是從基礎開始整理併發編程的知識吧,實在是太多了!!!
併發編程基礎
-
基礎概念:
-
線程與進程
- 進程:進程是系統進行
資源分配和調度
的基本單位,平時我們在電腦上啓動的一個程序就是一個進程。 - 線程:線程是
操作系統
進行調度
的最小單位 - 關係:一個進程可以啓動一個或多個線程,進程中所有的線程會
共享
進程中的內存空間
,每個線程都有自己的程序計數器
和棧區域
,進程中的棧資源用來存儲該線程的局部變量
,這些局部變量是線程私有
的 - 線程與進程的關係如圖所示:
- 進程:進程是系統進行
-
線程的創建
-
Java中有三種線程創建方式,分別是實現
Rannable接口
的run方法、繼承Thread類
重寫run方法、實現Callable接口
-
繼承Thread
繼承Thread類的實現代碼如下:/** * @author admin */ public class ThreadTest { //繼承Thread類並重寫run方法 public static class MyThread extends Thread{ @Override public void run(){ System.out.println("a thread"); } } public static void main(String[] args) { //創建線程 MyThread myThread = new MyThread(); //啓動線程 myThread.start(); } }
-
在以上代碼中MyThread繼承Thread類,並重寫了run方法。在main函數中創建MyThread實例,然後調用MyThread的
start()
方法啓動該線程。但是我們要知道在創建MyThread
對象後線程並沒有立即啓動執行,需要調用start方法後才啓動線程。 -
調用
start
方法之後線程並沒有馬上執行而是線程處於就緒狀態
,等待獲取到CPU資源之後才真正處於運行狀態
。 -
run方法執行完畢,線程就處於
終止狀態
。 -
使用繼承Thread類的好處是,在
run
方法內獲取線程時直接調用this
就可以了,無需通過Thread.currentThread()
方法;不好的地方是Java不允許多繼承
,如果繼承了Thread類就無法繼承其它類。並且任務與代碼沒有分離,當多個線程執行一樣的任務時需要多份任務代碼。
-
-
實現Rannable
實現Runnable接口代碼如下:
public static class RunnableTask implements Runnable{ @Override public void run() { System.out.println("a runnable thread"); } } public static void main(String[] args) { RunnableTask runnableTask = new RunnableTask(); new Thread(runnableTask).start(); new Thread(runnableTask).start(); }
- 在以上代碼中,兩個線程公用了一個runnableTask代碼邏輯,並且也可以根據需要給RunnableTask
添加參數
進行區分(重載
)。另外RunnableTask可以繼承其它類。但是繼承Thread類和實現Runnable接口的方式都是沒有返回值
的。
- 在以上代碼中,兩個線程公用了一個runnableTask代碼邏輯,並且也可以根據需要給RunnableTask
-
實現Callable接口
實現Callable接口方式://創建任務類 public static class CallTask implements Callable<String>{ @Override public String call() throws Exception { return "hello callAble"; } } public static void main(String[] args) throws InterruptedException { //創建異步任務 FutureTask<String> futureTask = new FutureTask<>(new CallTask()); //啓動線程 new Thread(futureTask).start(); try { //等待任務執行結束並返回結果 String result = futureTask.get(); System.out.println(result); }catch (ExecutionException e){ e.printStackTrace(); } }
- 在以上代碼中實現
Callable
接口的call()
方法,在main函數中創建一個FutureTask
對象(構造函數爲CallTask的實例),然後使用創建的FutureTask對象作爲任務創建一個線程並啓動它,最後通過futureTask.get()
等待任務執行完畢並返回結果。
- 在以上代碼中實現
-
總結:
- 使用繼承Thread類的好處是方便傳參數,可以在子類中添加成員變量,通過set方法設置參數或者構造函數進行傳遞;如果使用實現Runnable接口的方式,則只能使用主線程裏面被聲明爲final的變量,但是前兩種都沒有返回結果,但是Callable接口可以實現獲取返回結果。
-
-
線程等待和通知
-
Object級別
-
wait
:當一個線程調用共享變量的wait()方法時,該調用線程會被阻塞掛起
。-
直到發生一下幾件事才返回:(1)其它線程調用該共享對象的notify()或者notifyAll()方法,(2)其它線程調用了該線程的interrupt()方法,該線程拋出InterruptedException異常返回。
-
另外,如果調用wait()方法的線程沒有
事先獲取對象的監視器鎖
,則調用wait()方法時調用線程會拋出IllegalMonitorStateException異常。一個線程獲取該共享變量的監視器鎖的方式有兩種:1、執行synchronized同步代碼塊,使用該共享變量作爲參數synchronized(共享變量){ //doSomething }
2、調用該共享變量的方法,並且該方法使用了synchronized修飾
synchronized void add(int a,int b){ //doSomething }
-
另外需要注意的是,一個線程可以從掛起狀態變爲可以運行狀態,即使該線程沒有被其它線程調用notify()、notifyAll()方法進行喚醒,或者被中斷等,這就是所謂的虛假喚醒。雖然虛假喚醒很少發生,但是要防患於未然,做法就是不停的測試該線程被喚醒的條件是否滿足,不滿足就繼續等待,也就是說在一個循環調用中調用wait()方法進行防範。退出的條件是滿足了喚醒該線程的條件。
synchronized(obj){ while(條件不滿足){ obj.wait(); } }
-
線程掛起的方法也存在一個wait(long timeout)的方法,如果一個線程調用共享對象的wait(long timeout)方法後,如果沒有在指定的timeout ms時間內被其它線程調用該共享變量的notify()和notifyAll()方法喚醒,那麼該方法會因爲超時而返回。如果將timeout設置爲0,則效果和wait()方法效果一樣。
-
-
notify
- 一個線程調用共享對象的notify()方法後,會喚醒一個在共享變量上調用wait系列方法後被掛起的線程。一個共享變量上可能有多個線程在等待,具體喚醒哪個線程是隨機的。
- 此外被喚醒的線程並不會馬上從wait方法返回並執行,它必須在
獲取了共享對象的監視器鎖後纔可以返回
,也就是喚醒它的線程釋放了共享變量的監視器鎖之後,被喚醒的線程也不一定會直接獲取到共享變量的監視器鎖,因爲該線程還需要和其它線程競爭該鎖(所以說synchronized鎖並不是公平的
),只有競爭到共享變量的監視器鎖(synchronized鎖)之後纔可以繼續執行。
-
notifyAll
-
不同於notify()會喚醒被阻塞到該共享變量的一個線程,notifytAll()會
喚醒所有
的該共享變量上調用wait
系列方法被掛起的線程。//創建共享變量 private static volatile Object object = new Object(); public static void main(String[] args) throws InterruptedException { //創建線程A Thread threadA = new Thread(new Runnable() { @Override public void run() { synchronized (object) { try { System.out.println("threadA start wait"); object.wait(); System.out.println("threadA wait end"); } catch (InterruptedException e) { e.printStackTrace(); } } } }); //創建線程B Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized (object) { try { System.out.println("threadB start wait"); object.wait(); System.out.println("threadB wait end"); } catch (InterruptedException e) { e.printStackTrace(); } } } }); //創建線程C Thread threadC = new Thread(new Runnable() { @Override public void run() { synchronized (object) { System.out.println("threadC start notifyAll"); object.notifyAll(); System.out.println("threadC notifyAll end"); } } }); threadA.start(); threadB.start(); Thread.sleep(1000); threadC.start(); //等待線程結束 threadA.join(); threadB.join(); threadC.join(); }
如上代碼中我們創建兩個線程並都執行wait()方法掛起線程,然後沉睡一秒之後執行線程C執行notifyAll(),這樣線程A和線程B被同時喚醒,因此線程A和線程B需要競爭共享變量object的監視器鎖,誰先獲取到監視器鎖誰就先執行。如上的執行結果中我們可以看到線程A先獲取到了監視器鎖然後執行了後續的操作。
-
-
-
Thread級別
-
sleep
:讓線程睡眠的方法
-
當一個執行中的線程執行了sleep()方法後,該線程會暫時
讓出CPU的執行權,不參與CPU的調度
,但是該線程仍然持有監視器鎖(也就是不會釋放鎖)
。當指定的睡眠時間到了之後該函數會正常返回,線程就處於就緒狀態
,重新參與CPU調度,獲取到CPU資源之後就可以繼續運行。如果在睡眠期間其它線程調用了睡眠中的線程的interrupt()方法中斷該線程,那麼該線程會在調用sleep()的地方拋出IntermptedException異常並返回。public class SleepTest { //創建一個獨佔鎖 private static final Lock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { //創建線程A Thread threadA = new Thread(new Runnable() { @Override public void run() { //獲取獨佔鎖 lock.lock(); try { System.out.println("threadA start sleep"); Thread.sleep(10000); System.out.println("threadA end sleep"); }catch (InterruptedException e){ e.printStackTrace(); }finally { //釋放鎖 lock.unlock(); } } }); //創建線程B Thread threadB = new Thread(new Runnable() { @Override public void run() { //獲取獨佔鎖 lock.lock(); try { System.out.println("threadB start sleep"); Thread.sleep(10000); System.out.println("threadB end sleep"); }catch (InterruptedException e){ e.printStackTrace(); }finally { //釋放鎖 lock.unlock(); } } }); //啓動線程 threadA.start(); threadB.start(); } }
-
以上代碼中創建兩個線程,threadA和threadB,並且兩個線程都休眠10秒,並且啓動兩個線程,這樣的情況不論執行多少次都會是threadA先執行,因爲threadA睡眠時並不會釋放鎖,依然持有獨佔鎖資源,所以最後睡眠結束時依然是先持有鎖的線程先執行。
-
另外如果在sleep(long millis)中millis參數傳遞了一個負數,則會拋出IllegalArgumentException異常。
-
-
join
:等待線程執行終止的方法
- 當多個線程加載資源時,需要等待所有的線程執行完畢之後再做彙總處理的情況時,就需要一個方法來控制這些線程的執行,控制所有線程執行完畢才釋放線程,此時就需要用到join()方法。
public static void main(String[] args) throws InterruptedException { Thread threadA = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("threadA over"); } }); Thread threadB = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("threadB over"); } }); //啓動子線程 threadA.start(); threadB.start(); //等待子線程執行完畢返回 threadA.join(); threadB.join(); }
- 在以上代碼中啓動了兩個線程,分別調用了join()方法,那麼當主線程執行到threadA.join()時會被阻塞,等待threadA執行完畢之後返回,因此主線程執行到join()方法時會被阻塞,等待threadA的線程執行成功後才能繼續執行。同理threadB也是如此,主線程執行到threadB時也會被阻塞,等待threadB執行成功後才返回。
- 同時threadA調用threadB的join()方法時也會被阻塞,當其它線程調用threadA的interrupt()方法中斷threadA時,threadA會拋出InterruptedException異常並返回。
join()方法阻塞的是當前線程
,比如主線程中執行threadA.join(),阻塞的是主線程。而不是threadA線程。
-
yield
:讓出CPU執行權的方法
-
當一個線程調用yield()方法時,就是表明當前線程請求
讓出自己的CPU使用權
,但是線程調度器可以忽視這個請求。 -
操作系統中線程的調度是按照時間片進行分配CPU執行權的,當一個線程使用完自己的時間片之後,線程調度器纔會進行下一輪的線程調度,而當一個線程調用了yield()方法時,是告訴線程調度器自己的時間片還沒用完但是不想用了,線程調度器可以進行下一輪線程調度了。
public class YieldTest implements Runnable { YieldTest(){ //創建並啓動線程 Thread thread = new Thread(this); thread.start(); } @Override public void run() { for (int i = 0; i < 5; i++) { //當i=0時讓出CPU執行權,放棄時間片,進行下一輪調度 if (i%5==0){ System.out.println(Thread.currentThread()+"yield"); //當前線程讓出CPU執行權,放棄時間片,進行下一輪調度 Thread.yield(); } } System.out.println(Thread.currentThread()+"is over"); } public static void main(String[] args) { new YieldTest(); new YieldTest(); new YieldTest(); } }
-
以上代碼中啓動了三個線程並且分別在i=0 時候調用了Thread.yield()方法,所以三個線程中的輸出語句並沒有連在一起,因爲輸出第一行後當前線程就讓出了CPU執行權,其它線程先使用CPU調用了其它方法。
-
-
總結:
sleep()與yield()的區別在於,當線程調用sleep()
方法時線程會被阻塞掛起到指定的時間
,這期間線程調度器並不會調度該線程
。而使用yield()
方法時,線程知識讓出自己的時間片,並沒有被阻塞掛起,而是處於就緒狀態
,在線程調度器進行下一次調度時仍然參與線程的競爭調度,並且有可能調度到該線程。
-
-
-
線程中斷
-
void interrupt()
:中斷線程,當線程A正在運行時,線程B可以調用線程A的interrupt()的方法來設置線程A的中斷標誌爲true並立即返回。設置標誌僅僅是設置標誌,線程A並沒有被中斷,它會繼續執行。但是若線程A調用了wait()、join()、yield()方法被阻塞掛起時,此時線程B調用線程A的interrupt()方法,線程A會在調用這些方法的地方拋出InterruptedException異常而返回。 -
boolean isInterrupted()
:檢測當前線程是否被中斷,如果是則返回true,否則返回false。 -
boolean interrupted()
:檢測當前線程是否被中斷,如果是則返回true否則返回false。與isInterrupted()不同的是該方法如果發現當前線程被中斷則會清除中斷標誌
,並且該方法是static
方法,可以直接通過Thread類調用,並且interrupted()內部是獲取當前調用線程
的中斷標誌,而不是調用interrupted()方法的實例對象的中斷標誌。public static boolean interrupted() { return currentThread().isInterrupted(true); }
例子如下:
public class InterruptedTest { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Runnable() { @Override public void run() { for (;;){ } } }); //啓動線程 thread.start(); //設置中斷標誌 thread.interrupt(); //獲取中斷標誌 System.out.println("isInterrupted:"+thread.isInterrupted()); //獲取中斷標誌並重置(此時獲取的是主線程的中斷標誌) System.out.println("isInterrupted:"+ Thread.interrupted()); //獲取中斷標誌 System.out.println("isInterrupted:"+ thread.isInterrupted()); thread.join(); } }
在第二個獲取中斷標誌並重置的地方,此時獲取的中斷標誌其實是主線程的中斷標誌,
-
-
死鎖
- 概念:死鎖是指兩個或兩個以上的線程在執行過程中,因爭奪資源而造成的互相等待的現象,在沒有外力的情況下,這些線程會相互等待而無法執行下去。
- 產生條件
-
互斥
:線程對已經獲取到的資源會排斥其它線程的使用,也就是該資源同時只能由一個線程佔用。 -
循環等待
:發生死鎖時,是由一個線程請求資源的環形的鏈造成的。也就是t0等待t1的資源,t1等待t2資源…tn等待t0的資源。 -
不可剝奪
:線程獲得資源之後就不可被其它線程搶佔,除非自己使用完成。 -
佔用且等待
:一個線程持有了一個資源但是又請求其它資源,而其它資源正在被其它線程佔用,因此當前線程就會阻塞。public class DeadLockTest { //創建資源 private static Object objectA = new Object(); private static Object objectB = new Object(); public static void main(String[] args) { //創建線程A Thread threadA = new Thread(new Runnable() { @Override public void run() { synchronized (objectA){ System.out.println(Thread.currentThread()+"get ObjectA"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread()+"wait get ObjectB"); synchronized (objectB){ System.out.println(Thread.currentThread()+"get ObjectB"); } } } }); //創建線程A Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized (objectB){ System.out.println(Thread.currentThread()+"get ObjectB"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread()+"wait get ObjectA"); synchronized (objectA){ System.out.println(Thread.currentThread()+"get ObjectA"); } } } }); threadA.start(); threadB.start(); } }
在以上代碼中線程A獲取到objectA資源,線程B獲取到了objectB資源。線程A休眠結束後會企圖獲取objectB資源,但是objectB資源正在被線程B持有,所以線程A會被阻塞而等待,而線程B休眠結束後企圖獲取objectA資源,objectA正在被線程A持有,所以線程A與線程B就陷入相互等待中,也就產生了死鎖。
-
- 避免死鎖
打破
至少一個造成死鎖的條件
,但是目前只有佔有和等待以及循環等待是可以打破的。- 造成死鎖的原因其實和
申請資源的順序
有關,使用資源申請的有序性原則就可以避免死鎖。
-
Deamon守護線程
-
Java中的線程分爲兩類,分別爲
deamon
線程和user
線程(用戶線程),JVM啓動時會調用main函數,main函數所在的線程就是守護線程,其實JVM內部還啓動了好多守護線程,比如垃圾回收線程
。 -
守護線程與用戶線程的區別爲當最後一個
用戶線程結束
時,JVM
就會正常退出
,而不管當前是否還有守護線程,也就是說守護線程不影響
JVM的退出。因此只要有一個用戶線程沒有結束,正常情況下JVM就不會退出。創建一個守護線程如下:
public class DaemonTest { public static void main(String[] args) { Thread daemonThread = new Thread(new Runnable() { @Override public void run() { } }); daemonThread.setDaemon(true); daemonThread.start(); } }
只需要設置線程的
daemon
參數爲true即可。 -
當用戶線程都執行結束之後,JVM會執行一個叫做
DestroyJava VM
的線程,該線程會等待所有用戶線程結束後終止JVM進程。 -
總結
:如果你希望在主線程結束後 JVM進程馬上結束,那麼在創建線程時可以將其設置爲守護線程,如果你希望在主線程結束之後子線程繼續工作,等子線程結束之後再結束JVM進程,那麼就將子線程設置爲用戶線程。
-
-
ThreadLocal
-
概念:多線程訪問同一個共享變量時容易出現併發問題,特別是多個線程需要對一個共享變量寫入時, 爲了保證線程安全,在訪問共享變量時需要進行適當的同步。
-
同步的措施一般是加鎖,但是使用加鎖的方式增加了性能的損耗,因此可以使用ThreadLocal來實現。
-
ThreadLocal是JDK提供的,提供了
線程本地變量
,也就是如果創建了一個TreadLocal變量,那麼訪問這個變量的每個線程都會有這個變量的一個本地副本
。因此當多個線程操作這個ThreadLocal變量時,其實操作的是自己本地內存
裏面的變量,從而避免了線程安全問題。public class ThreadLocalTest { //創建ThreadLocal變量 static ThreadLocal<String> threadLocal = new ThreadLocal<>(); //print函數 static void print(String string){ //打印當前線程本地內存中的threadLocal System.out.println(string+":"+threadLocal.get()); //清除當前線程本地內存中的threadLocal // threadLocal.remove(); } public static void main(String[] args) { //創建線程 Thread threadA = new Thread(new Runnable() { @Override public void run() { //設置線程A中本地變量threadLocal的值 threadLocal.set("threadA"); //調用打印函數 print("threadA"); System.out.println("threadA remove "+threadLocal.get()); } }); //創建線程 Thread threadB = new Thread(new Runnable() { @Override public void run() { //設置線程B中本地變量threadLocal的值 threadLocal.set("threadB"); //調用打印函數 print("threadB"); System.out.println("threadB remove "+threadLocal.get()); } }); threadA.start(); threadB.start(); } }
執行threadLocal.remove()之後結果
-
以上代碼中創建了一個共享變量threadLocal,兩個線程,線程A和線程B,當我們在線程A中對共享變量設置值時,並不會影響線程B中threadLocal的值,線程中通過set方法設置threadLocal的值,其實設置的是線程中本地內存中的一個副本,這個副本線程B是訪問不了的。同時通過get獲取的是當前線程本地內存中的值。
-
實現原理
:
ThreadLocal的結構 -
通過圖中我們可以看到
Thread
類中有一個threadLocals
和inheritableThreadLocals
,它們都是ThreadLocalMap類型的變量,而ThreadLocalMap是一個定製化的HashMap
。在默認情況下每個線程中的threadLocals和inheritableThreadLocals都爲null,當線程第一次調用set或get時纔會創建
它們。 -
每個線程的本地變量並不存在ThreadLocal實例裏面,而是存放在線程中的
threadLocals
變量中,因此ThreadLocal就是一個工具殼,通過set將值放入線程中的threadLocals中,通過get將值從threadLocals中取出來,也可以通過調用remove將當前線程的threadLocals中的值刪除。 -
那麼爲什麼threadLocals爲什麼被設置成map結構,因爲
一個線程可能關聯多個ThreadLocal變量
。 -
每個線程內部都有一個threadLocals的成員變量,該變量類型爲HashMap,
key
爲ThreadLocal實例對象的引用
,value
則是需要設置的值
。每個線程
的本地變量都存放在線程自己的內存變量threadLocals
中,如果線程不死亡,那麼該變量會一直存在,因此會造成內存溢出,因此使用完畢應當調用remove
刪除threadLoccals中的變量。從源碼中我們也可以看到是通過threadLocals來實現的
public void set(T value) { //獲取當前線程 Thread t = Thread.currentThread(); 調用getMap,當前線程作爲key,去查找對應的線程變量 ThreadLocalMap map = getMap(t); //如果map不爲空,則調用set方法,key爲當前ThreadLocal的實例對象引用,value是傳遞的值。 if (map != null) map.set(this, value); else //如果是第一次則調用 createMap(t, value); } ThreadLocalMap getMap(Thread t) { //在getMap中獲取的是當前線程的threadLocals return t.threadLocals; }
另一個類
lnheritableThreadLocal
類是爲了解決子線程可以訪問父線程中設置的本地變量
。public class InheritableThreadLocal<T> extends ThreadLocal<T> { /** * * @param parentValue the parent thread's value * @return the child thread's initial value */ protected T childValue(T parentValue) { return parentValue; } /** * Get the map associated with a ThreadLocal. * * @param t the current thread */ ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } /** * Create the map associated with a ThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the table. */ void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } }
InheritableThreadLocal繼承ThreadLocal
,並重寫三個方法。因此當調用set
方法時,創建
的是當前線程的inheritableThreadLocals
變量的實例而不再是threadLocals,調用get
方法時獲取當前線程內部的map變量時,獲取的是inheritableThreadLocals
而不再是threadLocals。因此在InheritableThreadLocal的世界裏,變量inheritableThreadLocals替代
了threadLocals。
-
-
整理了三天的併發編程的知識,也不算整理就是看着併發編程的文章抄的,也算給自己加深學習吧!
該知識來自Java併發編程!
依然是會敲代碼的湯姆貓!