目錄
?1. 什么是線程和進程?
線程與進程有什么區別?
那什么是上下文切換?
進程間怎么通信?
什么是用戶線程和守護線程?
2. 并行和并發的區別?
3. 創建線程的幾種方式?
Runnable接口和Callable接口的區別?
run()方法和start()有什么區別?
4. Java線程狀態和方法?
描述線程的生命周期?
一個線程兩次調用start()方法會出現什么情況?
sleep()和wait()方法的區別是什么?
5. 并發編程的三要素是什么?
6. 什么是線程死鎖?
怎么定位死鎖?
7. Java并發包提供了哪些并發工具類?
并發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區別?
8. 什么是線程池?
講講線程池的生命周期?
線程池有哪些類型?
線程池的拒絕策略?
線程池的執行流程?
Java并發類庫提供的線程池有哪幾種? 分別有什么特點?
Executor創建線程為什么不建議使用了?
9. AtomicInteger底層實現原理是什么?
什么是CAS?
10. 鎖的分類有哪些?
synchronized和ReentrantLock有什么區別呢?
為什么Synchronized能實現線程同步?
鎖升級過程?
?1. 什么是線程和進程?
進程:是指一個在內存中運行的應用程序,常見的app都是一個個進程。進程具有自己獨立的內存空間,一個進程可以有多個線程;
線程:是指進程中的一個執行任務的單元,負責執行當前進程中程序的執行,一個進程至少有一個線程,一個進程內的多個線程見可共享數據。
線程與進程有什么區別?
- 根本區別:進程是操作系統資源分配的單位,而線程是處理器任務調度和執行的基本單位;
- 資源開銷:進程有獨立的代碼和數據空間(程序上下文),程序之間切換會有較大開銷;線程可以看作輕量級進程,同一類線程共享代碼和數據空間,每個線程都有自己獨立的運行棧和程序計數器,線程間切換開銷較小;
- 包含關系:一個進程中至少有一個線程,基本都是有多個線程共同完成一個進行的運行;而線程是屬于進行中的一部分;
- 內存分配:進程分配到獨立的地址空間和資源,而線程是功效一個進行內的地址空間和資源的;
- 影響關系:進程崩潰不影響其他進程的執行,但線程崩潰會導致進程崩潰,所以進程比線程的健壯性好;
- 執行過程:每個獨立的進程有程序運行的入口、順序執行序列和程序出口。但線程不可單獨執行,必須依附在進程中,由應用程序提供多線程的控制。
那什么是上下文切換?
- 在多線程編程中,大多線程數量大于CPU核心個數,而一個CPU在某一時刻只能執行一個線程。為了讓這些線程都能得到執行,CPU采用的策略是為每個線程分配一個時間片去輪流執行,當一個線程的時間片用完就會讓這個線程重新處于就緒狀態而把CPU讓給其他線程使用,這個過程就叫做上下文切換。簡單來說:任務從保存到再加載的過程就是一次上下文切換。
進程間怎么通信?
進程間的通信方式有很多,比如:管道、消息隊列、共享內存、信號、嵌套字
- 管道:包含無名管道和命名管道,無名管道半雙工,只能用于具有親緣關系的進程間通信,可以看作一種特殊文件;命名管道可以允許無親緣關系的進程間通信;
- 消息隊列: 就是一個消息的鏈表,是一系列保存在內核中消息的列表。當一個進程需要通信的時候,只需要將數據寫入這個消息列表當中,就可以正常退出干其他事情了,另一個進程需要數據的時候只需去讀取數據就行了。消息隊列獨立于發送和接收進程,哪怕發送進程終止了,隊列中數據也不會被刪除;
- 信號:用于通知接收進程某個事件發生
- 內存共享:使多個進程訪問同一塊內存空間
- 嵌套字:socket,用于不同主機間直接的通信
什么是用戶線程和守護線程?
- 用戶線程:指的是運行在前臺,執行具體任務的線程,如main所在縣丞就是用戶線程;
- 守護線程:指的是運行在后臺,為其他用戶線程服務的線程,如垃圾回收線程。特點是守護線程不影響JVM的退出。
2. 并行和并發的區別?
- 并發:多個任務在同一個CPU核上執行的時候,多個任務根據細分的時間片去交替執行,因為交替時間對人來說非常短暫,所以是一種感覺上的同時執行,但本質還是在某一時刻只執行一個線程;
- 并行:單位時間內,多個處理器或多核處理器同時處理多個任務,是真正意義上的同時執行,叫做并行。
多線程:就是指程序中包含多個執行流,就是在一個程序中可以同時運行多個不同的線程來執行不同的任務。
- 好處:提升了CPU的使用率:在多線程環境中,一個線程在等待時,其他線程可以運行(也就是說允許單個程序創建多個并行執行的線程來完成各自的任務),這樣大大提高了程序的效率。
- 劣勢:線程數量與內存占用成正比,線程越多消耗內存也越多;多線程需要協調和管理,所以需要CPU時間跟蹤線程;線程之間對共享資源的訪問會相互影響,也就是解決我們常說的線程共享資源問題(并發問題)。
3. 創建線程的幾種方式?
- 實現Runnable接口
- 繼承Thread類
- 實現Callable接口
- 使用Executors工具類創建線程池
Ps:有的文檔也把匿名內部類和Lambda表達式的方式也分別算作創建線程的方式,主要回答這四種比較常規。
Runnable接口和Callable接口的區別?
- 返回值:Runnable的run()方法執行沒有返回值,而Callable執行的call()方法可以返回執行結果;
- 異常處理:Runnable的run()方法不能拋出可被檢查的異常,只能拋出非受檢查的RuntimeException。而Callable接口的call()方法可以拋出任何類型的異常,包括受檢查的異常。
- 兼容性:Callable接口是在Java 5中引入的新接口,而Runnable接口是在Java 1.0中就存在的。Callable接口提供了更多的靈活性和功能,但Runnable接口仍然是使用較多的接口之一,因為它的簡單性和兼容性。
- 并發集合:Callable接口通常與ExecutorService和Future配合使用,以支持異步任務執行和獲取結果。 Runnable接口通常與Thread類或者Executor框架一起使用,用于執行簡單的線程任務。
run()方法和start()有什么區別?
每個線程都是通過其特定的Thread對象所對應的方法run()來完成其操作的,run()方法稱為線程體,通過調用Thread類的start()方法來啟動一個線程。
- start()方法用于啟動線程,run()方法用于執行線程的運行代碼;
- run()方法可以重復調用,而start()方法只能調用一次。
- start()方法來啟動一個線程,真正的實現了多線程運行。調用satrt()方法無需等待run()方法體代碼執行完畢,可以繼續執行其他代碼。此時線程處于就緒狀態,并沒有運行。然后調用run()方法來等待分配資源運行線程。
4. Java線程狀態和方法?
Java線程狀態有6種,分別是 NEW(新建狀態)、RUNNABLE(就緒狀態)、 BLOCKED(阻塞狀態)、WAIT(等待狀態)、TIME_WAIT(超時等待狀態)、TERMINATED(終止狀態):
- 新建狀態(NEW):線程最new出來最初始的狀態,還沒調用start方法;
- 就緒狀態(RUNNABLE): start()啟動線程后,Java線程把操作系統中就緒和運行兩種狀態統一稱為“運行中”;
- 阻塞狀態(BLOCKED):表示線程進入等待狀態,也就是線程因為某種原因放棄了CPU的使用權,阻塞也分為幾種情況:
- 等待阻塞:運行的線程執行了Thread.sleep、wait、join等方法,JVM會把當前線程設置為等待狀態,當sleep結束,join線程終止或者線程被喚醒后,該線程從等待狀態進入阻塞狀態,重新占用鎖后進行線程恢復;
- 同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被其他線程鎖占用了,那么JVM會把當前項城放入到鎖池中;
- 其他阻塞:發出I/O請求,JVM會把當前線程設置為阻塞狀態,當I/O處理完畢則線程恢復
- 等待狀態(WAITIING):等待狀態,沒有超時時間(無限等待),要被其他線程或者有其他的中斷操作 ,執行wait、join、LockSupport.park();
- 超時等待狀態(TIME_WAIT):與等待不同的是它不是無限等待,超時后自動返回 ,執行sleep、帶參數的wait等可以實現;
- 終止狀態(TERMINATED):線程執行完畢的狀態。
狀態和方法對應圖如下:
描述線程的生命周期?
線程的生命周期是指操作系統層面上的線程的五種狀態,五大生命周期 分別為:新建(NEW),就緒(Runnable),運行(Running),阻塞(Blocked)(又分為 Blocked,waiting,time-waiting),死亡(Dead/TERMINATED)具體如下:
- 新建(NEW):新創建了一個線程對象。
- 可運行(RUNNABLE):線程對象創建后,其他線程(比如main線程)調用了該對象的start()方法。該狀態的線程位于可運行線程池中,等待被線程調度選中,獲取cpu 的使用權 。
- 運行(RUNNING):可運行狀態(RUNNABLE)的線程獲得了CPU 時間片,執行程序代碼。
- 阻塞(BLOCKED):阻塞狀態是指線程因為某種原因放棄了cpu 使用權,也即讓出了cpu timeslice,暫時停止運行。直到線程進入可運行(runnable)狀態,才有機會再次獲得cpu timeslice 轉到運行(running)狀態。
- 死亡(DEAD):線程run()、main() 方法執行結束或因異常退出了run()方法,則該線程結束生命周期。
一個線程兩次調用start()方法會出現什么情況?
Java 的線程是不允許啟動兩次的,第二次調用必然會拋出IllegalThreadStateException,這是一種運行時異常,多次調用start被認為是編程錯誤。
sleep()和wait()方法的區別是什么?
雖然兩個方法都有讓線程暫停的作用,但是兩個還是發揮不同作用的:
- wait()是Object類的方法,sleep()是Thread類的方法;
- wait()釋放鎖,讓線程進入等待狀態,sleep()不釋放鎖,讓線程進入阻塞狀態;
- wait()方法后線程不會自動恢復執行,需要手動調用notify()/notifyAll()方法喚醒,sleep()在睡眠固定時間后會走動蘇醒;
- wait()常用于線程間交互/通信,sleep通常被用于暫停等待。
5. 并發編程的三要素是什么?
- 原子性:原子是指一個不可再分割的顆粒,原子性值得是一個或者多個操作要么全部成功要么全部失敗;
- 可見性:一個線程對貢獻變量的修改,另一個線程能夠立刻看到(volatile,synchronized);
- 有序性:程序執行代碼的順序要按照代碼的先后順序執行。
可能出現線程安全問題的原因:
- 線程切換帶來原子性問題
- 緩存導致可見性問題
- 編譯優化帶來的有序性問題
解決辦法:
- JDK Atomic開頭的原子類,synchronized、lock等可解決原子性問題
- synchronized、lock、volatile可解決原子性問題
- Happens-before規則可以解決有序性問題
6. 什么是線程死鎖?
死鎖是一種特定的程序狀態,在實體之間,由于循環依賴導致彼此一直處于等待之中,沒有任何個體可以繼續前進。死鎖不僅僅是在線程之間會發生,存在資源獨占的進程之間同樣也可能出現死鎖。通常來說,我們大多是聚焦在多線程場景中的死鎖,指兩個或多個線程之間,由于互相持有對方需要的鎖,而永久處于阻塞的狀態。死鎖示意圖:
形成死鎖的四個必要條件:
- 互斥條件:線程申請的資源在一段時間中只能被一個線程使用
- 請求與等待條件:線程已經擁有了一個資源,但是又申請新的資源,擁有的資源保持不變 。
- 不可剝奪條件:在一個線程沒有用完,主動釋放資源的時候,不能被搶占。
- 循環等待條件:多個線程之間存在資源循環鏈。
處理死鎖的方法:
- 預防死鎖:破壞死鎖產生的四個條件之一,注意,互斥條件不能破壞。
- 避免死鎖:合理的分配資源。
- 檢查死鎖:利用專門的死鎖機構檢查死鎖的發生,然后采取相應的方法。
- 解除死鎖:發生死鎖時候,采取合理的方法解決死鎖,一般是強行剝奪資源。
怎么定位死鎖?
最常見的方式就是利用jstack等工具獲取線程棧,然后定位互相之間的依賴關系,進而找到死鎖。如果是比較明顯的死鎖,往往 jstack 等就能直接定位,類似 JConsole 甚至可以在圖形界面進行有限的死鎖檢測。
如果程序運行時發生了死鎖,絕大多數情況下都是無法在線解決的,只能重啟、修正程序本身問題。所以,代碼開發階段互相審查,或者利用工具進行預防性排查,往往也是很重要的。
7. Java并發包提供了哪些并發工具類?
- 提供了比 synchronized 更加高級的各種同步結構,包括 CountDownLatch、CyclicBarrier、Semaphore 等,可以實現更加豐富的多線程操作,比如利用 Semaphore 作為資源控制器,限制同時進行工作的線程數量。
- 各種線程安全的容器,比如最常見的 ConcurrentHashMap、有序的 ConcurrentSkipListMap,或者通過類似快照機制,實現線程安全的動態數組 CopyOnWriteArrayList 等。
- 各種并發隊列實現,如各種 BlockingQueue 實現,比較典型的 ArrayBlockingQueue、 SynchronousQueue 或針對特定場景的 PriorityBlockingQueue 等。
- 強大的 Executor 框架,可以創建各種不同類型的線程池,調度任務運行等,絕大部分情況下,不再需要自己從頭實現線程池和任務調度器。
Ps:java.util.concurrent 包提供的容器(Queue、List、Set)、Map,從命名上可以大概區分為 Concurrent*、CopyOnWrite和 Blocking等三類,同樣是線程安全容器,可以簡單認為:
- Concurrent 類型沒有類似 CopyOnWrite 之類容器相對較重的修改開銷。
- 但是,凡事都是有代價的,Concurrent 往往提供了較低的遍歷一致性。
- 你可以這樣理解所謂的弱一致性,例如,當利用迭代器遍歷時,如果容器發生修改,迭代器仍然可以繼續進行遍歷。
- 與弱一致性對應的,就是我介紹過的同步容器常見的行為“fail-fast”,也就是檢測到容器在遍歷過程中發生了修改,則拋出 ConcurrentModificationException,不再繼續遍歷。
- 弱一致性的另外一個體現是,size 等操作準確性是有限的,未必是 100% 準確。與此同時,讀取的性能具有一定的不確定性。
并發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區別?
LinkedBlockingQueue 和 ConcurrentLinkedQueue 是 Java 高并發場景中最常使用的隊列。盡管這兩個隊列經常被用作并發場景的數據結構,但它們之間仍有細微的特征和行為差異。LinkedBlockingQueue 是一個 “可選且有界” 的阻塞隊列實現,ConcurrentLinkedQueue 是一個無邊界、線程安全且無阻塞的隊列。
相似之處:
- 都實現 Queue 接口
- 它們都使用 linked nodes 存儲節點
- 都適用于并發訪問場景
不同之處:
特性
LinkedBlockingQueue
ConcurrentLinkedQueue
阻塞性
阻塞隊列,并實現
blocking queue
接口
非阻塞隊列,不實現
blocking queue
接口
隊列大小
可選的有界隊列,這意味著可以在創建期間定義隊列大小
無邊界隊列,并且沒有在創建期間指定隊列大小的規定
鎖特性
基于鎖的隊列
無鎖隊列
算法
鎖的實現基于 “雙鎖隊列(two lock queue)” 算法
依賴于
Michael&Scott算法來實現無阻塞、無鎖隊列
實現
在雙鎖隊列算法機制中,
LinkedBlockingQueue使用兩種不同的鎖,putLock和takeLock。put/take
操作使用第一個鎖類型,take/poll操作使用另一個鎖類型
使用CAS(Compare And Swap)進行操作
阻塞行為
當隊列為空時,它會阻塞訪問線程
當隊列為空時返回 null,它不會阻塞訪問線程
8. 什么是線程池?
線程池就是管理線程的一個容器,有任務需要處理時,會相繼判斷核心線程數是否還有空閑、線程池中的任務隊列是否已滿、是否超過線程池大小,然后調用或創建線程或者排隊,線程執行完任務后并不會立即被銷毀,而是仍然在線程池中等待下一個任務,如果超過存活時間還沒有新的任務就會被銷毀,通過這樣復用線程從而降低開銷。
使用線程池的優點:
- 提升線程池中線程的使用率,減少對象的創建、銷毀。
- 線程池的伸縮性對性能有較大的影響,使用線程池可以控制線程數,有效的提升服務器的使用資源,避免由于資源不足而發生宕機等問題。
講講線程池的生命周期?
- RUNNING :接收新的任務,并且可執行隊列里的任務
- SHUTDOWN :shutdown()方法將線程池狀態轉換為SHUTDOWN,停止接收新任務,但可執行隊列里的任務
- STOP :shutdownNow()方法將線程池狀態轉換為STOP,同時中斷所有線程,停止接收新任務,不執行隊列里的任務,中斷正在執行的任務
- TIDYING :所有任務都已終止,線程數為0,線程池變為TIDYING狀態,會執行鉤子函數terminated(),鉤子方法是指使用一個抽象類實現接口,一個抽象類實現這個接口,需要的方法設置為abstract,其它設置為空方法
- TERMINATED :執行完terminated()鉤子方法,線程池已終止,變為TERMINATED狀態
線程池有哪些類型?
- FixedThreadPool:固定線程數的線程池,核心線程數和最大線程數一樣。特點是當線程達到核心線程數后,如果任務隊列滿了,也不會創建額外的非核心線程去執行任務,而是執行拒絕策略。
- CachedThreadPool:緩存線程池,特點是線程數幾乎是可以無限增加的(最大值是Integer.MAX_VALUE,基本不會達到),當線程閑置時還可以進行回收,而且它采用的存儲任務的隊列是SynchronousQueue隊列,隊列容量是0,實際不存儲任務,只負責對任務的中轉和傳遞,所以來一個任務線程池就看是否有空閑的線程,有的話就用空閑的線程去執行任務,否則就創建一個線程去執行,效率比較高。
- ScheduledThreadPool:支持定時或者周期性執行任務
- SingleThreadExecutor:線程池中只有一個線程去執行任務,如果執行任務過程中發生了異常,則線程池會創建一個新線程來執行后續任務,這個線程因為只有一個線程,所以可以保證任務執行的有序性。
- SingleThreadScheduleExecutor:和ScheduledThreadPool很相似,只不過它的內部也只有一個線程,他只是將核心線程數設置為了1,如果執行期間發生異常,同樣會創建一個新線程去執行任務。
- ForkJoinPool:支持將一個任務拆分成多個“小任務”并行計算,這個線程池是在jdk1.7之后加入的,它主要用于實現“分而治之”的算法,特別是分治之后遞歸調用的函數。
線程池的拒絕策略?
當任務隊列和線程池都滿了時所采取的應對策略:
- 默認是AbordPolicy,表示無法處理新任務,并拋出RejectedExecutionException異常;
- CallerRunsPolicy:用調用者所在的線程處理任務。此策略提供簡單的反饋機制,能夠減緩新任務的提交速度;
- DiscardPolicy:不能執行任務,并將任務刪除;
- DiscardOldestPolicy:丟棄隊列最近的任務,并執行當前的任務。
線程池的執行流程?
Java并發類庫提供的線程池有哪幾種? 分別有什么特點?
通常開發者都是利用 Executors 提供的通用線程池創建方法,去創建不同配置的線程池,主要區別在于不同的 ExecutorService 類型或者不同的初始參數。Executors 目前提供了 5 種不同的線程池創建配置:
- newCachedThreadPool(),它是一種用來處理大量短時間工作任務的線程池,具有幾個鮮明特點:它會試圖緩存線程并重用,當無緩存線程可用時,就會創建新的工作線程;如果線程閑置的時間超過 60 秒,則被終止并移出緩存;長時間閑置時,這種線程池,不會消耗什么資源。其內部使用 SynchronousQueue 作為工作隊列。
- newFixedThreadPool(int nThreads),重用指定數目(nThreads)的線程,其背后使用的是無界的工作隊列,任何時候最多有 nThreads 個工作線程是活動的。這意味著,如果任務數量超過了活動隊列數目,將在工作隊列中等待空閑線程出現;如果有工作線程退出,將會有新的工作線程被創建,以補足指定的數目 nThreads。
- newSingleThreadExecutor(),它的特點在于工作線程數目被限制為 1,操作一個無界的工作隊列,所以它保證了所有任務的都是被順序執行,最多會有一個任務處于活動狀態,并且不允許使用者改動線程池實例,因此可以避免其改變線程數目。
- newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize),創建的是個 ScheduledExecutorService,可以進行定時或周期性的工作調度,區別在于單一工作線程還是多個工作線程。
- newWorkStealingPool(int parallelism),這是一個經常被人忽略的線程池,Java 8 才加入這個創建方法,其內部會構建ForkJoinPool,利用Work-Stealing算法,并行地處理任務,不保證處理順序。
Executor創建線程為什么不建議使用了?
- 缺乏對線程池的精細控制:Executors 提供的方法通常創建一些簡單的線程池,如固定大小的線程池、單線程線程池等。然而,這些線程池的配置通常是有限制的,難以進行進一步的定制和優化。
- 可能引發內存泄漏:一些 Executors 創建的線程池,特別是 FixedThreadPool 和 SingleThreadExecutor,使用無界隊列來存儲等待執行的任務。這意味著如果任務提交速度遠遠快于任務執行速度,隊列中可能會積累大量未執行的任務,可能導致內存泄漏。
- 不易處理異常:Executors 創建的線程池默認使用一種默認的異常處理策略,通常只會將異常打印到標準輸出或記錄到日志中,但不會提供更多的控制。這可能導致異常被忽略或無法及時處理。
- 不支持線程池的動態調整:某些線程池應該支持動態調整線程數量以應對不同的負載情況。Executors 創建的線程池通常是固定大小的,不容易進行動態調整。
- 可能導致不合理的線程數目:一些 Executors 方法創建的線程池默認將線程數目設置為非常大的值,這可能導致系統資源的浪費和性能下降。
9. AtomicInteger底層實現原理是什么?
AtomicIntger 是對 int 類型的一個封裝,提供原子性的訪問和更新操作,其原子性操作的實現是基于 CAS(compare-and-swap)技術。AtomicInteger是java.util.concurrent.atomic 包下的一個原子類,該包下還有AtomicBoolean, AtomicLong,AtomicLongArray, AtomicReference等原子類,主要用于在高并發環境下,保證線程安全。
什么是CAS?
所謂 CAS,表征的是一系列操作的集合,獲取當前數值,進行一些運算,利用 CAS 指令試圖進行更新。如果當前數值未變,代表沒有其他線程進行并發修改,則成功更新。否則,可能出現不同的選擇,要么進行重試,要么就返回一個成功或者失敗的結果。CAS 是 Java 并發中所謂 lock-free 機制的基礎。執行流程如圖:
10. 鎖的分類有哪些?
常見描述種的鎖以及其解釋:
- 樂觀鎖:樂觀鎖認為一個線程去拿數據的時候不會有其他線程對數據進行更改,所以不會上鎖。實現:CAS機制、版本號機制。以Atomic開頭的包裝類,例如AtomicBoolean,AtomicInteger,AtomicLong。適合讀操作多的場景,不加鎖的特點能夠使其讀操作的性能大幅提升。
- 悲觀鎖:悲觀鎖認為一個線程去拿數據時一定會有其他線程對數據進行更改。所以一個線程在拿數據的時候都會順便加鎖,這樣別的線程此時想拿這個數據就會阻塞。實現:synchronized關鍵字和Lock的實現類加鎖。(Synchronized關鍵字會讓沒有得到鎖資源的線程進入BLOCKED狀態,而后在爭奪到鎖資源后恢復為RUNNABLE狀態,這個過程中涉及到操作系統用戶模式和內核模式的轉換,代價比較高。 盡管Java1.6為Synchronized做了優化,增加了從偏向鎖到輕量級鎖再到重量級鎖的過度,但是在最終轉變為重量級鎖之后,性能仍然較低。)
- 自旋鎖:自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是采用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU。
- 公平鎖:多個線程相互競爭時要排隊,等待線程按照申請鎖的順序來獲取鎖。公平鎖的優點是等待鎖的線程不會餓死。缺點是整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。
- 非公平鎖:多個線程加鎖時直接嘗試獲取鎖,獲取不到才會到等待隊列的隊尾等待,即先插隊再排隊。有可能出現后申請鎖的線程先獲取鎖的場景。非公平鎖的優點是可以減少喚起線程的開銷,整體的吞吐效率高,因為線程有幾率不阻塞直接獲得鎖,CPU不必喚醒所有線程。缺點是處于等待隊列中的線程可能會餓死,或者等很久才會獲得鎖。
- 可重入鎖:可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,再進入該線程的內層方法會自動獲取鎖(前提鎖對象得是同一個對象或者class),不會因為之前已經獲取過還沒釋放而阻塞。優點:避免死鎖
- 分段鎖 :設計目的是細化鎖的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。 實現:CurrentHashMap底層就用了分段鎖。
- 獨享鎖:該鎖一次只能被一個線程所持有。如果線程T對數據A加上排它鎖后,則其他線程不能再對A加任何類型的鎖。獲得排它鎖的線程即能讀數據又能修改數據。
- 共享鎖:該鎖可以被多個線程所持有。如果線程T對數據A加上共享鎖后,則其他線程只能對A再加共享鎖,不能加排它鎖。獲得共享鎖的線程只能讀數據,不能修改數據。
- 互斥鎖:具體實現就是synchronized、ReentrantLock和JUC中Lock的實現類
- 讀寫鎖:ReentrantReadWriteLock中的讀鎖ReadLock是共享鎖,寫鎖WriteLock是獨享鎖。
synchronized和ReentrantLock有什么區別呢?
- 原始構成:synchronized是java關鍵字屬于JVM層面。 monitorenter(底層通過monitor對象來完成,其實wait/notify等方法也依賴于monitor對象只有在同步塊或方法中才能調用wait/notify等方法)。ReentrantLock是具體類(java.util.concurrent.locks.Lock)是API層面。
- 使用方法:synchronized不需要手動釋放鎖,代碼執行完系統會自動讓線程釋放對鎖的占用 ReentrantLock則需要手動去釋放鎖,若沒有主動釋放優肯出現死鎖,也就是lock()和unlock()方法需要配合try/finally語句快來使用
- 等待是否可以中斷:synchronized不可中斷,除非拋出異常或者正常運行完成。ReentrantLock可中斷:
- 設置超時時間 tryLock(long timeout,TimeUnit unit)
- lockInterruptibly()房代碼塊中調用interrupt()方法可中斷
- 枷鎖是否公平:synchronized是非公平鎖,ReentrantLock 兩者都可,默認是非公平鎖(構造方法傳入boolean值,true是公平鎖,false是非公平鎖)
- 鎖綁定多個條件Condition:ReentrantLock 用來實現分組喚醒需要喚醒的線程,可以精確喚醒,而不是像synchronized要么隨機喚醒一個要么全部喚醒
為什么Synchronized能實現線程同步?
synchronized是悲觀鎖,在操作同步資源之前需要給同步資源先加鎖,這把鎖就是存在Java對象頭里的。synchronized通過Monitor來實現線程同步,Monitor是依賴于底層的操作系統的Mutex Lock(互斥鎖)來實現的線程同步。 如同我們在自旋鎖中提到的“阻塞或喚醒一個Java線程需要操作系統切換CPU狀態來完成,這種狀態轉換需要耗費處理器時間。如果同步代碼塊中的內容過于簡單,狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長”。這種方式就是synchronized最初實現同步的方式,這就是JDK 6之前synchronized效率低的原因。這種依賴于操作系統Mutex Lock所實現的鎖我們稱之為“重量級鎖”,JDK 6中為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”。
Ps:
- Java對象頭 :以Hotspot虛擬機為例,Hotspot的對象頭主要包括兩部分數據:Mark Word(標記字段)、Klass Pointer(類型指針)。
- Mark Word:默認存儲對象的HashCode,分代年齡和鎖標志位信息。這些信息都是與對象無關的數據,所以Mark Word被設計成一個非固定的數據結構,以便在極小的空間內存盡量存儲更多的數據。它會根據對象的狀態復用自己的存儲空間,也就是說在運行期間Mark Word里存儲的數據會隨著鎖標志位的變化而變化。
- synchronized的底層實現是完全依賴JVM虛擬機的,所以談synchronized的底層實現,就不得不談數據在JVM內存的存儲:Java對象頭,以及Monitor對象監視器。
- Klass Point:對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
- Monitor:可以理解為一個同步工具或一種同步機制,通常被描述為一個對象。每一個Java對象就有一把看不見的鎖,稱為內部鎖或者Monitor鎖。 Monitor是線程私有的數據結構,每一個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。每一個被鎖住的對象都會和一個monitor關聯,同時monitor中有一個Owner字段存放擁有該鎖的線程的唯一標識,表示該鎖被這個線程占用。
鎖升級過程?
鎖升級過程是由無鎖,偏向鎖,輕量級鎖,到重量級鎖的過程。多個線程在爭搶synchronized 鎖時,在某些情況下,會由無鎖狀態一步步升級為最終的重量級鎖狀態。整個升級過程大致包括如下幾個步驟:
- 線程在競爭 synchronized 鎖時,JVM 首先會檢測鎖對象的 Mark word 中偏向鎖鎖標記位是否為 1,鎖標記位是否為 01,如果兩個條件都滿足,則當前鎖處于可偏向的狀態。
- 爭搶 synchronized 鎖的線程檢查鎖對象的 Mark Word 中存儲的線程 ID 是否是自己的線程 ID ,如果是自己的線程 ID,則表示處于偏向鎖狀態。當前線程可以,直接進入方法或者代碼塊執行邏輯。
- 如果鎖對象的 Mark word 中存儲的不是當前線程的 ID,則當前線程會通過 CAS 自旋的方式競爭鎖資源。如果成功搶占到鎖,則將 Mark Word 中存儲的線程 ID 修改為自己的線程 ID ,將偏向鎖標記設置為 1,鎖標志位設置為 01,當前鎖處于偏向鎖狀態。
- 如果當前線程通過 CAS 自旋操作競爭鎖失敗,則說明此時有其他線程也在爭搶鎖資源。此時會撤銷偏向鎖,觸發升級為輕量級鎖的操作。
- 當前線程會根據鎖對象的 Mark word 中存儲的線程 ID 通知對應的線程暫停,對應的線程會將 Mark Word 的內容置空。
- 當前線程與上次獲取到鎖的線程都會把鎖對象的 HashCode 等信息復制到自己的 Displaced Mark Word中,隨后兩個線程都會執行 CAS 自旋操作,嘗試把鎖對象的 Mark Word 中的內容修改為指向自己的 Displaced Mark Word 空間來競爭鎖。
- 競爭鎖成功的線程獲取到鎖,執行方法或代碼塊中的邏輯。同時,競爭鎖成功的線程會將鎖對象的 Mark Word 中的鎖標志位設置為 00,此時進入輕量級鎖狀態。
- 競爭失敗的線程會繼續使用 CAS 自旋的方式嘗試競爭鎖,如果自旋成功競爭到鎖,則當前鎖仍然處于輕量級鎖狀態。
- 如果線程的 CAS 自旋操作達到一定次數仍未獲取到鎖,則輕量級鎖會膨脹為重量級鎖,此時會將鎮對鎖的 Mark Word 中的鎖標志位設置為 10,進人重量級鎖狀態。