1、并發編程
并發編程三要素
- 原子性:只一個操作要么全部成功,要么全部失敗
- 可見性:一個線程對共享變量的修改,其他線程能夠立刻看到
- 有序性:程序執行的順序按照代碼的先后順序執行
synchronized,Lock解決原子性問題
volatile,,Lock解決可見性和有序性問題
2、線程和進程的區別
- 根本區別:
進程是操作系統資源分配的最小單位;線程是處理任務調度和執行的最小單位
。 - 資源開銷:每個進程都有單位的空間,進程間的切換有較大的開銷;線程是輕量級的進程,同一類線程共享數據空間,每個線程有獨立的運行棧和程序計數器,線程之間的切換開銷較小
- 包含關系:進程包含線程
- 內存分配:線程共享地址空間和資源,進程之間的地址空間和資源相互獨立
- 影響關系:一個進程崩潰,在保護模式下其他進程不受影響;一個線程崩潰,其他線程也會受影響,整個進程都會崩潰。多進程要比多線程更加健壯
- 執行關系:進程有獨立的執行入口,線程不能獨立執行,必須依賴進程
上下文切換:任務從保存到再加載的過程就是一次上下文切換。
3、創建線程的四種方式
- 繼承
Thread
類 - 實現
Runnable
接口 - 實現
Callable
接口 - 使用
Executors
工具類創建線程池
Runnable和Callable區別
相同點:
- 都是接口;
- 都可以編寫多線程程序;
- 都采用Thread.start()啟動線程
不同點
- Runnable接口的
run()方法沒有返回值
;Callable()接口的call()方法有返回值
,是個泛型 - Runnable接口run()方法只能拋出運行時異常,且無法捕捉處理;Callable接口call()方法允許拋出異常,可以獲取異常信息
線程的run()和start()有何區別
- start()用于啟動線程,run()用于執行線程運行時的代碼。run()方法可以重復調用,start()只能調用一次
- start()方法用于啟動線程,真正實現了多線程的運行。調用start()時無需等待run()方法方法體代碼執行完畢就可以執行其他代碼;此時線程是就緒態,并沒有開始運行,然后通過Thread類調用run()方法完成其運行狀態,run()方法運行結束,此線程就終止了
- run()方法是在線程里的,直接調用run()方法,相當于調用了一個普通的函數,必須等待run()方法執行完畢才可以執行下面的代碼,所以執行路徑還是一條,沒有多線程的特征,所以在線程啟動時,要調用start()方法而不是run()方法。
Future和FutureTask
- Callable接口的call()方法有返回值,Future可以拿到異步執行任務(這個任務也許并沒有完成)的返回值,并且可以拋出異常信息。
- FutureTask是Future的具體實現。FutureTask實現了RunnableFuture接口。RunnableFuture接口又同時繼承了Future和 Runnable接口。所以FutureTask既可以作為Runnable被線程執行,又可以作為Future得到Callable的返回值。
4、線程的狀態和基本操作
線程的五種狀態和生命周期
- 新建(new);
- 可運行/就緒態(runnable):調用start()方法后就處于Runnable態
- 運行(running):runnable態獲取到時間片,就進入running態;
就緒態是進入運行態的唯一入口,線程要進入運行態就必須要進入就緒態
- 阻塞(block);
- 死亡(dead):死亡的線程不可復生
線程調度的方法
1、wait()
:使一個線程處于阻塞等待狀態,并且釋放所持有的對象鎖
2、sleep()
:使役個正在運行的線程處于睡眠狀態,是一個靜態方法
3、notify()
:喚醒一個處于等待隊列的線程,再調用此方法時,并不能確切的喚醒某個等待的線程,由JVM確定喚醒哪個線程,并且與優先級無關
4、notifyAll()
:喚醒所有處于等待隊列的線程,然后重新競爭鎖
wait()和sleep()區別
- 所在類不同:sleep()是Thread類的靜態方法;wait()是Object類的方法
- 鎖:sleep()不釋放鎖,wait()釋放鎖
- 用途:wait()用于線程之間通信;sleep()用于暫停線程執行
- 用法不同:wait()在結束后,不會自動蘇醒,需要別的線程調用同一個對象上的notify()或者notifyAll()去喚醒;而sleep()方法在結束后,自動蘇醒。
interrupt()、interrupted()、isInterrupted()
- interrupt()、isInterrupted()是通過Thread對象調用,是實例方法;interrupted()是通過Thread類調用,是靜態方法
- interrupt()方法,只是通知該線程停止運行,只是通知,并沒有直接中斷,而是由程序自己決定是否中斷
線程類的構造方法、靜態代碼塊是被new這個線程的類的線程所調用的(誰new誰調用),run()方法是自身線程調用
5、synchronized
作用:用來控制線程同步的,被synchronized修飾的代碼不能被多個線程同時執行,可以修飾類、方法、變量
synchronized底層原理
synchronized修飾的代碼在反編譯為字節碼文件時,前后都出現了monitor
字樣,前面出現的是monitorenter
,后面出現的是monitorexit
,就是釋放鎖。當執行monitorenter時,當前線程試圖獲取對象鎖所持有的monitor,當計數器為0時,就可以成功獲取,當獲取到時,計數器+1。并且就算當前線程已經擁有對象鎖的monitor的持有權,那么就可以重入這個monitor,重入計數器也會加一。如果其他線程占有monitor持有權,那么當前線程就會阻塞,知道其他線程執行monitorexit,執行后鎖釋放,計數器設置為0。
自旋
即其他線程不進入阻塞態,而是在synchronized邊界循環等待,不斷嘗試獲取鎖,這就是自旋
synchronized鎖升級
synchronized涉及到用戶態和內核態的切換,在1.6之前,鎖都是重量級鎖,即我們不管什么線程來操作資源,都要進行加鎖釋放鎖,如果有多線程,還要等待之類的,很浪費資源,1.6之后引入了偏向鎖與輕量鎖來減小獲取和釋放鎖所帶來的性能消耗。
鎖升級其實就是對synchronized的優化,以前用synchronized修飾一個對象或者是方法,方法也等于是鎖住對象,直接用一把操作系統層面的大鎖,萬一只有少量線程的話會大題小作了,如果大量線程的話又會特別消耗時間,劃不來,所以要將以前的二話不說用一把大鎖進行優化。
無鎖->偏向鎖->輕量級鎖->重量級鎖
原理:在鎖對象的對象頭有一個threadid
字段,第一次訪問時threadid為空,JVM讓其持有偏向鎖,并把threadid設置為線程id,再次進入時只需要判斷兩個id是否相等,相等就直接進入,不相等就升級為輕量級鎖;自旋一段時間后還沒有獲取到就升級到重量級鎖。
synchronized、volatile、CAS區別
synchronized
是悲觀鎖,屬于搶占式,會引起其他線程阻塞volatile
提供多個線程共享變量可見性和禁止指令重排序CAS
是基于沖突監測的樂觀鎖(非阻塞)
synchronized、Lock、ReentrantLock區別
- synchronized、ReentrantLock都是
可重入鎖
synchronized
是關鍵字,Lock
是接口,ReentrantLock
是實現了Lock接口的一個類- synchronized可以給類、方法、代碼塊加鎖,Lock和ReentrantLock只能給代碼塊加鎖
- synchronized不用手動獲取和釋放,發生異常會自動釋放鎖,不會造成死鎖;Lock和ReentrantLock需要手動,沒有unLock()就會死鎖
- Lock可以知道是否成功獲取到鎖,synchronized不行
6、volatile
保證可見性和禁止指令重排序,提供happens-before
的保證,確保一個線程的修改對于其他線程是可見的。被volatile修飾的共享變量,當它被修改時,可以將修改的值立即更新到主內存中,其他線程需要讀取時,重新去主內存中讀取新值
volatile可以保證可見性和禁止重排序,但不能保證原子性;atomic方法可以讓這種方法具有原子性
7、Lock體系
Lock是synchronized的擴展版本,Lock提供了無條件的、可輪詢的(tryLock方法
)、定時的(tryLock帶參方法
)、可中斷的(lockInterruptibly
)、可多條件隊列的鎖操作。Lock的實現基本都支持公平鎖和非公平鎖,synchronized只支持非公平鎖
悲觀鎖
:悲傷的假設最壞的情況,每次拿數據都認為別人會修改,所以在拿的時候就會加鎖,別人想拿就阻塞(共享資源每次只給一個線程使用,其他線程阻塞,用完再把資源轉讓給其他線程)樂觀鎖
:每次拿數據,不會上鎖,直到提交數據時才會證實數據是否被修改(產生并發沖突),多用于多讀場景。一般用版本號或者CAS實現
8、CAS
CompareAndSweep
——比較并交換
CAS包含三個操作數——內存位置(V)、預期值(A)、擬寫入的新值(B)
- 第一步:比較V和A是否相等
- 第二步:相等,就把B寫入V
- 第三步:返回boolean類型,表示操作成功
多個線程進行CAS操作時,只有一個線程可以操作成功
,其他線程自旋等待
CAS產生的問題
ABA問題
:從A變到B,再從B變到A,過程不知道;解決辦法:引入版本號
循環開銷時間大
:資源競爭嚴重時,CAS自旋概率大,浪費CPU只能保證一個共享變量的原子性操作
9、線程死鎖
兩個或以上的線程互相持有對方資源并且不主動釋放造成的惡性循環
死鎖的四個條件
互斥條件
:一個資源只能被一個線程占用請求與保持條件
:請求被占用資源而阻塞,不放棄已經獲得的資源不剝奪條件
:資源未使用前不能被其他線程強行剝奪循環等待條件
:等待的線程形成了一個死循環
避免死鎖
破壞造成死鎖四個條件中的一個就行
- 互斥條件無法破壞
- 破壞請求與保持條件:一次性申請所有資源
- 破壞不剝奪條件:申請不到被占用的資源,就主動釋放
- 破壞循壞等待條件:
活鎖
沒有被阻塞,只是某些條件沒滿足,導致一直重復嘗試、失敗、嘗試、失敗這個過程
活鎖有可能自己解開,死鎖不能
饑餓
因為種種原因無法獲取到所需要的資源,導致一直無法執行
10、ThreadLocal
為線程提供局部變量,保證各個線程里的變量獨立于其他線程的變量,也就是說ThreadLocal為每個線程創建一個單獨的副本
,線程之間不相關,
同步機制是為了保證多線程環境下數據的統一性,而ThreadLocal則是保證多線程環境下數據的獨立性
ThreadLocal底層原理
Thread類中有一個ThreadHashMap
的數據結構,用來保存線程對象的變量
每個線程的ThreadHashMap都是屬于線程自己的,這就保證了每個線程都是獨立的,多個操作不會互相影響