從計算機底層來說: 線程可以比作是輕量級的進程,是程序執行的最小單位,線程間的切換和調度的成本遠遠小于進程。另外,多核 CPU 時代意味著多個線程可以同時運行,這減少了線程上下文切換的開銷。
從當代互聯網發展趨勢來說: 現在的系統動不動就要求百萬級甚至千萬級的并發量,而多線程并發編程正是開發高并發系統的基礎,利用好多線程機制可以大大提高系統整體的并發能力以及性能。
總結:并發編程的目的就是為了能提高程序的執行效率提高程序運行速度,充分的利用多核CPU資源。
原子性(Atomicity):在一次或多次操作中,要么所有的操作都執行并且不會受其他因素干擾而中斷,要么所有的操作都不執行
可見性:一個線程對共享變量的修改,其他線程能夠立刻看到。(synchronized,volatile)
有序性:程序執行的順序按照代碼的先后順序執行。(指令重排:處理器為了提高程序運行效率,處理器根據指令之間的數據依賴性,可能會對指令進行重排序,單線程下可以保證程序最終執行結果和代碼順序執行的結果是一致的,但是多線程下有可能出現問題)。
可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
舉個簡單的例子,看下面這段代碼:
//線程1執行的代碼
int i = 0;
i = 10;
//線程2執行的代碼
j = i;
假若執行線程1的是CPU1,執行線程2的是CPU2。由上面的分析可知,當線程1執行 i = 10這句時,會先把i的初始值加載到CPU1的高速緩存中,然后賦值為10,那么在CPU1的高速緩存當中i的值變為10了,卻沒有立即寫入到主存當中。此時線程2執行 j = i,它會先去主存讀取i的值并加載到CPU2的緩存當中,注意此時內存當中i的值還是0,那么就會使得j的值為0,而不是10。線程1對變量i修改了之后,線程2沒有立即看到線程1修改的值。在多線程環境下,一個線程對共享變量的操作對其他線程是不可見的。這就是可見性問題。
對于可見性,Java提供了volatile關鍵字來保證可見性。當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。而普通的共享變量不能保證可見性,因為普通共享變量被修改之后,什么時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證可見性。
另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執行同步代碼,并且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。
即程序執行的順序按照代碼的先后順序執行。
舉個簡單的例子,看下面這段代碼:
int i = 0; ? ? ? ? ? ? ?
boolean flag = false;
i = 1; ? ? ? ? ? ? ? ?//語句1 ?
flag = true; ? ? ? ? ?//語句2
上面代碼定義了一個int型變量,定義了一個boolean類型變量,然后分別對兩個變量進行賦值操作。從代碼順序上看,語句1是在語句2前面的,那么JVM在真正執行這段代碼的時候會保證語句1一定會在語句2前面執行嗎?不一定,為什么呢?這里可能會發生指令重排序(Instruction Reorder)。
下面解釋一下什么是指令重排序,一般來說,處理器為了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先后順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。
比如上面的代碼中,語句1和語句2誰先執行對最終的程序結果并沒有影響,那么就有可能在執行過程中,語句2先執行而語句1后執行。那么它靠什么保證的呢?進行重排序時是會考慮指令之間的數據依賴性。雖然重排序不會影響單個線程內程序執行的結果,但是多線程呢?下面看一個例子:
//線程1:
context = loadContext(); ? //語句1
inited = true; ? ? ? ? ? ? //語句2
//線程2:
while(!inited ){
? ? sleep()
}
doSomethingwithconfig(context);
上面代碼中,由于語句1和語句2沒有數據依賴性,因此可能會被重排序。假如發生了重排序,在線程1執行過程中先執行語句2,而此是線程2會以為初始化工作已經完成,那么就會跳出while循環,去執行doSomethingwithconfig(context)方法,而此時context并沒有被初始化,就會導致程序出錯。
從上面可以看出,在Java內存模型中,允許編譯器和處理器對指令進行重排序,
但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程并發執行的正確性。
你吃飯吃到一半,電話來了,你一直到吃完了以后才去接,這就說明你不支持并發也不支持并行。
你吃飯吃到一半,電話來了,你停了下來接了電話,接完后繼續吃飯,這說明你支持并發。 (不一定是同時的)
你吃飯吃到一半,電話來了,你一邊打電話一邊吃飯,這說明你支持并行。
并發的關鍵是你有處理多個任務的能力,不一定要同時。
并行的關鍵是你有同時處理多個任務的能力。
1)Java 中的線程對應是操作系統級別的線程,線程數量控制不好,頻繁的創建、銷毀線程和線程間的切換,比較消耗內存和時間。
2)容易帶來線程安全問題。如線程的可見性、有序性、原子性問題,會導致程序出現的結果與預期結果不一致。
3)多線程容易造成死鎖、活鎖、線程饑餓等問題。此類問題往往只能通過手動停止線程、甚至是進程才能解決,影響嚴重。
4)對編程人員的技術要求較高,編寫出正確的并發程序并不容易。
5)并發程序易出問題,且難調試和排查;問題常常詭異地出現,又詭異地消失。
Java 5.0 提供了java.util.concurrent(簡稱JUC)包,在此包中增加了在并發編程中很常見的實用工具類,用于定義類似于編程的自定義子系統,包括線程池、異步IO和輕量級任務框架。提供可調的、靈活的線程池。還提供了設計用于多線程上下文的Collection實現等。
JMM其實并不像JVM內存模型一樣是真實存在的,它只是一個抽象的規范。在不同的硬件或者操作系統下,對內存的訪問邏輯都有一定的差異,而這種差異會導致同一套代碼在不同操作系統或者硬件下,得到了不同的結果,而JMM的存在就是為了解決這個問題,通過JMM的規范,保證Java程序在各種平臺下對內存的訪問都能得到一致的效果。
計算機在執行程序的時候,每條指令都是在 CPU 中執行的,而執行的時候,又免不了和數據打交道,而計算機上面的數據,是存放在計算機的物理內存上的。當內存的讀取速度和CPU的執行速度相比差別不大的時候,這樣的機制是沒有任何問題的,可是隨著CPU的技術的發展,CPU的執行速度和內存的讀取速度差距越來越大,導致CPU每次操作內存都要耗費很多等待時間。
為了解決這個問題,初代程序員大佬們想到了一個的辦法,就是在CPU和物理內存上新增高速緩存,這樣程序在運行過程中,會將運算所需要的數據從主內存復制一份到CPU的高速緩存中,當CPU進行計算時就可以直接從高速緩存中讀數據和寫數據了,當運算結束再將數據刷新到主內存就可以了。
隨著時代的變遷,CPU開始出現了多核的概念,每個核都有一套自己的緩存,并且隨著計算機能力不斷提升,還開始支持多線程,最終演變成多個線程訪問進程中的某個共享內存,且這多個線程分別在不同的核心上執行,則每個核心都會在各自的 Cache 中保留一份共享內存的緩沖,我們知道多核是可以并行的,這樣就會出現多個線程同時寫各自的緩存的情況,導致各自的 Cache 之間的數據可能不同。
總結下來就是:在多核 CPU 中,每個核的自己的緩存,關于同一個數據的緩存內容可能不一致。
重排序指的是在執行程序時,為了提高性能,從源代碼到最終執行指令的過程中,編譯器和處理器會對指令進行重排的一種手段。
下圖為從源代碼到最終指令示意圖
重排序的分為3種
1)編譯器優化的重排序:編譯器在不改變單線程程序語義(as-if-serial)的的前提下,可以重新安排語句的執行順序。
2)指令級并行的重排序:現在處理器采用指令級并行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
3)內存系統的重排序:由于處理器使用了存儲和讀寫緩沖區,這使得加載和存儲操作看上去亂序執行。
1.編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
2.指令級并行的重排序。現代處理器采用了指令級并行技術(ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
3.內存系統的重排序。由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。
4.這些重排序對于單線程沒問題,但是多線程都可能會導致多線程程序出現內存可見性問題。
數據依賴性:編譯器和處理器在重排序時,針對單個處理器中執行的指令序列和單個線程中執行的操作會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關系的兩個操作的執行順序。
遵守as-if-serial 語義:不管編譯器和處理器為了提高并行度怎么重排序,(單線程)程序的執行結果不能被改變。
區別:
as-if-serial定義:無論編譯器和處理器如何進行重排序,單線程程序的執行結果不會改變。
happens-before定義:一個操作happens-before另一個操作,表示第一個的操作結果對第二個操作可見,并且第一個操作的執行順序也在第二個操作之前。但這并不意味著Java虛擬機必須按照這個順序來執行程序。如果重排序的后的執行結果與按happens-before關系執行的結果一致,Java虛擬機也會允許重排序的發生。
happens-before關系保證了同步的多線程程序的執行結果不被改變,as-if-serial保證了單線程內程序的執行結果不被改變。
相同點:happens-before和as-if-serial的作用都是在不改變程序執行結果的前提下,提高程序執行的并行度。
不可變對象即對象一旦被創建,它的狀態(對象屬性值)就不能改變。
不可變對象的類即為不可變類。Java 平臺類庫中包含許多不可變類,如 String、基本類型的包裝類、BigInteger 和 BigDecimal 等。
不可變對象保證了對象的內存可見性,對不可變對象的讀取不需要進行額外的同步手段,提升了代碼執行效率。
1.保證變量寫操作的可見性;
2.保證變量前后代碼的執行順序;
不能。volatile不能保證原子性,只能保證線程可見性,可見性表現在當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。
從實踐角度而言,volatile的一個重要作用就是和CAS結合,保證了原子性,詳細的可以參見java.util.concurrent.atomic包下的類,比如AtomicInteger。
被volatile修飾的變量被修改時,會將修改后的變量直接寫入主存中,并且將其他線程中該變量的緩存置為無效,從而讓其它線程對該變量的引用直接從主存中獲取數據,這樣就保證了變量的可見性。
但是volatile修飾的變量在自增時由于該操作分為讀寫兩個步驟,所以當一個線程的讀操作被阻塞時,另一個線程同時也進行了自增操作,此時由于第一個線程的寫操作沒有進行所以主存中仍舊是之前的原數據,所以當兩個線程自增完成后,該變量可能只加了1。因而volatile是無法保證對變量的任何操作都是原子性的。
能,Java 中可以創建 volatile 類型數組,但如果多個線程改變引用指向的數組,將會受到 volatile 的保護,如果多個線程改變數組的元素內容,volatile 標示符就不能起到之前的保護作用了。
volatile變量可以確保先行關系,即寫操作會發生在后續的讀操作之前,但它并不能保證原子性。例如用volatile修飾count變量那么count++操作就不是原子性的。
而AtomicInteger類提供的atomic方法可以讓這種操作具有原子性如getAndIncrement( )方法會原子性的進行增量操作把當前值加- ,其它數據類型和引用變量也可以進行相似操作。
原子操作是指不會被線程調度機制打斷的操作,這種操作一旦開始,就一直運行到結束,中間不會有任何線程上下文切換。原子操作可以是一個步驟,也可以是多個操作步驟,但是其順序不可以被打亂,也不可以被切割而只執行其中的一部分,將整個操作視作一個整體是原子性的核心特征。
而 java.util.concurrent.atomic 下的類,就是具有原子性的類,可以原子性地執行添加、遞增、遞減等操作。比如之前多線程下的線程不安全的 i++ 問題,到了原子類這里,就可以用功能相同且線程安全的 getAndIncrement 方法來優雅地解決。
原子類的作用和鎖有類似之處,是為了保證并發情況下線程安全。不過原子類相比于鎖,有一定的優勢:
粒度更細:原子變量可以把競爭范圍縮小到變量級別,通常情況下,鎖的粒度都要大于原子變量的粒度。
效率更高:除了高度競爭的情況之外,使用原子類的效率通常會比使用同步互斥鎖的效率更高,因為原子類底層利用了 CAS 操作,不會阻塞線程。原子類的作用和鎖有類似之處,是為了保證并發情況下線程安全。不過原子類相比于鎖,有一定的優勢:
粒度更細:原子變量可以把競爭范圍縮小到變量級別,通常情況下,鎖的粒度都要大于原子變量的粒度。
效率更高:除了高度競爭的情況之外,使用原子類的效率通常會比使用同步互斥鎖的效率更高,因為原子類底層利用了 CAS 操作,不會阻塞線程。
AtomicInteger與AtomicLong:它們的底層實現使用了CAS鎖,不同點在于AtomicInteger包裝了一個Integer型變量,而AtomicLong包裝了一個Long型變量。
LongAdder:它的底層實現是分段鎖+CAS鎖。
atomic代表的是concurrent包下Atomic開頭的類,如AtomicBoolean、AtomicInteger、AtomicLong等都是用原子的方式來實現指定類型的值的更新,它的底層通過CAS原理解決并發情況下原子性的問題,在jdk中CAS是Unsafe類中的api來實現的。
CAS,全稱為Compare and Swap,即比較-替換,實現并發算法時常用到的一種技術。假設有三個操作數:內存值V、舊的預期值A、要修改的值B,當且僅當預期值A和內存值V相同時,才會將內存值修改為B并返回true,否則什么都不做并返回false。當然CAS一定要volatile變量配合,這樣才能保證每次拿到的變量是主內存中最新的那個值,否則舊的預期值A對某條線程來說,永遠是一個不會變的值A,只要某次CAS操作失敗,永遠都不可能成功。
以AtomicInteger為例,說明CAS的使用與原理。首先atomicIngeter初始化為5,調用對象的compareAndSet方法來對比當前值與內存中的值,是否相等,相等則更新為2019,不相等則不會更新,compareAndSet方法返回的是boolean類型。
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,2019)+" \t current "+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5,2014)+" \t current "+atomicInteger.get());
}
}
分析:第一次調用,內存中的值是5,通過對比相等更新為2019,輸出 true current 2019,第二次調用時,內存重點的值已經更新為2019,不相等不更新內存中的值,輸出 false current 2019。
1)CAS存在一個很明顯的問題,即ABA問題。
如果變量V初次讀取的時候是A,并且在準備賦值的時候檢查到它仍然是A,那能說明它的值沒有被其他線程修改過了嗎?如果在這段期間曾經被改成B,然后又改回A,那CAS操作就會誤認為它從來沒有被修改過。針對這種情況,java并發包中提供了一個帶有標記的原子引用類AtomicStampedReference,它可以通過控制變量值的版本來保證CAS的正確性。
2)只能保證一個共享變量的原子性。當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無 法保證操作的原子性,這個時候就可以使用鎖來保證原子性。
多線程訪問共享資源的時候,避免不了資源競爭而導致數據錯亂的問題,所以我們通常為了解決這一問題,都會在訪問共享資源之前加鎖。不同種類有不同的成本開銷,不同的鎖適用于不同的場景。
從資源已被鎖定,線程是否阻塞可以分為 自旋鎖(spinlock)和互斥鎖(mutexlock);
從線程是否需要對資源加鎖可以分為 悲觀鎖 和 樂觀鎖;
從多個線程并發訪問資源,也就是 Synchronized 可以分為 無鎖、偏向鎖、 輕量級鎖 和 重量級鎖;
從鎖的公平性進行區分,可以分為公平鎖 和 非公平鎖;
從根據鎖是否重復獲取可以分為可重入鎖(自己獲得鎖以后,自己還可以進入鎖之中) 和 不可重入鎖;
從那個多個線程能否獲取同一把鎖分為共享鎖和 排他鎖;
互斥鎖是在訪問共享資源之前對其進行加鎖操作,在訪問完成之后進行解鎖操作。加鎖后,任何其它試圖再次加鎖的線程都會被阻塞,直到當前線程解鎖。在這種方式下,只有一個線程能夠訪問被互斥鎖保護的資源。如synchronized/Lock 這些方式都是互斥鎖,不同線程不能同時進入 synchronized Lock 設定鎖的范圍
自旋鎖是一種特殊的互斥鎖,當資源被加鎖后,其它線程想要再次加鎖,此時該線程不會被阻塞睡眠而是陷入循環等待狀態(CPU不能做其它事情),循環檢查資源持有者是否已經釋放了資源,這樣做的好處是減少了線程從睡眠到喚醒的資源消耗,但會一直占用CPU的資源。
區別:互斥鎖的起始開銷要高于自旋鎖,但是基本上是一勞永逸,臨界區持鎖時間的大小并不會對互斥鎖的開銷造成影響,而自旋鎖是死循環檢測,加鎖全程消耗cpu,起始開銷雖然低于互斥鎖,但是隨著持鎖時間,加鎖的開銷是線性增長。
讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。
ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現了這個接口。可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。
讀寫鎖也叫共享鎖。其共享是在讀數據的時候,可以讓多個線程同時進行讀操作的。在寫的時候具有排他性,其他讀或者寫操作都要被阻塞。
1. 悲觀鎖
線程對一個共享變量進行訪問,它就自動加鎖,所以只能有一個線程訪問它
悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時數據正確。
缺點:只有一個線程對它操作時,沒有必要加鎖,造成了性能浪費
2.樂觀鎖
線程訪問共享變量時不加鎖,當執行完后,同步值到內存時,使用舊值和內存中的值進行判斷,如果相同,那么寫入,如果不相同,重新使用新值執行。樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其讀操作的性能大幅提升。
缺點:值相同的情況,可能被其他線程執行過;操作變量頻繁時,重新執行次數多,造成性能浪費;完成比較后,寫入前,被其他線程修改了值,導致不同步問題
1)synchronized 同步語句塊的情況
public class SynchronizedDemo {
?? ?public void method() {
?? ??? ?synchronized (this) {
?? ??? ??? ?System.out.println("synchronized 代碼塊");
?? ??? ?}
?? ?}
}
通過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關字節碼信息:首先切換到類的對應目錄執行 javac SynchronizedDemo.java 命令生成編譯后的 .class 文件,然后執行javap -c -s -v -l SynchronizedDemo.class。
從上面我們可以看出:synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置。當執行 monitorenter 指令時,線程試圖獲取鎖也就是獲取 對象監視器 monitor 的持有權。
在 Java 虛擬機(HotSpot)中,Monitor 是基于 C++實現的,由ObjectMonitor實現的。每個對象中都內置了一個 ObjectMonitor對象。另外,wait/notify等方法也依賴于monitor對象,這就是為什么只有在同步的塊或者方法中才能調用wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException的異常的原因。
在執行monitorenter時,會嘗試獲取對象的鎖,如果鎖的計數器為 0 則表示鎖可以被獲取,獲取后將鎖計數器設為 1 也就是加 1。
在執行 monitorexit 指令后,將鎖計數器設為 0,表明鎖被釋放。如果獲取對象鎖失敗,那當前線程就要阻塞等待,直到鎖被另外一個線程釋放為止。
2) synchronized 修飾方法的的情況
public class SynchronizedDemo2 {
?? ?public synchronized void method() {
?? ??? ?System.out.println("synchronized 方法");
?? ?}
}
通過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關字節碼信息:首先切換到類的對應目錄執行 javac SynchronizedDemo2.java 命令生成編譯后的 .class 文件,然后執行javap -c -s -v -l SynchronizedDemo2.class。
synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是 ACC_SYNCHRONIZED 標識,該標識指明了該方法是一個同步方法。JVM 通過該 ACC_SYNCHRONIZED 訪問標志來辨別一個方法是否聲明為同步方法,從而執行相應的同步調用。
public class Singleton {
// 這里為什么需要加上volatile 后面會講解
private volatile static Singleton uniqueInstance;
// 私有化構造方法
private Singleton() {
}
// 提供getInstance方法
public static Singleton getInstance() {
//先判斷對象是否已經實例過,沒有實例化過才進入加鎖代碼
if (uniqueInstance == null) {
//類對象加鎖
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
其中uniqueInstance 變量采用 volatile 關鍵字修飾,分析如下:
uniqueInstance = new Singleton(); 這段代碼其實是分為三步執行:
1.為 uniqueInstance 分配內存空間
2.初始化 uniqueInstance
3.將 uniqueInstance 指向分配的內存地址
但是由于 JVM 具有指令重排的特性,執行順序有可能變成 1->3->2。指令重排在單線程環境下不會出現問題,但是在多線程環境下會導致一個線程獲得還沒有初始化的實例。例如,線程 T1 執行了 1 和 3,此時 T2 調用 getUniqueInstance() 后發現 uniqueInstance 不為空,因此返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。
可重入原理即加鎖次數計數器。一個線程拿到鎖之后,可以繼續地持有鎖,如果想再次進入由這把鎖控制的方法,那么它可以直接進入。它的原理是利用加鎖次數計數器來實現的。
1.每重入一次,計數器+1
每個對象自動含有一把鎖,JVM負責跟蹤對象被加鎖的次數。
線程第一次給對象加鎖的時候,計數器=0+1=1,每當這個相同的線程在此對象上再次獲得鎖時,計數器再+1。只有首先獲取這把鎖的線程,才能繼續在這個對象上多次地獲取這把鎖
2.計數器-1
每當任務結束離開時,計數遞減,當計數器減為0,鎖被完全釋放。
利用這個計數器可以得知這把鎖是被當前多次持有,還是如果=0的話就是完全釋放了。
不能。其它線程只能訪問該對象的非同步方法,同步方法則不能進入。因為非靜態方法上的 synchronized 修飾符要求執行方法時要獲得對象的鎖,如果已經進入A 方法說明對象鎖已經被取走,那么試圖進入 B 方法的線程就只能在等鎖池(注意不是等待池哦)中等待對象的鎖。
1)lock是一個接口,而synchronized是java的一個關鍵字。
2)synchronized在發生異常時會自動釋放占有的鎖,因此不會出現死鎖;而lock發生異常時,不會主動釋放占有的鎖,必須手動來釋放鎖,可能引起死鎖的發生。
synchronized是和if、else、for、while一樣的關鍵字,ReentrantLock是類,這是二者的本質區別。既然ReentrantLock是類,那么它就提供了比synchronized更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變量,ReentrantLock比synchronized的擴展性體現在幾點上:
(1)ReentrantLock可以對獲取鎖的等待時間進行設置,這樣就避免了死鎖
(2)ReentrantLock可以獲取各種鎖的信息
(3)ReentrantLock可以靈活地實現多路通知
另外,二者的鎖機制其實也是不一樣的。ReentrantLock底層調用的是Unsafe的park方法加鎖,synchronized操作的應該是對象頭中mark word,這點我不能確定。
1)synchronized保證內存可見性和操作的原子性
2)volatile只能保證內存可見性
3)volatile不需要加鎖,比Synchronized更輕量級,并不會阻塞線程(volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞。)
4)volatile標記的變量不會被編譯器優化,而synchronized標記的變量可以被編譯器優化(如編譯器重排序的優化).
5)volatile是變量修飾符,僅能用于變量,而synchronized是一個方法或塊的修飾符。
volatile本質是在告訴JVM當前變量在寄存器中的值是不確定的,使用前,需要先從主存中讀取,因此可以實現可見性。而對n=n+1,n++等操作時,volatile關鍵字將失效,不能起到像synchronized一樣的線程同步(原子性)的效果。
1. volatile 修飾變量
2. synchronized 修飾修改變量的方法
3. wait/notify
4. 輪詢
監視器和鎖在Java虛擬機中是一塊使用的。監視器監視一塊同步代碼塊,確保一次只有一個線程執行同步代碼塊。每一個監視器都和一個對象引用相關聯。線程在獲取鎖之前不允許執行同步代碼。
在 java 虛擬機中, 每個對象( Object 和 class )通過某種邏輯關聯監視器,每個監視器和一個對象引用相關聯, 為了實現監視器的互斥功能, 每個對象都關聯著一把鎖.一旦方法或者代碼塊被 synchronized 修飾, 那么這個部分就放入了監視器的監視區域, 確保一次只能有一個線程執行該部分的代碼, 線程在獲取鎖之前不允許執行該部分的代碼。另外 java 還提供了顯式監視器( Lock )和隱式監視器( synchronized )兩種鎖方案。
死鎖 : 指多個線程在運行過程中因爭奪資源而造成的一種僵局。比如有一個線程A,按照先鎖a再獲得鎖b的的順序獲得鎖,而在此同時又有另外一個線程B,按照先鎖b再鎖a的順序獲得鎖。
死鎖發生的必要條件
(1) 互斥,同一時刻只能有一個線程訪問。
(2) 持有且等待,當線程持有資源A時,再去競爭資源B并不會釋放資源A。
(3) 不可搶占,線程T1占有資源A,其他線程不能強制搶占。
(4) 循環等待,線程T1占有資源A,再去搶占資源B如果沒有搶占到會一直等待下去。
想要破壞死鎖那么上訴條件只要不滿足一個即可,那么分析如下
(1) 互斥條件,不可破壞,如果破壞那么并發安全就不存在了。
(2) 持有且等待,可以破壞,可以一次性申請所有的資源。
(3) 不可搶占,當線程T1持有資源A再次獲取資源B時,發現資源B被占用那么主動釋放資源A。
(4) 循環等待,可以將資源排序,可以按照排序順序的資源申請,這樣就不會存在環形資源申請了。
活鎖:是指線程1可以使用資源,但它很禮貌,讓其他線程先使用資源,線程2也可以使用資源,但它很紳士,也讓其他線程先使用資源。這樣你讓我,我讓你,最后兩個線程都無法使用資源。
就類似馬路中間有條小橋,只能容納一輛車經過,橋兩頭開來兩輛車A和B,A比較禮貌,示意B先過,B也比較禮貌,示意A先過,結果兩人一直謙讓誰也過不去。
饑餓:是指如果線程T1占用了資源R,線程T2又請求封鎖R,于是T2等待。T3也請求資源R,當T1釋放了R上的封鎖后,系統首先批準了T3的請求,T2仍然等待。然后T4又請求封鎖R,當T3釋放了R上的封鎖之后,系統又批準了T4的請求…,T2可能永遠等待。
類似有兩條道A和B上都堵滿了車輛,其中A道堵的時間最長,B相對相對堵的時間較短,這時,前面道路已疏通,交警按照最佳分配原則,示意B道上車輛先過,B道路上過了一輛又一輛,A道上排隊時間最長的確沒法通過,只能等B道上沒有車輛通過的時候再等交警發指令讓A道依次通過。
活鎖和死鎖類似,不同之處在于處于活鎖的線程或進程的狀態是不斷改變的,活鎖可以認為是一種特殊的饑餓。一個現實的活鎖例子是兩個人在狹小的走廊碰到,兩個人都試著避讓對方好讓彼此通過,但是因為避讓的方向都一樣導致最后誰都不能通過走廊。簡單的說就是,活鎖和死鎖的主要區別是前者進程的狀態可以改變但是卻不能繼續執行。
饑餓與死鎖有一定聯系:二者都是由于競爭資源而引起的,但又有明顯差別,主要表現在如下幾個方面:
(1)從進程狀態考慮,死鎖進程都處于等待狀態,忙式等待(處于運行或就緒狀態)的進程并非處于等待狀態,但卻可能被餓死;
(2)死鎖進程等待永遠不會被釋放的資源,餓死進程等待會被釋放但卻不會分配給自己的資源,表現為等待時限沒有上界(排隊等待或忙式等待);
(3)死鎖一定發生了循環等待,而餓死則不然。這也表明通過資源分配圖可以檢測死鎖存在與否,但卻不能檢測是否有進程餓死;
(4)死鎖一定涉及多個進程,而饑餓或被餓死的進程可能只有一個。饑餓和餓死與資源分配策略有關,因而防止饑餓與餓死可從公平性考慮,確保所有進程不被忽視,如FCFS分配算法。