鎖的分類
悲觀鎖和樂觀鎖
在Java里使用的各種鎖,幾乎全都是悲觀鎖。synchronized從偏向鎖、輕量級鎖到重量級鎖,全是悲觀鎖。JDK提供的Lock實現類全是悲觀鎖。其實只要有“鎖對象”出現,那么就一定是悲觀鎖。因為樂觀鎖不是鎖,而是一個在循環里嘗試CAS的算法。
樂觀鎖是atomic包下的原子類。
公平鎖、非公平鎖
多個線程申請一把公平鎖,那么當鎖釋放的時候,先申請的先得到,非常公平。顯然如果是非公平鎖,后申請的線程可能先獲取到鎖,是隨機或者按照其他優先級排序的。
公平鎖與非公平鎖的區別在代碼中的區別為:
final void lock() {
if (this.compareAndSetState(0, 1)) {
this.setExclusiveOwnerThread(Thread.currentThread());
} else {
this.acquire(1);
}
}
就是非公平鎖在調用lock()時,先嘗試插隊直接去拿鎖,更改state狀態為1,如果成功則把Owner線程設置為當前線程,則表示成功獲得鎖,沒獲得鎖則acquire,而公平鎖直接acquire。
偏向鎖 → 輕量級鎖 → 重量級鎖
初次執行到synchronized代碼塊的時候,鎖對象變成偏向鎖(通過CAS修改對象頭里的鎖標志位),字面意思是“偏向于第一個獲得它的線程”的鎖。執行完同步代碼塊后,線程并不會主動釋放偏向鎖。當第二次到達同步代碼塊時,線程會判斷此時持有鎖的線程是否就是自己(持有鎖的線程ID也在對象頭里),如果是則正常往下執行。由于之前沒有釋放鎖,這里也就不需要重新加鎖。如果自始至終使用鎖的線程只有一個,很明顯偏向鎖幾乎沒有額外開銷,性能極高。
一旦有第二個線程加入鎖競爭,偏向鎖就升級為輕量級鎖(自旋鎖)。在輕量級鎖狀態下繼續鎖競爭,沒有搶到鎖的線程將自旋,即不停地循環判斷鎖是否能夠被成功獲取。獲取鎖的操作,其實就是通過CAS修改對象頭里的鎖標志位。先比較當前鎖標志位是否為“釋放”,如果是則將其設置為“鎖定”,比較并設置是原子性發生的。這就算搶到鎖了,然后線程將當前鎖的持有者信息修改為自己。
長時間的自旋操作是非常消耗資源的,一個線程持有鎖,其他線程就只能在原地空耗CPU,執行不了任何有效的任務,這種現象叫做忙等(busy-waiting)。如果多個線程用一個鎖,但是沒有發生鎖競爭,或者發生了很輕微的鎖競爭,那么synchronized就用輕量級鎖,允許短時間的忙等現象。這是一種折衷的想法,短時間的忙等,換取線程在用戶態和內核態之間切換的開銷。
此忙等是有限度的(有個計數器記錄自旋次數,默認允許循環10次,可以通過虛擬機參數更改)。如果鎖競爭情況嚴重,某個達到最大自旋次數的線程,會將輕量級鎖升級為重量級鎖(依然是CAS修改鎖標志位,但不修改持有鎖的線程ID)。當后續線程嘗試獲取鎖時,發現被占用的鎖是重量級鎖,則直接將自己掛起(而不是忙等),等待將來被喚醒。
獨占鎖與共享鎖
獨占鎖:隊首持鎖,喚醒隊二后tryAcquire嘗試拿鎖,隊三及以后休眠
共享鎖:相較于獨占模式只喚醒隊二 ,共享模式還喚醒所有mode=shared節點
獨占和共享是對于加鎖而言(能否多線程同時獲鎖),釋放鎖時沒有獨占和共享的概念。
怎么實現同步
根據參考文章1,提出了4中方式實現同步:
1:自旋實現同步
缺點:耗費cpu資源。沒有競爭到鎖的線程會一直占用cpu資源進行cas操作
2:yield+自旋實現同步
優點:要解決自旋鎖的性能問題必須讓競爭鎖失敗的線程不空轉,而是在獲取不到鎖的時候能把cpu資源給讓出來,yield()方法就能讓出cpu資源,當線程競爭鎖失敗時,會調用yield方法讓出cpu。
缺點:自旋+yield的方式并沒有完全解決問題,當系統只有兩個線程競爭鎖時,yield是有效的。需要注意的是該方法只是當前讓出cpu,有可能操作系統下次還是選擇運行該線程
3:sleep+自旋方式實現同步
缺點:sleep的時間怎設置
4:park+自旋方式實現同步
volatile int status=0;
Queue parkQueue;//集合 數組 list
void lock(){
while(!compareAndSet(0,1)){
//
park();
}
//lock 10分鐘
unlock()
}
void unlock(){
lock_notify();
}
void park(){
//將當期線程加入到等待隊列
parkQueue.add(currentThread);
//將當期線程釋放cpu 阻塞
releaseCpu();
}
void lock_notify(){
//得到要喚醒的線程頭部線程
Thread t=parkQueue.header();
//喚醒等待線程
unpark(t);
}
ReentrantLock源碼分析
而我們的ReentrantLock就是采用第四種方法實現的。
首先來看一段代碼
public class MyRunnable implements Runnable {
private int num = 0;
private ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (num < 20){
lock.lock();
try{
num++;
Log.e("zzf",Thread.currentThread().getName() + "獲得鎖,num is"+ num);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}
初始化鎖實例
首先來看構造函數,
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
有兩種方式,無參的默認new 一個非公平鎖NonfairSync。
在我們上面的例子中,調用了第二個構造函數,并且傳入的true,所以使用的是公平鎖。(后續都以公平鎖進行分析)
lock.lock()
public void lock() {
sync.lock();
}
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
調用lock()時,首先調用acquire(),對于acquire()分兩部分來講。
tryAcquire
這個方法是嘗試去獲取鎖。
protected final boolean tryAcquire(int var1) {
Thread var2 = Thread.currentThread();
int var3 = this.getState();
if (var3 == 0) {
if (!this.hasQueuedPredecessors() && this.compareAndSetState(0, var1)) {
this.setExclusiveOwnerThread(var2);
return true;
}
} else if (var2 == this.getExclusiveOwnerThread()) {
int var4 = var3 + var1;
if (var4 < 0) {
throw new Error("Maximum lock count exceeded");
}
this.setState(var4);
return true;
}
return false;
}
}
要想知道上面代碼的意思,我們需要先了解一下AQS的一個靜態內部類Node;
static final class Node {
static final AbstractQueuedSynchronizer.Node SHARED = new AbstractQueuedSynchronizer.Node();//表示共享模式
static final AbstractQueuedSynchronizer.Node EXCLUSIVE = null;//表示獨占模式
static final int CANCELLED = 1;//因為超時或者中斷,結點會被設置為取消狀態,被取消狀態的結點不應該去競爭鎖,只能保持取消狀態不變,不能轉換為其他狀態。處于這種狀態的結點會被踢出隊列,被GC回收;
static final int SIGNAL = -1;//表示這個結點的繼任結點被阻塞了,到時需要通知它
static final int CONDITION = -2;//表示這個結點在條件隊列中,因為等待某個條件而被阻塞;
static final int PROPAGATE = -3;//使用在共享模式頭結點有可能牌處于這種狀態,表示鎖的下一次獲取可以無條件傳播;
volatile int waitStatus;//用來表示上面4種狀態
volatile AbstractQueuedSynchronizer.Node prev;//前一個節點
volatile AbstractQueuedSynchronizer.Node next;//后一個節點
volatile Thread thread;//表示線程
Node nextWaiter;//條件隊列才使用。
}
getState()得到的是一個int類型的volatile int state;被volatile修飾。默認為0,表示無鎖狀態,1表示上鎖狀態,>1表示重入。
當第一個線程進來,此時getState()為0,所以進入第一個if中。
public final boolean hasQueuedPredecessors() {
AbstractQueuedSynchronizer.Node var1 = this.tail;
AbstractQueuedSynchronizer.Node var2 = this.head;
AbstractQueuedSynchronizer.Node var3;
return var2 != var1 && ((var3 = var2.next) == null || var3.thread != Thread.currentThread());
}
該方法是判斷是否需要排隊。
在這需要分三種情況討論:
1:隊列沒有初始化
當隊列沒有初始化的時候,var1 = null,var2 = null.所以hasQueuedPredecessors()返回false。在第一個if里面進行取反,則表示不需要入隊列處理,直接CAS進行加鎖操作。并把當前線程設置成Node的thread。然后返回true,而此時在acquire()中又是取反,則不會走if里面的代碼。從這里可以看出,其實加鎖就是讓獲取到這個鎖的線程能夠順暢的往下執行邏輯。
2:隊列初始化且>1個節點
在隊列初始化的時候,此時會new一個Node,放在最前面,而在隊列的第二才是真正排隊的第一個,而new 出來的那個Node可以理解為第一個拿到鎖的那個的占位。當大于一個節點的時候var2 != var1為true,因為var2最開始為null,現在>1個節點,表示var2.next是有值的,所以(var3 = var2.next) == null為false。
var3.thread != Thread.currentThread())為true時表示前面已經有人在排隊,那我就必須要排隊。所以hasQueuedPredecessors()返回true。此時則會跳出第一個if。tryAcquire返回flase,則需在acquire()判斷acquireQueued(addWaiter(Node.EXCLUSIVE), arg)在后面講解。
var3.thread != Thread.currentThread()為flase時,則不需要排隊,最簡單的理解是前面排隊的是自己的女朋友,那她買票等價于我買票了,就不需要排隊了。
3:隊列初始化只有一個節點
當只有一個節點的時候,相當于就只有初始化隊列的時候new的那個Node。那此時隊首和隊尾就是它自己,但是它又是不算排隊的那個,只是算正在持有鎖的那個線程在隊列中的一個占位而已。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
當處于上面講的第二種情況時,就會執行這段代碼。addWaiter()是將當前線程封裝成Node,然后添加到AQS隊列中。
private Node addWaiter(Node mode) {
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
U.putObject(node, Node.PREV, oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
initializeSyncQueue();
}
}
}
將當前的線程封裝成Node并且mode為獨占鎖。
然后進行一個無限循環,第一次oldTail為null,則進入到else中,進行初始化。第二次進來的時候,oldTail則不為null,將當前線程Node的prev節點指向tail,然后通過cas將node加入AQS隊列,成功之后,把舊的tail的next指向新的tail。所以AQS并不是第一的時候就進行初始化。
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
首先獲取前一個節點,只有當前節點的前一個節點是head,表示當前節點在隊列中是第二個元素。AQS是根據FIFO來的,所以當前一個釋放了鎖,只有隊列中的第二個才能去獲取鎖。如果索取鎖成功,則吧當前的節點設置為head。并把前一個head斷開。因為為head的那個node的node.head 和node.thread都是為null,只需要把node.next置為null時,就斷開了鏈表的鏈接。
如果獲取鎖失敗,則根據waitStatus決定是否需要掛起線程。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
如果前一個節點的waitStatus是SIGNAL,表明當前的的線程需要被unpark。
如果waitStatus > 0,則表明是cancel狀態,則從前節點開始逐步循環找到下一個沒有被cancel的節點設置為當前節點的前節點。目的將cancel狀態的節點踢除掉。如果是其他的狀態,將前節點改成SIGNAL狀態。當返回true時,繼續走下面代碼。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
意思就是通過park()將當前線程掛起到WATING狀態。
lock.unlock()
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
執行unLock的時候,會執行release();
protected final boolean tryRelease(int var1) {
int var2 = this.getState() - var1;
if (Thread.currentThread() != this.getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
} else {
boolean var3 = false;
if (var2 == 0) {
var3 = true;
this.setExclusiveOwnerThread((Thread)null);
}
this.setState(var2);
return var3;
}
}
getState() -1表示可能有重入的,getState()可能>1,只有當var2 ==0時,把當前線程設置為null,并釋放鎖,返回true。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
LockSupport.unpark(s.thread);
}
該方法是真正的釋放鎖,傳入的是head節點,當前線程被釋放后,需要喚醒下一個節點的線程。
這里是從隊列尾部向前遍歷找到最前面的一個waitStatus小于0的節點。如果從前面向后遍歷會導致死循環。
如果獲取到head的next節點不為空,則釋放許可喚醒線程。
Condition
在上面講的是AQS的同步隊列,除了這個同步隊列外,還有一個條件隊列。
加入條件隊列的前提是,當前線程已經拿到了鎖,并處于運行狀態,加入條件隊列后,就需要釋放鎖,進入阻塞狀態,同時喚醒同步隊列的隊二拿鎖。喚醒操作是將一個node從條件隊列,移動到同步隊列的隊尾,讓它返回同步隊列park,并不是隨機就喚醒一個線程。
流程
1、創建節點并添加到條件隊列的尾部。
2、釋放線程所持有的鎖。
3、判斷是否在同步隊列中,在的話,進行CAS拿鎖操作,不在的話,喚醒進入同步隊列的尾部。
public class ConditionDemo {
private int queueSize = 10;
private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
private Lock lock = new ReentrantLock();
private Condition full = lock.newCondition();
private Condition empty = lock.newCondition();
class Consumer implements Runnable{
@Override
public void run() {
consume();
}
private void consume() {
while (true){
lock.lock();
try {
while (queue.size() == 0){
try {
System.out.println("隊列空,等待數據");
empty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.poll();
full.signal();
System.out.println("從隊列取走一個元素,隊列剩余"+queue.size()+"個元素");
}finally {
lock.unlock();
}
}
}
}
class Producer implements Runnable{
@Override
public void run() {
produce();
}
private void produce() {
while (true){
lock.lock();
try {
while(queue.size()== queueSize){
try {
System.out.println("隊列滿,等待有空余空間");
full.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.offer(1);
empty.signal();
}finally {
lock.unlock();
}
}
}
}
}
上面是利用Condition實現生產者消費者模式,當隊列的size為0的時候,empty調用await().
休眠
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
在ConditionObject中,包含了兩個成員對象,一個表示隊列的第一個node,一個表示隊列的最后一個node。
private transient Node firstWaiter;
private transient Node lastWaiter;
private Node addConditionWaiter() {
Node t = lastWaiter;
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
addConditionWaiter就是創建一CONDITION狀態的個node,如果隊列中沒有,則把這個新創建的賦值給首節點,如果有,則賦值給當前節點的后一個。并且這個也是最后一個節點。條件隊列也是先進先出的形式,只不過是單鏈表的形式。
final int fullyRelease(Node node) {
try {
int savedState = getState();
if (release(savedState))
return savedState;
else
throw new IllegalMonitorStateException();
} catch (Throwable t) {
node.waitStatus = Node.CANCELLED;
throw t;
}
}
首先獲取當前的state,然后調用release方法進行釋放鎖。
在第三篇參考博客寫到,fullyRelease會一次性釋放所有的鎖,不管重入多少次。我理解的是重入的時候,還是同一個線程,同一把鎖。所以釋放的只是一把鎖吧。
經過上面的代碼之后,節點已經放到條件隊列并且釋放了所持有的鎖,而后需要掛起。但是在前面說了,掛起需要不在同步隊列,所以需要判斷是否在同步隊列。
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
if (node.next != null) // If has successor, it must be on queue
return true;
return findNodeFromTail(node);
}
private boolean findNodeFromTail(Node node) {
for (Node p = tail;;) {
if (p == node)
return true;
if (p == null)
return false;
p = p.prev;
}
}
當節點的狀態為Node.CONDITION或者node.prev == null時,則不在同步隊列,node.prev,node.next在條件隊列中沒有用到。如果含有這個,則表示是在同步隊列中。
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}else
trail = t;
t = next;
}
}
如果狀態不是CONDITION,就會自動刪除。
喚醒
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
首先判斷是不是同一個線程,不是則拋出異常。然后獲取等待隊列的頭結點,后續的操作都是基于該節點。
final boolean transferForSignal(Node node) {
if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
return false;
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
private Node enq(Node node) {
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
U.putObject(node, Node.PREV, oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return oldTail;
}
} else {
initializeSyncQueue();
}
}
}
enq()是將該節點移入到同步隊列中去。
signal是只會對等待隊列的頭結點進行操作,而signalAll則是對每一個節點都移入到同步隊列中。
同步隊列與條件隊列完整配合如下圖所示:
參考文獻
https://blog.csdn.net/java_lyvee/article/details/98966684