鎖的對比
java中的鎖一共有4種狀態(tài),級別從低到高分別是:
- 無鎖狀態(tài)
- 偏向鎖
- 輕量級鎖
- 重量級鎖
鎖只能升級,不能降級
偏向鎖
顧名思義,為了讓線程獲得鎖的代價更低,引入了偏向鎖。
加鎖
當一個線程訪問同步塊并且獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程id,這樣,這個線程便獲取了這個對象的偏向鎖,之后這個線程進入和退出就不需要通過CAS操作,也就是原子操作,來進行加鎖和解鎖,只需要簡單的測試下對象頭存儲的偏向鎖的線程id是否和自身的id一致,如果一致,那么已經獲取鎖,直接進入。否則,判斷對象中是否已經存儲了偏向鎖,如果沒有鎖,那么使用CAS競爭鎖,如果設置了,那么嘗試使用CAS將對象頭的偏向鎖指向當前線程。
解鎖
偏向鎖的解鎖時機是在競爭時才會釋放鎖,撤銷時需要等待全局安全點,這個時間點沒有正在執(zhí)行的字節(jié)碼,首先會暫停擁有偏向鎖的線程,然后檢查偏向鎖的線程是否存活,如果不活動,那么直接設置為無鎖狀態(tài)。否則要么偏向其他鎖,要么恢復到無鎖或者標記對象不適合偏向鎖。
輕量鎖
會自旋嘗試獲取鎖,消耗cpu資源
加鎖
一旦多線程發(fā)起了鎖競爭,并且釋放了偏向鎖之后,線程通過CAS修改Mark Word,如果當前沒有對象持有同步體的鎖,那么直接將同步體的鎖修改的輕量鎖,否則,該線程將自旋獲取鎖,直到膨脹為重量級鎖,修改同步體的Mark Word為重量級鎖,然后阻塞
解鎖
一旦有其他線程因想獲取當前鎖而膨脹為重量級鎖,那么這個線程將會通過CAS替換Mark Word,然后失敗,解鎖,并且喚醒其他等待線程。
重量級鎖
會阻塞,不消耗cpu資源,但是響應時間較慢
原子操作
處理器實現(xiàn)原子操作
總線鎖
通過總線鎖保證只有單一處理器占有共享內存
緩存鎖
只對某個內存地址進行加鎖,保證其他的內存地址不受影響。
java實現(xiàn)原子操作
循環(huán)使用CAS操作
需要解決以下三個問題
- ABA問題
如果一個值最開始為1,中間修改為2,最后再修改為1,其實這個值是修改過的,所以只通過基本的CAS操作是有問題的,這個時候就需要通過引用來處理。 - 耗時
通過pause指令解決 - 只能保證一個共享變量
將多個共享變量合成一個共享變量
鎖機制也是通過循環(huán)CAS操作
volatile原理
在jvm內存模型上,即每一個線程都持有一份對于內存的拷貝,volatile保證了對于一個volatile變量的讀,總能看到任意線程對這個volatile變量最后的寫入。
我們可以這么理解,對一個volatile變量,它的set和get都將加鎖,只要是一個volatile變量,對該變量的讀寫就具有原子性,但是復合操作,比如說volatile操作整體上不具有原子性。
- 可見性:對于java內存模型,每一個線程都擁有一個主存的副本,在一個共享變量被某個線程修改之后,將會將其他線程中內存的副本標志為無效,強制從主存中取值。
- 原子性:對于被volatile修飾的變量,其get和set操作都加了鎖,
- 不可重排:對于被volatile修飾的變量,保證編譯器不會對一些代碼時序做重排
synchronized
內部也是利用了鎖。
每一個對象都有一個自己的monitor,必須先獲取這個monitor對象才能夠進入同步塊或同步方法,而這個monitor對象的獲取是排他的,也就是同一時刻只能有一個線程獲取到這個monitor
修飾同步塊
主要通過monitorenter和monitorexit指令來進行獲取
修飾方法
主要是通過修飾符ACC_SYMCHRONIZED來修飾
如果獲取對象的monitor失敗,線程將會切換為BLOCK狀態(tài),
synchronized作為java中用得最廣泛的同步關鍵字。實際上內部實現(xiàn)的依靠著兩個關鍵的命令,monitorenter和monitorexit。synchronized同樣是通過互斥量和臨界區(qū)來實現(xiàn)。monitorenter指令表示進入臨界區(qū),互斥量+1,monitorexit表示退出臨界區(qū),互斥量減一。在jdk1.5之前,synchroize操作都是重操作,所加的鎖都是自旋鎖。即,線程在執(zhí)行了monitorenter時,發(fā)現(xiàn)互斥量已經為0了,則必須在外面循環(huán)等待,而不會進行阻塞,加入等待隊列。而在jdk1.6之后,對synchronized做了很多優(yōu)化操作,鎖粗化,鎖消除,輕量級鎖,偏向鎖,適應性自旋鎖等來減少操作的開銷。下面我們分別來介紹幾個優(yōu)化操作。
- 鎖膨脹(Lock Coarsening):如果系統(tǒng)檢測到有對同一個對象反復加鎖或者解鎖,那么系統(tǒng)在保證加鎖的正確性之后,會將鎖擴張,保證不必要的加鎖操作和解鎖操作。
- 鎖削除(Lock Elimination):在虛擬機運行時,通過JIT編譯器的逃逸分析檢測出部分上鎖的代碼不可能出現(xiàn)數據多線程數據共享的狀態(tài),那么自然會將這個鎖給去除。
- 輕量級鎖(Lightweight Locking):輕量級鎖是相對于重量級鎖來說的,它的本意是在沒有多線程競爭的情況下,減少重量級鎖使用的互斥量產生的性能消耗,輕量級鎖不用使用操作系統(tǒng)的互斥量,僅僅只是在hotspot對象頭記錄,在這里,我們必須先介紹一下Hotspot的對象頭,其中又個markWork,用來記錄當前的鎖狀態(tài):
01: 表示未鎖定
00: 表示輕量級鎖定
10: 表示重量級鎖定 - 偏向鎖:偏向第一個持有鎖的對象,一旦持有,則不會再進行同步。直到有下一個線程嘗試獲取這個鎖。
- 適應性自旋鎖:在線程首次嘗試輕量級鎖失敗時,會進入忙等待一段時間,然后再次重試,在重試了一定次數失敗之后,就會進入阻塞態(tài)。
和lock的區(qū)別
synchronized將會隱式的獲取鎖,但是它把鎖給固化了,沒有辦法比較靈活的利用鎖,比如說非阻塞式的獲取鎖,超時獲取鎖等等。
synchronized和lock方式兩種加鎖的方式的最底層實現(xiàn)是一致的,都是通過臨界區(qū)的互斥量來判斷的。只是synchronized加鎖及解鎖操作都是由jvm為我們實現(xiàn)好了。而lock加鎖方式是基于我們代碼的實現(xiàn),我們必須要手動的解鎖。這就讓lock方式能夠有一些相對于synchronized有一些優(yōu)點了。
提供了tryLock方法,可以讓我們提前預知能否加鎖而不會讓該線程進入阻塞狀態(tài)。synchronized加鎖之后,一旦不能加鎖便會進入阻塞狀態(tài)。
lockInterruptibly方法:提供了立即響應線程中斷的方法,通常來說,線程并不會是在任何狀態(tài)下都會被立即中斷的,只有在線程處于阻塞狀態(tài)才會被立即中斷。線程在獲取鎖的時候,會處于一種不可中斷的狀態(tài),比如synchonized或者單純的lock操作就是這樣的。
提供了獲取鎖超時的返回選項,對于競爭激烈的臨界區(qū),這種機制顯得格外重要。這個機制也是通過tryLock來實現(xiàn),多加了一個超時參數判斷。
在我們代碼的實現(xiàn)要求中,假如沒有明確的要求,我們可以利用synchronized關鍵字,sunchronized關鍵字在競爭不是很激烈的情況下,性能要優(yōu)于lock操作,但是在競爭激烈的情況下,性能將會下降特別多。
隊列同步器 AbstractQueuedSynchronizer
提供了3類模版方法:
- 獨占式獲取與釋放同步狀態(tài)
- 共享式獲取與釋放同步狀態(tài)
- 查詢同步隊列的等待線程
通過下面3個方法來訪問或修改同步狀態(tài):
- getState
- setState
- compareAndSetState
同步器內部依賴于同步隊列來完成同步狀態(tài)的管理,當前線程獲取同步狀態(tài)失敗時,同步器將會把這個線程構造成一個node添加到同步隊列,并且阻塞線程,當同步狀態(tài)釋放時,將會把首節(jié)點的線程喚醒,讓其再次嘗試獲取同步狀態(tài)。 對于入隊操作,同樣也需要CAS操作進行入隊
- 可重入鎖: 線程可以進入它已經加上鎖的同步代碼塊。即假如這個線程在某個位置加上可重入鎖,在后面這個線程仍然可以再次加鎖。進入同樣的同步代碼塊,也就是臨界區(qū)。
- 不可重入鎖: 一旦線程在某個地方加上鎖了,在沒解鎖前,這個鎖就是該線程唯一一次進入該臨界區(qū)的通道。而且,在這個臨界區(qū)中,有且僅能有一個這樣的鎖。
重入鎖
ReetrantLock,顧名思義,它表示該鎖能夠支持一個線程對資源的重復加鎖。
symchronized默認支持的是可重入鎖。
java中鎖主要是在util.concurrent.locks包下,最常用的就是ReentrantLock類,這個鎖被成為可重入鎖。既然有可重入鎖,也就會有不可重入鎖,這種鎖的區(qū)別主要是針對于同一個線程來說的,下面簡單聊聊這兩種鎖的區(qū)別:
在java中,ReentrantLock和synchronized所加上的鎖都是可以重入.
在介紹ReentrantLock之前,我們必須先了解其內部實現(xiàn)的兩種鎖,公平鎖和非公平鎖。
- 公平鎖: 所有的嘗試加鎖的對象,都會添加到一個隊列中,先來的排在隊列前頭,到時候可以加鎖時,從隊首取第一個加鎖對象。
- 非公平鎖: 所有嘗試加鎖的對象能夠加上鎖的順序不能保證,隨機。
我們先來看看ReentrantLock對象的創(chuàng)建:
Lock fairLock = new ReentrantLock(true); //創(chuàng)建公平鎖
Lock noFairLock = new Reentrantlock(); //創(chuàng)建非公平鎖
=====>兩種構造函數的實現(xiàn)
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
{%endcodeblock%}
我們先只針對于非公平鎖來分析,我們平時調用這個鎖并不會特意的指定鎖類型。對于加鎖,我們只需要調用lock方法即可。
{%codeblock%}
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
{%endcodeblock%}
compareAndSetState中,0時我們期待的值,1是我們現(xiàn)在想要更新的值,這個值表示的是當前臨界區(qū)的互斥量。加入返回true的話,表示當前沒有線程給這個臨界區(qū)上鎖,則直接上鎖,設置持有線程,否則,現(xiàn)在已經有鎖了,調用acquire(1).
{%codeblock%}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
========>添加隊列節(jié)點
private Node addWaiter(Node mode) {
//新建隊列節(jié)點,節(jié)點是需要記錄線程的
Node node = new Node(Thread.currentThread(), mode);
//獲取尾部元素
Node pred = tail;
if (pred != null) {
//插入在尾部后面
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//加入沒有尾部元素,執(zhí)行enq操作,
enq(node);
return node;
}
==========>
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
//沒有元素,插入頭部
if (compareAndSetHead(new Node()))
tail = head;
} else {
//有元素,插入尾部
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
=========> 利用斷言,判斷是否能夠加鎖
public boolean tryAcquire(long acquires) {
assertEquals(LOCKED, acquires);
return compareAndSetState(UNLOCKED, LOCKED);
}
=======> 在嘗試獲取鎖失敗之后,會添加到等待隊列中
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//找到前一個節(jié)點
final Node p = node.predecessor();
//如果這個節(jié)點是頭節(jié)點的話,那么在再次嘗試進入臨界區(qū)
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//shouldParkAfterFailedAcquire返回這個線程是否需要阻塞等待,
//parkAndCheckInterrupt()用于阻塞這個線程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
通過上面這個加鎖操作,我們可以了解當一個線程嘗試加鎖失敗時,會判斷是否需要阻塞,如果需要阻塞,則直接阻塞該線程.
上面操作,是直接執(zhí)行l(wèi)ock方法,我們可以先嘗試執(zhí)行tryLock方法:
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
=======>嘗試獲取非公平鎖,嘗試加鎖,加鎖是以線程的基礎
final boolean nonfairTryAcquire(int acquires) {
//獲取當前線程
final Thread current = Thread.currentThread();
//state表示獲取的當前狀態(tài)的互斥量值
int c = getState();
if (c == 0) {
//為0表示沒上鎖,重新嘗試進入臨界區(qū)
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//當前的線程與以上鎖的線程是同一個,可重入,計算同一個計算上鎖個數
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
下面我們來看看解鎖操作:
public void unlock() {
sync.release(1);
}
=======>
public final boolean release(long arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
======>在tryRelease中利用斷言解鎖,假如現(xiàn)在沒有鎖,那么則會拋異常
public boolean tryRelease(long releases) {
if (getState() != LOCKED) throw new IllegalMonitorStateException();
setState(UNLOCKED);
return true;
}
======>unparkSuccessor(h)讓隊列頭進入臨界區(qū)
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//尋找第一個合法的等待節(jié)點
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != null && p != node; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
//打斷阻塞,進入臨界區(qū)
LockSupport.unpark(s.thread);
}
加鎖
如果當前對象沒有加鎖,那么直接加鎖,如果已經加鎖,判斷之前加鎖的線程與當前要加鎖線程是否是同一個,如果是,那么再次獲取。
解鎖
如果一個線程獲取了n次鎖,那么就需要解鎖n次。
讀寫鎖
在同一時刻允許多個讀線程訪問,但是在寫線程訪問時,所有的讀線程和其他寫線程都被阻塞。其實內部是維護了一對鎖