匯智動力-Java并發(fā)編程基礎(chǔ)
一?;靖拍钆c方法
1.線程與進(jìn)程
進(jìn)程是CPU分配資源的最小單位,由一個或多個線程組成。
線程是CPU進(jìn)行調(diào)度的最小單位,被稱為輕量級線程。
一個程序至少一個進(jìn)程,一個進(jìn)程至少一個線程
2.Java中線程的三種創(chuàng)建方式
(1)繼承Thread類,并重寫run()方法
(2)實現(xiàn)Runnable接口,并重寫run()方法
(3)實現(xiàn)Callable接口,并重寫call()方法;此種方法有返回值,且需要使用FutureTask類進(jìn)行封裝
實現(xiàn)接口與繼承Thread類的比較:
- Java中只能單繼承,但是可以實現(xiàn)多個接口;使用接口的方法更適合擴(kuò)展
- 繼承整個Thread類的方法開銷過大
- 若想在線程執(zhí)行體中(即run方法體中)訪問當(dāng)前線程,繼承方式可以直接通過this;而接口方法要通過Thread.currrentThread()
- 此外實現(xiàn)Runnable接口創(chuàng)建的線程可以處理同一資源,從而實現(xiàn)資源的共享
1 import java.util.concurrent.Callable; 2 import java.util.concurrent.ExecutionException; 3 import java.util.concurrent.FutureTask; 4 5 public class TestThread { 6 7 public static void main(String[] args) throws InterruptedException { 8 9 ThreadA threadA = new ThreadA(); 10 11 ThreadB threadB = new ThreadB(); 12 13 FutureTaskfutureTask = new FutureTask<>(new ThreadC()); 14 15 //依次啟動三個線程 16 threadA.start(); 17 Thread.sleep(1000);//主線程休眠1000ms 18 19 //需傳入實現(xiàn)接口的類以創(chuàng)建線程 20 new Thread(threadB).start(); 21 Thread.sleep(1000); 22 23 new Thread(futureTask).start(); 24 try { 25 //獲取第三種方法創(chuàng)建線程中設(shè)置的返回值 26 String result = futureTask.get(); 27 System.out.println(result); 28 } catch (ExecutionException e) { 29 e.printStackTrace(); 30 } 31 } 32 33 } 34 35 /** 36 * 繼承Thread類并重寫run()方法 37 */ 38 class ThreadA extends Thread { 39 @Override 40 public void run() { 41 System.out.println("我是線程A"); 42 } 43 } 44 45 /** 46 * 實現(xiàn)Runnable接口并重寫run()方法 47 */ 48 class ThreadB implements Runnable { 49 50 @Override 51 public void run() { 52 System.out.println("我是線程B"); 53 } 54 55 } 56 57 /** 58 *實現(xiàn)Callable接口并重寫call()方法 59 */ 60 class ThreadC implements Callable { 61 62 @Override 63 public String call() throws Exception { 64 return "我是線程C"; 65 } 66 67 }
3.線程的狀態(tài)
(1)新建狀態(tài):創(chuàng)建后未啟動
(2)就緒狀態(tài):調(diào)用start()方法后進(jìn)入該狀態(tài),與其他就緒狀態(tài)線程一起競爭CPU,等待CPU的調(diào)度。
(3)運(yùn)行狀態(tài):就緒狀態(tài)的線程獲得CPU時間片,真正的執(zhí)行run()方法。線程只能從就緒狀態(tài)進(jìn)入運(yùn)行狀態(tài)
(4)阻塞狀態(tài):線程由于如下所示的各種原因進(jìn)入阻塞,線程掛起
- 該線程調(diào)用Thread.sleep()方法
- 等待阻塞,線程中的共享變量調(diào)用了wait()方法
- I/O流方式,如read()方法,receive()方法等待數(shù)據(jù)
- 同步阻塞,線程因無法獲得目標(biāo)資源的鎖而被掛起
4.sleep()方法和wait()方法
sleep()是Thread類中的靜態(tài)方法,調(diào)用Thread.sleep(time)后線程休眠time毫秒,休眠過程中線程不會釋放擁有的對象鎖。如果該線程睡眠期間其他線程調(diào)用了該線程的interrupt()方法中斷了該線程,該線程會在調(diào)用sleep()方法的地方拋出InterruptedException。
wait()是Object類中的方法,當(dāng)線程調(diào)用一個共享變量的wait()方法是,該線程會被掛起并且釋放該對象鎖,進(jìn)入等待此對象的等待鎖定池,直到其他線程調(diào)用了該共享對象的notify()或者notifyAll()方法。其中,notify()是在等待鎖定池中隨機(jī)喚醒一個線程,notifyAll()是喚醒所有因該對象的wait()方法而掛起的線程。
注意:調(diào)用共享變量的wait()、notify()、notifyAll()方法,需要先獲得共享變量的對象鎖。被喚醒的線程不會立即執(zhí)行,需要和其他線程一起競爭對象鎖(由調(diào)用notify()方法的線程所釋放的對象鎖)。
public class TestNotify { private static volatile Object resourceA = new Object(); public static void main(String[] args) throws InterruptedException { //創(chuàng)建線程A Thread threadA = new Thread(new Runnable() { @Override public void run() { //先獲取共享資源resourceA的對象鎖 synchronized(resourceA){ System.out.println("TA get RA lock"); try { System.out.println("TA begin wait"); //調(diào)用共享對象的wait()方法 resourceA.wait(); System.out.println("TA end wait"); } catch (InterruptedException e) { e.printStackTrace(); } } } }); //創(chuàng)建線程B Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized(resourceA) { System.out.println("TB get RA lock"); try { System.out.println("TB begin wait"); resourceA.wait(); System.out.println("TB end wait"); } catch (InterruptedException e) { e.printStackTrace(); } } } }); //創(chuàng)建線程C Thread threadC = new Thread(new Runnable() { @Override public void run() { //只有先獲得共享對象的鎖才能調(diào)用notify()等方法,否則會拋出IllegalMonitorStateException synchronized(resourceA) { System.out.println("TC begin notify"); resourceA.notify();//隨機(jī)喚醒一個因resourceA.wait()掛起的線程 } } }); //啟動線程,線程進(jìn)入就緒狀態(tài),具體什么時候運(yùn)行由CPU調(diào)度決定 threadA.start(); threadB.start(); Thread.sleep(1000);//主線程休眠1000ms threadC.start();//啟動線程C //等待線程結(jié)束 threadA.join(); threadB.join(); threadC.join(); System.out.println("main over"); } }
5.join()方法和yield()方法
join()方法,Thread類的成員方法,插隊方法,線程A的執(zhí)行體中調(diào)用 B.join(),B代表線程B,則線程A會阻塞,讓B線程插隊。參數(shù)可以傳入時間(毫秒),表示允許插隊運(yùn)行的時間長度。
yield()方法,Thread類的靜態(tài)方法,禮讓方法,線程A調(diào)用Thread.yield()方法后會讓出CPU使用權(quán),進(jìn)入就緒狀態(tài),與其他處于就緒狀態(tài)的線程一起競爭CPU。(實際上,調(diào)用yield()方法之后,線程調(diào)度器會從線程就緒隊列中獲取一個線程優(yōu)先級最高的線程,而該線程的優(yōu)先級會變?yōu)?)
6.線程中斷
線程中斷是線程間的一種協(xié)作模式,通過設(shè)置線程的中斷標(biāo)志并不能直接終止該線程的執(zhí)行,而是被中斷的線程根據(jù)中斷狀態(tài)自行處理。
interrupt()方法,中斷線程,將線程的中斷標(biāo)志設(shè)置為true。當(dāng)線程因調(diào)用wait()、join()、sleep()等方法進(jìn)入阻塞時,其他線程調(diào)用該線程的interrupt()方法,該線程會拋出InterruptedException并返回。如果調(diào)用線程的interrupt()方法后未拋出InterruptedException,則應(yīng)通過interrupted()方法判斷當(dāng)前線程是否被中斷來返回線程(如在執(zhí)行體中使用該方法作為線程執(zhí)行前提條件)
7.守護(hù)線程與用戶線程
守護(hù)線程是服務(wù)于用戶線程的,可以通過調(diào)用setDaemon(true)方法將用戶線程設(shè)置為守護(hù)線程
兩者可以通過JVM是否等待線程結(jié)束來區(qū)分,JVM只會等待用戶線程結(jié)束;守護(hù)線程不會影響JVM的退出,不管其是否運(yùn)行結(jié)束都會隨著JVM的結(jié)束而結(jié)束。即用戶線程全部結(jié)束時,程序終止,并殺死所有守護(hù)線程。如main函數(shù)就是一個用戶線程,而垃圾回收線程就是一個守護(hù)線程。
8.ThreadLocal的使用
ThreadLocal由JDK包提供,它提供了線程本地變量,即每個訪問ThreadLocal變量的線程都會有一個該變量的隨機(jī)副本。線程對該變量進(jìn)行操作時,實際上是對自己的本地內(nèi)存里的變量進(jìn)行操作,從而避免了多線程共享一個變量時的安全問題。如在封裝MyBatisUtil工具包時,其中就用到了將SqlSession的實例對象存儲在ThreadLocal的實例對象中,每次通過 tl.get()獲取,使用完后關(guān)閉SqlSession實例對象,并 tl.set(null)將ThreadLocal清空;tl是ThreadLocal的實例對象。
二。線程安全問題與解決
1.Java中的線程安全問題
當(dāng)多個線程對共享資源進(jìn)行訪問時,只有當(dāng)至少有一個線程修改共享資源時才會存在線程安全問題。典型的如計數(shù)器類實現(xiàn)中的丟失修改問題。
2.共享變量的內(nèi)存可見性問題
Java中所有的變量存放在主存中,而線程使用變量時會把主內(nèi)存里面的變量復(fù)制到自己的工作內(nèi)存中,線程讀寫變量時操作的是自己工作變量中的內(nèi)存,然后將自己工作內(nèi)存中的變量刷新到主內(nèi)存中。因此,當(dāng)線程A和線程B同時處理一個共享變量時,會存在內(nèi)存不可見的問題。
3.鎖的概念
(1)樂觀鎖與悲觀鎖:是從數(shù)據(jù)庫概念中引入的詞。悲觀鎖指認(rèn)為數(shù)據(jù)很容易被其他線程修改,因此會在數(shù)據(jù)被處理前對數(shù)據(jù)進(jìn)行加鎖,使得整個處理過程中數(shù)據(jù)處于鎖定狀態(tài)。樂觀鎖則是認(rèn)為數(shù)據(jù)在一般情況下不會造成沖突,因此在訪問數(shù)據(jù)前不會加排它鎖,只有在數(shù)據(jù)提交更新時,才會正式的對數(shù)據(jù)沖突與否進(jìn)行檢測。
(2)獨(dú)占鎖與共享鎖:根據(jù)鎖只能被單個線程持有還是能被多個線程持有,分為獨(dú)占鎖(排它鎖)和共享鎖。獨(dú)占鎖是一種悲觀鎖,每次訪問資源前都先加上互斥鎖,只允許同一時間由一個線程讀取數(shù)據(jù)。而共享鎖是一種樂觀鎖,允許多個線程同時進(jìn)行讀操作。
(3)公平鎖與非公平鎖:根據(jù)線程獲取鎖的搶占機(jī)制,可以分為公平鎖與非公平鎖。公平鎖表示線程獲取鎖的順序是按照線程請求鎖的時間早晚來決定的,即早到早得。而非公平鎖則不一定先到先得。ReentrantLock提供的鎖默認(rèn)是非公平鎖。一般來說,在沒有公平性需求的前提下,盡量使用非公平鎖,因為公平鎖會帶來性能開銷。
(4)可重入鎖:一個線程再次獲取它自己已經(jīng)獲得的鎖時,則稱為可重入鎖。可重入的原理是在鎖內(nèi)部維護(hù)一個線程表示,線程表示來指示該鎖目前被哪個線程占有,然后關(guān)聯(lián)一個計數(shù)器來表示該鎖是否被線程占用,0為未被占用,1為已占用,此后每次重入則計數(shù)器+1.
(5)自旋鎖:自旋鎖是指線程在獲取鎖失敗時不會馬上掛起,而是在不放棄CPU使用權(quán)的情況下,多次嘗試獲取該鎖(默認(rèn)10次)。一般而言,當(dāng)線程獲取鎖失敗后,會切換到內(nèi)核狀態(tài)而被掛起;當(dāng)該線程獲取鎖后又需要將其切換到內(nèi)核狀態(tài)而喚醒該線程,而用戶狀態(tài)切換到內(nèi)核狀態(tài)的開銷是比較大的,即自旋鎖是使用CPU時間換取線程阻塞與調(diào)度的開銷。
4.synchronized的使用
synchronized是Java提供的一種原子性內(nèi)置鎖。是一種排它鎖,同時也是非公平的。synchronized可以解決共享變量的內(nèi)存可見性問題。
進(jìn)入synchronized塊的語義是,把塊內(nèi)使用的變量從線程的工作內(nèi)存中清除,這樣線程就會直接從主內(nèi)存中去獲取塊內(nèi)需要使用的變量。
退出synchronized塊的語義是,將synchronized塊內(nèi)對共享變量的修改刷新到主內(nèi)存中。
5.volatile的使用
使用鎖的方式解決共享變量內(nèi)存可見性的問題太過繁瑣,開銷太大,因此Java提供了一種弱形式的同步,即volatile關(guān)鍵字。
類成員變量或者類靜態(tài)成員變量被volatile修飾后主要有兩個特性
(1)解決不同線程對該變量進(jìn)行的操作時的可見性問題。因為線程在操作volatile修飾的變量時,不會把值緩存到寄存器或者其他地方,而是直接把值刷新會主內(nèi);當(dāng)其他線程獲取該變量時,會從主內(nèi)存中重新獲取最新值,而不是使用當(dāng)前線程工作內(nèi)存中的值。
(2)禁止指令重排,一定程度上能保證有序性。具體情況是,寫volatile變量時,寫之前的操作不會被編譯器重排序到volatile寫之后。讀volatile變量時,讀之后的操作不會被編譯器重排序到volatile讀之前。
6.Java中的CAS操作
Java中使用鎖來處理并發(fā)會產(chǎn)生線程上下文切換和重新調(diào)度的開銷。而非阻塞的volatile關(guān)鍵字只能保證共享變量的可見性,不能解決讀-改-寫等原子性問題。因此JDK提供了非阻塞原子性操作,即CAS(Compare and Swap)操作,它通過硬件保證了比較-更新操作的原子性。
CAS操作有個經(jīng)典的ABA問題,大概意思是 線程1獲取變量X的值(A),然后修改變量X的值為B,這種情況下即使使用CAS操作成,程序也不一定運(yùn)行正確。因為可能存在線程2在1獲取變量X后,使用CAS操作修改了X的值為B,然后又使用CAS操作修改X的值為A,這樣線程1修改變量X的值是,已經(jīng)是此A非彼A了。
ABA問題大概流程:1.CASget(X-A) --->2.CASset(X-B)--->2.CASset(X-A)--->1.CASset(X-B)。
ABA問題的產(chǎn)生是因為變量的狀態(tài)值產(chǎn)生了環(huán)形轉(zhuǎn)換,即變量值從A到B,然后再從B到A。如果規(guī)定變量的值只能朝著一個方向轉(zhuǎn)換,則不會出現(xiàn)該問題。因此JDK中的AtomicStampedReference類給每個變量的狀態(tài)值都配置了一個時間戳死,以避免ABA問題發(fā)生。

